From ce0660a537f2e4e7abf0bf8bda48812734e7ae85 Mon Sep 17 00:00:00 2001 From: Peter Vachon Date: Wed, 8 Apr 2026 14:05:28 -0400 Subject: [PATCH 01/35] chore(apollo-vertex): AI Chat isolated --- .../ai-chat/preview/mock-data.ts | 200 ++++++++++ .../ai-chat/preview/page.tsx | 366 ++++++++++++++++++ apps/apollo-vertex/lib/auth.ts | 2 +- apps/apollo-vertex/next-env.d.ts | 2 +- apps/apollo-vertex/registry.json | 79 +++- .../registry/accordion/accordion.tsx | 2 +- .../ai-chat/adapters/agenthub/tools.ts | 2 +- .../ai-chat/components/ai-chat-code-block.tsx | 66 ++++ .../components/ai-chat-empty-state.tsx | 56 +++ .../ai-chat/components/ai-chat-input.tsx | 156 ++++++-- .../ai-chat/components/ai-chat-loading.tsx | 51 ++- .../ai-chat/components/ai-chat-markdown.tsx | 60 +-- .../components/ai-chat-message-actions.tsx | 157 ++++++++ .../ai-chat/components/ai-chat-message.tsx | 157 +++++++- .../ai-chat/components/ai-chat-provider.tsx | 39 ++ .../components/ai-chat-suggestions.tsx | 22 +- .../registry/ai-chat/components/ai-chat.tsx | 238 +++++++----- apps/apollo-vertex/registry/ai-chat/types.ts | 36 ++ .../registry/alert-dialog/alert-dialog.tsx | 2 +- apps/apollo-vertex/registry/avatar/avatar.tsx | 2 +- apps/apollo-vertex/registry/badge/badge.tsx | 2 +- .../registry/breadcrumb/breadcrumb.tsx | 2 +- apps/apollo-vertex/registry/button/button.tsx | 2 +- apps/apollo-vertex/registry/card/card.tsx | 2 +- .../registry/checkbox/checkbox.tsx | 2 +- .../registry/command/command.tsx | 2 +- .../registry/context-menu/context-menu.tsx | 2 +- .../data-table/data-table-column-header.tsx | 2 +- .../registry/data-table/data-table-row.tsx | 2 +- .../registry/date-picker/date-picker.tsx | 2 +- apps/apollo-vertex/registry/dialog/dialog.tsx | 2 +- apps/apollo-vertex/registry/drawer/drawer.tsx | 2 +- .../registry/dropdown-menu/dropdown-menu.tsx | 2 +- .../registry/hover-card/hover-card.tsx | 2 +- apps/apollo-vertex/registry/input/input.tsx | 2 +- apps/apollo-vertex/registry/label/label.tsx | 2 +- .../registry/menubar/menubar.tsx | 2 +- .../navigation-menu/navigation-menu.tsx | 2 +- .../registry/pagination/pagination.tsx | 2 +- .../registry/popover/popover.tsx | 2 +- .../registry/progress/progress.tsx | 2 +- .../registry/radio-group/radio-group.tsx | 2 +- .../registry/resizable/resizable.tsx | 2 +- .../registry/scroll-area/scroll-area.tsx | 2 +- apps/apollo-vertex/registry/select/select.tsx | 2 +- .../registry/separator/separator.tsx | 2 +- apps/apollo-vertex/registry/sheet/sheet.tsx | 2 +- .../registry/shell/shell-nav-item.tsx | 2 +- .../registry/sidebar/sidebar-content.tsx | 2 +- .../registry/sidebar/sidebar-footer.tsx | 2 +- .../registry/sidebar/sidebar-group-action.tsx | 2 +- .../sidebar/sidebar-group-content.tsx | 2 +- .../registry/sidebar/sidebar-group-label.tsx | 2 +- .../registry/sidebar/sidebar-group.tsx | 2 +- .../registry/sidebar/sidebar-header.tsx | 2 +- .../registry/sidebar/sidebar-input.tsx | 2 +- .../registry/sidebar/sidebar-inset.tsx | 2 +- .../registry/sidebar/sidebar-menu-action.tsx | 2 +- .../registry/sidebar/sidebar-menu-badge.tsx | 2 +- .../registry/sidebar/sidebar-menu-button.tsx | 2 +- .../registry/sidebar/sidebar-menu-item.tsx | 2 +- .../sidebar/sidebar-menu-sub-button.tsx | 2 +- .../sidebar/sidebar-menu-sub-item.tsx | 2 +- .../registry/sidebar/sidebar-menu-sub.tsx | 2 +- .../registry/sidebar/sidebar-menu.tsx | 2 +- .../registry/sidebar/sidebar-rail.tsx | 2 +- .../registry/sidebar/sidebar-separator.tsx | 2 +- .../registry/sidebar/sidebar-trigger.tsx | 2 +- .../registry/sidebar/sidebar.tsx | 2 +- apps/apollo-vertex/registry/switch/switch.tsx | 2 +- apps/apollo-vertex/registry/table/table.tsx | 2 +- apps/apollo-vertex/registry/tabs/tabs.tsx | 2 +- .../registry/textarea/textarea.tsx | 2 +- apps/apollo-vertex/registry/toggle/toggle.tsx | 2 +- .../registry/tooltip/tooltip.tsx | 2 +- .../use-data-table/useEntityDataTable.tsx | 3 +- .../use-data-table/usePersistedColumnOrder.ts | 3 +- .../use-data-table/usePersistedSorting.ts | 3 +- .../templates/ai-chat/AiChatLoginGate.tsx | 2 +- 79 files changed, 1565 insertions(+), 251 deletions(-) create mode 100644 apps/apollo-vertex/app/vertex-components/ai-chat/preview/mock-data.ts create mode 100644 apps/apollo-vertex/app/vertex-components/ai-chat/preview/page.tsx create mode 100644 apps/apollo-vertex/registry/ai-chat/components/ai-chat-code-block.tsx create mode 100644 apps/apollo-vertex/registry/ai-chat/components/ai-chat-empty-state.tsx create mode 100644 apps/apollo-vertex/registry/ai-chat/components/ai-chat-message-actions.tsx create mode 100644 apps/apollo-vertex/registry/ai-chat/components/ai-chat-provider.tsx diff --git a/apps/apollo-vertex/app/vertex-components/ai-chat/preview/mock-data.ts b/apps/apollo-vertex/app/vertex-components/ai-chat/preview/mock-data.ts new file mode 100644 index 000000000..8601abe16 --- /dev/null +++ b/apps/apollo-vertex/app/vertex-components/ai-chat/preview/mock-data.ts @@ -0,0 +1,200 @@ +import type { UIMessage } from "@tanstack/ai-client"; + +export const MOCK_MESSAGES_BASIC: UIMessage[] = [ + { + id: "1", + role: "user", + parts: [{ type: "text", content: "What is Apollo Design System?" }], + createdAt: new Date(Date.now() - 120000), + }, + { + id: "2", + role: "assistant", + parts: [ + { + type: "text", + content: + "Apollo is UiPath's open-source design system. It provides a unified component library with **design tokens**, **React components**, and **Web Components** for building consistent user interfaces.", + }, + ], + createdAt: new Date(Date.now() - 60000), + }, +]; + +export const MOCK_MESSAGES_MARKDOWN: UIMessage[] = [ + { + id: "1", + role: "user", + parts: [ + { + type: "text", + content: "Show me all the markdown formatting you support.", + }, + ], + createdAt: new Date(), + }, + { + id: "2", + role: "assistant", + parts: [ + { + type: "text", + content: `Here's a comprehensive demo of supported markdown: + +## Headings & Text + +This is a paragraph with **bold**, *italic*, and \`inline code\`. + +> This is a blockquote with some important information. + +--- + +## Lists + +**Unordered list:** +- First item +- Second item with **bold** +- Third item + +**Ordered list:** +1. Step one +2. Step two +3. Step three + +## Code Block + +\`\`\`typescript +interface AiChatProps { + messages: UIMessage[]; + isLoading: boolean; + onSendMessage: (content: string) => void; +} +\`\`\` + +## Table + +| Component | Status | Description | +|-----------|--------|-------------| +| AiChat | Ready | Main chat shell | +| AiChatMessage | Ready | Message renderer | +| AiChatInput | Ready | Text input | +| AiChatMarkdown | Ready | Markdown renderer | + +## Links + +Check [Apollo UI](https://github.com/UiPath/apollo-ui) for more info.`, + }, + ], + createdAt: new Date(), + }, +]; + +export const MOCK_MESSAGES_CONVERSATION: UIMessage[] = [ + { + id: "1", + role: "user", + parts: [{ type: "text", content: "Can you help me set up a new project?" }], + createdAt: new Date(Date.now() - 300000), + }, + { + id: "2", + role: "assistant", + parts: [ + { + type: "text", + content: + "Of course! I'd be happy to help. What kind of project are you looking to create?", + }, + ], + createdAt: new Date(Date.now() - 240000), + }, + { + id: "3", + role: "user", + parts: [ + { + type: "text", + content: "A React app with Tailwind CSS and TypeScript.", + }, + ], + createdAt: new Date(Date.now() - 180000), + }, + { + id: "4", + role: "assistant", + parts: [ + { + type: "text", + content: + "Great choice! Here's how to get started:\n\n1. Run `npx create-next-app@latest`\n2. Select **TypeScript** and **Tailwind CSS** during setup\n3. Install Apollo components:\n\n```bash\nnpx shadcn@latest add @uipath/button\n```\n\nWould you like me to walk through any of these steps in detail?", + }, + ], + createdAt: new Date(Date.now() - 120000), + }, + { + id: "5", + role: "user", + parts: [{ type: "text", content: "Yes, explain step 3 please." }], + createdAt: new Date(Date.now() - 60000), + }, + { + id: "6", + role: "assistant", + parts: [ + { + type: "text", + content: + "The `shadcn` CLI copies component source code directly into your project. When you run the command, it will:\n\n- Create `components/ui/button.tsx` in your project\n- Add any required dependencies to `package.json`\n- Set up the `cn()` utility if not present\n\nYou own the code — modify it however you need.", + }, + ], + createdAt: new Date(), + }, +]; + +export const MOCK_MESSAGES_WITH_CHOICES: UIMessage[] = [ + { + id: "1", + role: "user", + parts: [{ type: "text", content: "What presentation style should I use?" }], + createdAt: new Date(), + }, + { + id: "2", + role: "assistant", + parts: [ + { + type: "text", + content: "Here are some options based on your audience:", + }, + { + type: "tool-call", + id: "call-1", + name: "presentChoices", + arguments: "{}", + state: "input-complete" as const, + }, + { + type: "tool-result", + toolCallId: "call-1", + state: "complete" as const, + content: JSON.stringify({ + type: "choices", + prompt: "Pick a presentation style:", + options: [ + { id: "1", label: "Executive Summary", recommended: true }, + { id: "2", label: "Technical Deep Dive" }, + { id: "3", label: "Workshop Format" }, + ], + }), + }, + ], + createdAt: new Date(), + }, +]; + +export const MOCK_CHOICE_OPTIONS = [ + { id: "1", label: "Option A — Recommended", recommended: true }, + { id: "2", label: "Option B — Alternative" }, + { id: "3", label: "Option C — Experimental" }, + { id: "4", label: "Option D — Conservative" }, +]; diff --git a/apps/apollo-vertex/app/vertex-components/ai-chat/preview/page.tsx b/apps/apollo-vertex/app/vertex-components/ai-chat/preview/page.tsx new file mode 100644 index 000000000..363b9d439 --- /dev/null +++ b/apps/apollo-vertex/app/vertex-components/ai-chat/preview/page.tsx @@ -0,0 +1,366 @@ +"use client"; + +import { AiChat } from "@/registry/ai-chat/components/ai-chat"; +import { AiChatCodeBlock } from "@/registry/ai-chat/components/ai-chat-code-block"; +import { AiChatEmptyState } from "@/registry/ai-chat/components/ai-chat-empty-state"; +import { AiChatInput } from "@/registry/ai-chat/components/ai-chat-input"; +import { AiChatLoading } from "@/registry/ai-chat/components/ai-chat-loading"; +import { AiChatMarkdown } from "@/registry/ai-chat/components/ai-chat-markdown"; +import { AiChatMessage } from "@/registry/ai-chat/components/ai-chat-message"; +import { AiChatSuggestions } from "@/registry/ai-chat/components/ai-chat-suggestions"; +import { + MOCK_CHOICE_OPTIONS, + MOCK_MESSAGES_BASIC, + MOCK_MESSAGES_CONVERSATION, + MOCK_MESSAGES_MARKDOWN, + MOCK_MESSAGES_WITH_CHOICES, +} from "./mock-data"; + +function noop() { + // no-op for preview +} + +function SectionHeader({ + title, + description, +}: { + title: string; + description: string; +}) { + return ( +
+

{title}

+

{description}

+
+ ); +} + +function PreviewCard({ + children, + className, +}: { + children: React.ReactNode; + className?: string; +}) { + return ( +
+ {children} +
+ ); +} + +export default function AiChatPreviewPage() { + return ( +
+
+

+ {"AI Chat — Component Preview"} +

+

+ {"All visual states and sub-components rendered with mock data."} +

+
+ + {/* ── 1. Empty State (default) ────────────────────────── */} +
+ + + + +
+ + {/* ── 2. Empty State (with suggestions) ──────────────── */} +
+ + + + } + /> + +
+ + {/* ── 3. Basic Conversation with timestamps ──────────── */} +
+ + + + {MOCK_MESSAGES_BASIC.map((msg) => ( + + ))} + + +
+ + {/* ── 4. Multi-turn Conversation ─────────────────────── */} +
+ + + + {MOCK_MESSAGES_CONVERSATION.map((msg) => ( + + ))} + + +
+ + {/* ── 5. Markdown + Code Block ───────────────────────── */} +
+ + + + {MOCK_MESSAGES_MARKDOWN.map((msg) => ( + + ))} + + +
+ + {/* ── 6. Loading States ──────────────────────────────── */} +
+ +
+ + + + + + +

+ {"Dots variant (standalone)"} +

+ +
+
+
+ + {/* ── 7. Error + Retry ───────────────────────────────── */} +
+ + + + {MOCK_MESSAGES_BASIC.map((msg) => ( + + ))} + + +
+ + {/* ── 8. Suggestion Buttons ──────────────────────────── */} +
+ + + + {MOCK_MESSAGES_WITH_CHOICES.map((msg) => ( + + ))} + + +
+ + {/* ── 9. Variant Comparison ──────────────────────────── */} +
+ +
+ + + {MOCK_MESSAGES_BASIC.map((msg) => ( + + ))} + + + + + {MOCK_MESSAGES_BASIC.map((msg) => ( + + ))} + + + + + {MOCK_MESSAGES_BASIC.map((msg) => ( + + ))} + + +
+
+ + {/* ── 10. Isolated Sub-Components ────────────────────── */} +
+ +
+ +

+ {"AiChatSuggestions (with recommended)"} +

+ +
+ + +

+ {"AiChatInput (with text + clear)"} +

+ +
+ + +

+ {"AiChatCodeBlock"} +

+ + {`import { AiChat } from "@/components/ui/ai-chat"; + +function App() { + return ; +}`} + +
+ + +

+ {"AiChatMarkdown (standalone)"} +

+ + {`Here's a quick example with **bold**, *italic*, \`code\`, and a [link](https://example.com).\n\n| Feature | Supported |\n|---------|----------|\n| Tables | Yes |\n| Code | Yes |`} + +
+
+
+
+ ); +} diff --git a/apps/apollo-vertex/lib/auth.ts b/apps/apollo-vertex/lib/auth.ts index 3f8cfcc4e..3dff14828 100644 --- a/apps/apollo-vertex/lib/auth.ts +++ b/apps/apollo-vertex/lib/auth.ts @@ -1,4 +1,4 @@ -import { useQueryClient } from "@tanstack/react-query"; +import type { useQueryClient } from "@tanstack/react-query"; import PKCEChallenge from "pkce-challenge"; import { toast } from "sonner"; import { z } from "zod"; diff --git a/apps/apollo-vertex/next-env.d.ts b/apps/apollo-vertex/next-env.d.ts index 9edff1c7c..c4b7818fb 100644 --- a/apps/apollo-vertex/next-env.d.ts +++ b/apps/apollo-vertex/next-env.d.ts @@ -1,6 +1,6 @@ /// /// -import "./.next/types/routes.d.ts"; +import "./.next/dev/types/routes.d.ts"; // NOTE: This file should not be edited // see https://nextjs.org/docs/app/api-reference/config/typescript for more information. diff --git a/apps/apollo-vertex/registry.json b/apps/apollo-vertex/registry.json index 170d79712..30c12d26c 100644 --- a/apps/apollo-vertex/registry.json +++ b/apps/apollo-vertex/registry.json @@ -66,6 +66,20 @@ "color-sidebar-accent-foreground": "var(--sidebar-accent-foreground)", "color-sidebar-border": "var(--sidebar-border)", "color-sidebar-ring": "var(--sidebar-ring)", + "color-ai-chat": "var(--ai-chat)", + "color-ai-chat-foreground": "var(--ai-chat-foreground)", + "color-ai-chat-accent": "var(--ai-chat-accent)", + "color-ai-chat-accent-foreground": "var(--ai-chat-accent-foreground)", + "color-ai-chat-bubble-user": "var(--ai-chat-bubble-user)", + "color-ai-chat-bubble-user-foreground": "var(--ai-chat-bubble-user-foreground)", + "color-ai-chat-bubble-assistant": "var(--ai-chat-bubble-assistant)", + "color-ai-chat-bubble-assistant-foreground": "var(--ai-chat-bubble-assistant-foreground)", + "color-ai-chat-border": "var(--ai-chat-border)", + "color-ai-chat-input": "var(--ai-chat-input)", + "color-ai-chat-input-foreground": "var(--ai-chat-input-foreground)", + "color-ai-chat-ring": "var(--ai-chat-ring)", + "color-ai-chat-muted": "var(--ai-chat-muted)", + "color-ai-chat-muted-foreground": "var(--ai-chat-muted-foreground)", "font-sans": "var(--font-sans)" }, "light": { @@ -120,6 +134,23 @@ "sidebar-accent-foreground": "oklch(0.1660 0.0283 203.3380)", "sidebar-border": "oklch(0.9237 0.0133 262.3780)", "sidebar-ring": "oklch(0.64 0.115 208)", + "ai-chat": "oklch(0.985 0.002 260)", + "ai-chat-foreground": "oklch(0.2394 0.0455 252.45)", + "ai-chat-accent": "oklch(0.64 0.115 208)", + "ai-chat-accent-foreground": "oklch(1 0 0)", + "ai-chat-bubble-user": "oklch(0.955 0.008 252)", + "ai-chat-bubble-user-foreground": "oklch(0.2394 0.0455 252.45)", + "ai-chat-bubble-assistant": "oklch(0.96 0.020 275)", + "ai-chat-bubble-assistant-foreground": "oklch(0.2394 0.0455 252.45)", + "ai-chat-border": "oklch(0.92 0.008 260)", + "ai-chat-input": "oklch(0.975 0.004 260)", + "ai-chat-input-foreground": "oklch(0.2394 0.0455 252.45)", + "ai-chat-ring": "oklch(0.64 0.115 208)", + "ai-chat-muted": "oklch(0.955 0.006 260)", + "ai-chat-muted-foreground": "oklch(0.50 0.020 260)", + "font-sans": "Inter, ui-sans-serif, sans-serif, system-ui", + "font-serif": "IBM Plex Serif, ui-serif, serif", + "font-mono": "IBM Plex Mono, ui-monospace, monospace", "radius": "0.625rem", "shadow-x": "0", "shadow-y": "0px", @@ -206,6 +237,23 @@ "sidebar-accent-foreground": "oklch(0.9525 0.0110 225.9830)", "sidebar-border": "oklch(0.9525 0.0110 225.9830)", "sidebar-ring": "oklch(0.69 0.112 207)", + "ai-chat": "oklch(0.19 0.025 260)", + "ai-chat-foreground": "oklch(0.95 0.010 225)", + "ai-chat-accent": "oklch(0.69 0.112 207)", + "ai-chat-accent-foreground": "oklch(0.1660 0.0283 203.34)", + "ai-chat-bubble-user": "oklch(0.28 0.035 255)", + "ai-chat-bubble-user-foreground": "oklch(0.95 0.010 225)", + "ai-chat-bubble-assistant": "oklch(0.25 0.030 270)", + "ai-chat-bubble-assistant-foreground": "oklch(0.95 0.010 225)", + "ai-chat-border": "oklch(0.30 0.040 258)", + "ai-chat-input": "oklch(0.22 0.030 258)", + "ai-chat-input-foreground": "oklch(0.95 0.010 225)", + "ai-chat-ring": "oklch(0.69 0.112 207)", + "ai-chat-muted": "oklch(0.26 0.030 258)", + "ai-chat-muted-foreground": "oklch(0.65 0.020 258)", + "font-sans": "Inter, ui-sans-serif, sans-serif, system-ui", + "font-serif": "IBM Plex Serif, ui-serif, serif", + "font-mono": "IBM Plex Mono, ui-monospace, monospace", "radius": "0.625rem", "shadow-x": "0", "shadow-y": "0px", @@ -272,7 +320,16 @@ "react-markdown", "remark-gfm" ], - "registryDependencies": [], + "registryDependencies": [ + "avatar", + "button", + "tooltip", + "dropdown-menu", + "scroll-area", + "skeleton", + "badge", + "kbd" + ], "files": [ { "path": "registry/ai-chat/types.ts", @@ -339,6 +396,11 @@ "type": "registry:lib", "target": "components/ui/ai-chat/hooks/use-sticky-scroll.ts" }, + { + "path": "registry/ai-chat/components/ai-chat-provider.tsx", + "type": "registry:ui", + "target": "components/ui/ai-chat/components/ai-chat-provider.tsx" + }, { "path": "registry/ai-chat/components/ai-chat.tsx", "type": "registry:ui", @@ -368,6 +430,21 @@ "path": "registry/ai-chat/components/ai-chat-suggestions.tsx", "type": "registry:ui", "target": "components/ui/ai-chat/components/ai-chat-suggestions.tsx" + }, + { + "path": "registry/ai-chat/components/ai-chat-message-actions.tsx", + "type": "registry:ui", + "target": "components/ui/ai-chat/components/ai-chat-message-actions.tsx" + }, + { + "path": "registry/ai-chat/components/ai-chat-code-block.tsx", + "type": "registry:ui", + "target": "components/ui/ai-chat/components/ai-chat-code-block.tsx" + }, + { + "path": "registry/ai-chat/components/ai-chat-empty-state.tsx", + "type": "registry:ui", + "target": "components/ui/ai-chat/components/ai-chat-empty-state.tsx" } ] }, diff --git a/apps/apollo-vertex/registry/accordion/accordion.tsx b/apps/apollo-vertex/registry/accordion/accordion.tsx index f0428981e..d09272c18 100644 --- a/apps/apollo-vertex/registry/accordion/accordion.tsx +++ b/apps/apollo-vertex/registry/accordion/accordion.tsx @@ -2,7 +2,7 @@ import * as AccordionPrimitive from "@radix-ui/react-accordion"; import { ChevronDownIcon } from "lucide-react"; -import * as React from "react"; +import type * as React from "react"; import { cn } from "@/lib/utils"; diff --git a/apps/apollo-vertex/registry/ai-chat/adapters/agenthub/tools.ts b/apps/apollo-vertex/registry/ai-chat/adapters/agenthub/tools.ts index 855440c6e..26488eca0 100644 --- a/apps/apollo-vertex/registry/ai-chat/adapters/agenthub/tools.ts +++ b/apps/apollo-vertex/registry/ai-chat/adapters/agenthub/tools.ts @@ -1,4 +1,4 @@ -import { convertSchemaToJsonSchema, type AnyClientTool } from "@tanstack/ai"; +import { type AnyClientTool, convertSchemaToJsonSchema } from "@tanstack/ai"; import type { AgentHubVendor } from "./types"; export function buildToolDefinitions( diff --git a/apps/apollo-vertex/registry/ai-chat/components/ai-chat-code-block.tsx b/apps/apollo-vertex/registry/ai-chat/components/ai-chat-code-block.tsx new file mode 100644 index 000000000..8601d3d96 --- /dev/null +++ b/apps/apollo-vertex/registry/ai-chat/components/ai-chat-code-block.tsx @@ -0,0 +1,66 @@ +"use client"; + +import { Check, ClipboardCopy } from "lucide-react"; +import { useState } from "react"; +import { + Tooltip, + TooltipContent, + TooltipTrigger, +} from "@/registry/tooltip/tooltip"; + +const COPY_LABEL = "Copy code"; +const COPIED_LABEL = "Copied!"; + +interface AiChatCodeBlockProps { + children: string; + language?: string; +} + +export function AiChatCodeBlock({ children, language }: AiChatCodeBlockProps) { + const [copied, setCopied] = useState(false); + + const handleCopy = async () => { + await navigator.clipboard.writeText(children); + setCopied(true); + setTimeout(() => setCopied(false), 2000); + }; + + const copyLabel = copied ? COPIED_LABEL : COPY_LABEL; + + return ( +
+
+ {language && ( + + {language} + + )} + + + + + {copyLabel} + +
+
+        
+          {children}
+        
+      
+
+ ); +} diff --git a/apps/apollo-vertex/registry/ai-chat/components/ai-chat-empty-state.tsx b/apps/apollo-vertex/registry/ai-chat/components/ai-chat-empty-state.tsx new file mode 100644 index 000000000..344c7ff1b --- /dev/null +++ b/apps/apollo-vertex/registry/ai-chat/components/ai-chat-empty-state.tsx @@ -0,0 +1,56 @@ +"use client"; + +import { Sparkles } from "lucide-react"; +import type { ReactNode } from "react"; +import { Button } from "@/registry/button/button"; + +interface AiChatEmptyStateProps { + title?: string; + description?: string; + icon?: ReactNode; + suggestions?: string[]; + onSuggestionClick?: (suggestion: string) => void; +} + +export function AiChatEmptyState({ + title = "How can I help you?", + description, + icon, + suggestions, + onSuggestionClick, +}: AiChatEmptyStateProps) { + return ( +
+
+ {icon ?? ( +
+
+

{title}

+ {description &&

{description}

} +
+ {suggestions && suggestions.length > 0 && onSuggestionClick && ( +
+ {suggestions.map((suggestion) => ( + + ))} +
+ )} +
+ ); +} diff --git a/apps/apollo-vertex/registry/ai-chat/components/ai-chat-input.tsx b/apps/apollo-vertex/registry/ai-chat/components/ai-chat-input.tsx index f0da010fe..6184fec2a 100644 --- a/apps/apollo-vertex/registry/ai-chat/components/ai-chat-input.tsx +++ b/apps/apollo-vertex/registry/ai-chat/components/ai-chat-input.tsx @@ -1,8 +1,18 @@ "use client"; -import { Send, Square, Trash2 } from "lucide-react"; -import type { FormEvent, KeyboardEvent } from "react"; +import { Paperclip, Send, Square, Trash2 } from "lucide-react"; +import { type FormEvent, type KeyboardEvent, useRef } from "react"; import { useTranslation } from "react-i18next"; +import { Button } from "@/registry/button/button"; +import { + Tooltip, + TooltipContent, + TooltipTrigger, +} from "@/registry/tooltip/tooltip"; +import { useAiChat } from "./ai-chat-provider"; + +const UPLOAD_LABEL = "Attach files"; +const ENTER_HINT = "Enter to send, Shift+Enter for new line"; interface AiChatInputProps { value: string; @@ -10,11 +20,13 @@ interface AiChatInputProps { onSubmit: () => void; onStop: () => void; onClear?: () => void; + onFileSelect?: (files: FileList) => void; isLoading: boolean; disabled?: boolean; placeholder?: string; showClearButton?: boolean; hasMessages?: boolean; + maxLength?: number; } export function AiChatInput({ @@ -23,18 +35,42 @@ export function AiChatInput({ onSubmit, onStop, onClear, + onFileSelect, isLoading, disabled = false, placeholder, showClearButton = true, hasMessages = false, + maxLength, }: AiChatInputProps) { const { t } = useTranslation(); + const { variant } = useAiChat(); + const textareaRef = useRef(null); + const isCompact = variant === "compact"; const displayPlaceholder = placeholder ?? t("type_a_message"); + const adjustHeight = () => { + const el = textareaRef.current; + if (!el) return; + el.style.height = "auto"; + el.style.height = `${Math.min(el.scrollHeight, 200)}px`; + }; + + const handleChange = (val: string) => { + if (maxLength && val.length > maxLength) return; + onChange(val); + requestAnimationFrame(adjustHeight); + }; + const submitMessage = () => { if (!value.trim() || isLoading) return; onSubmit(); + requestAnimationFrame(() => { + const el = textareaRef.current; + if (el) { + el.style.height = "auto"; + } + }); }; const handleSubmit = (e: FormEvent) => { @@ -49,55 +85,109 @@ export function AiChatInput({ } }; + const showCharCount = maxLength && value.length > maxLength * 0.8; + return (
-
+
+ {onFileSelect && ( + + + + + {UPLOAD_LABEL} + + )}