eve
Version:
Filesystem-first framework for durable backend AI agents that run anywhere.
12 lines • 150 kB
TypeScript
export declare const WEB_APP_TEMPLATE_FILES: {
readonly "agent/channels/eve.ts": 'import { eveChannel } from "eve/channels/eve";\nimport { localDev, placeholderAuth, vercelOidc } from "eve/channels/auth";\n\nexport default eveChannel({\n auth: [\n // Open on localhost for `eve dev` and the REPL; ignored in production.\n localDev(),\n // Lets the eve TUI and your Vercel deployments reach the deployed agent.\n vercelOidc(),\n // This placeholder will not allow browser requests in production.\n // Replace it with your app\'s auth provider, like Auth.js or Clerk,\n // or use none() for a public demo.\n placeholderAuth(),\n ],\n});\n';
readonly "app/_components/agent-chat.tsx": '"use client";\n\nimport { useEveAgent } from "eve/react";\nimport { AlertCircleIcon } from "lucide-react";\nimport {\n Conversation,\n ConversationContent,\n ConversationScrollButton,\n} from "@/components/ai-elements/conversation";\nimport {\n PromptInput,\n type PromptInputMessage,\n PromptInputSubmit,\n PromptInputTextarea,\n} from "@/components/ai-elements/prompt-input";\nimport { cn } from "@/lib/utils";\nimport { AgentMessage } from "./agent-message";\n\nconst AGENT_NAME = "__EVE_INIT_APP_NAME__";\nconst BETA_TERMS_HREF = "https://vercel.com/docs/release-phases/public-beta-agreement";\n\ntype AgentStatus = ReturnType<typeof useEveAgent>["status"];\n\nexport function AgentChat() {\n const agent = useEveAgent();\n const isBusy = agent.status === "submitted" || agent.status === "streaming";\n const isEmpty = agent.data.messages.length === 0;\n\n const handleSubmit = async (message: PromptInputMessage) => {\n const text = message.text.trim();\n if (!text || isBusy) return;\n\n await agent.send({ message: text });\n };\n\n const composer = (\n <PromptInput onSubmit={handleSubmit}>\n <PromptInputTextarea placeholder="Send a message…" />\n <PromptInputSubmit onStop={agent.stop} status={agent.status} />\n </PromptInput>\n );\n\n return (\n <main className="flex h-dvh flex-col overflow-hidden bg-background text-foreground">\n {isEmpty ? null : (\n <header className="flex h-14 shrink-0 items-center justify-center gap-3 pl-4 pr-2">\n <span className="flex min-w-0 items-center gap-2">\n <span className="truncate text-muted-foreground text-sm">{AGENT_NAME}</span>\n <StatusDot status={agent.status} />\n </span>\n <a\n className="rounded-full border border-amber-500/30 px-2 py-0.5 font-medium text-amber-700 text-xs transition-colors hover:bg-amber-500/10 dark:text-amber-300"\n href={BETA_TERMS_HREF}\n rel="noreferrer"\n target="_blank"\n >\n Public preview\n </a>\n </header>\n )}\n\n {agent.error ? (\n <div className="mx-auto w-full max-w-3xl shrink-0 px-4 pt-2 sm:px-6">\n <div className="flex items-start gap-3 rounded-lg border border-destructive/30 bg-destructive/5 px-3 py-2.5 text-sm">\n <AlertCircleIcon className="mt-0.5 size-4 shrink-0 text-destructive" />\n <div>\n <p className="font-medium">Request failed</p>\n <p className="mt-0.5 text-muted-foreground">{agent.error.message}</p>\n </div>\n </div>\n </div>\n ) : null}\n\n {isEmpty ? null : (\n <Conversation className="min-h-0 flex-1">\n <ConversationContent className="mx-auto w-full max-w-3xl gap-6 px-4 py-6 sm:px-6">\n {agent.data.messages.map((message, index) => (\n <AgentMessage\n canRespond={!isBusy}\n isStreaming={\n agent.status === "streaming" && index === agent.data.messages.length - 1\n }\n key={message.id}\n message={message}\n onInputResponses={(inputResponses) => agent.send({ inputResponses })}\n />\n ))}\n </ConversationContent>\n <ConversationScrollButton />\n </Conversation>\n )}\n\n <div\n className={cn(\n "mx-auto w-full px-4 sm:px-6",\n isEmpty\n ? "flex max-w-xl flex-1 flex-col items-center justify-center gap-8 pb-[10vh]"\n : "max-w-3xl shrink-0 pb-6",\n )}\n >\n {isEmpty ? (\n <div className="flex flex-col items-center gap-3 text-center">\n <h1 className="font-medium text-5xl tracking-tighter">{AGENT_NAME}</h1>\n <a\n className="rounded-full border border-amber-500/30 px-2 py-0.5 font-medium text-amber-700 text-xs transition-colors hover:bg-amber-500/10 dark:text-amber-300"\n href={BETA_TERMS_HREF}\n rel="noreferrer"\n target="_blank"\n >\n Public preview\n </a>\n </div>\n ) : null}\n <div className="w-full">{composer}</div>\n </div>\n </main>\n );\n}\n\nfunction StatusDot({ status }: { readonly status: AgentStatus }) {\n const isLive = status === "submitted" || status === "streaming";\n const tone =\n status === "error"\n ? "bg-destructive"\n : isLive\n ? "bg-emerald-500"\n : status === "ready"\n ? "bg-muted-foreground"\n : "bg-muted-foreground/50";\n\n return (\n <span className="relative flex size-1">\n {isLive ? (\n <span\n className={cn(\n "absolute inline-flex size-full animate-ping rounded-full opacity-75",\n tone,\n )}\n />\n ) : null}\n <span className={cn("relative inline-flex size-1 rounded-full transition-colors", tone)} />\n </span>\n );\n}\n';
readonly "app/_components/agent-message.tsx": '"use client";\n\nimport type { EveDynamicToolPart, EveMessage, EveMessagePart } from "eve/react";\nimport { Message, MessageContent, MessageResponse } from "@/components/ai-elements/message";\nimport { Reasoning, ReasoningContent, ReasoningTrigger } from "@/components/ai-elements/reasoning";\nimport {\n Tool,\n ToolContent,\n ToolHeader,\n ToolInput,\n ToolOutput,\n} from "@/components/ai-elements/tool";\nimport { Button } from "@/components/ui/button";\n\nexport type AgentInputResponse = {\n readonly optionId?: string;\n readonly requestId: string;\n readonly text?: string;\n};\n\nexport function AgentMessage({\n canRespond,\n isStreaming,\n message,\n onInputResponses,\n}: {\n readonly canRespond: boolean;\n readonly isStreaming: boolean;\n readonly message: EveMessage;\n readonly onInputResponses: (responses: readonly AgentInputResponse[]) => void | Promise<void>;\n}) {\n const lastTextIndex = message.parts.reduce(\n (last, part, index) => (part.type === "text" ? index : last),\n -1,\n );\n\n return (\n <Message\n data-optimistic={message.metadata?.optimistic ? "true" : undefined}\n from={message.role}\n >\n <MessageContent>\n {message.parts.map((part, index) => (\n <AgentMessagePart\n canRespond={canRespond}\n key={partKey(part, index)}\n onInputResponses={onInputResponses}\n part={part}\n showCaret={isStreaming && message.role === "assistant" && index === lastTextIndex}\n />\n ))}\n </MessageContent>\n </Message>\n );\n}\n\nfunction AgentMessagePart({\n canRespond,\n onInputResponses,\n part,\n showCaret,\n}: {\n readonly canRespond: boolean;\n readonly onInputResponses: (responses: readonly AgentInputResponse[]) => void | Promise<void>;\n readonly part: EveMessagePart;\n readonly showCaret: boolean;\n}) {\n switch (part.type) {\n case "step-start":\n return null;\n case "text":\n return (\n <MessageResponse caret="block" isAnimating={showCaret}>\n {part.text}\n </MessageResponse>\n );\n case "reasoning":\n return (\n <Reasoning defaultOpen isStreaming={part.state === "streaming"}>\n <ReasoningTrigger />\n <ReasoningContent>{part.text}</ReasoningContent>\n </Reasoning>\n );\n case "dynamic-tool":\n return (\n <Tool\n defaultOpen={part.state === "approval-requested" || part.state === "approval-responded"}\n >\n <ToolHeader\n state={part.state}\n title={part.toolName}\n toolName={part.toolName}\n type="dynamic-tool"\n />\n <ToolContent>\n <ToolInput input={part.input} />\n <InputRequestActions\n canRespond={canRespond}\n part={part}\n onInputResponses={onInputResponses}\n />\n <ToolOutput errorText={part.errorText} output={part.output} />\n </ToolContent>\n </Tool>\n );\n }\n}\n\nfunction InputRequestActions({\n canRespond,\n onInputResponses,\n part,\n}: {\n readonly canRespond: boolean;\n readonly onInputResponses: (responses: readonly AgentInputResponse[]) => void | Promise<void>;\n readonly part: EveDynamicToolPart;\n}) {\n const inputRequest = part.toolMetadata?.eve?.inputRequest;\n if (!inputRequest) {\n return null;\n }\n\n const inputResponse = part.toolMetadata?.eve?.inputResponse;\n const selectedOption = inputRequest.options?.find(\n (option) => option.id === inputResponse?.optionId,\n );\n\n return (\n <div className="space-y-3 rounded-md border border-yellow-500/30 bg-yellow-500/5 p-3">\n <p className="text-muted-foreground text-sm">{inputRequest.prompt}</p>\n {inputResponse ? (\n <p className="font-medium text-sm">\n Responded: {selectedOption?.label ?? inputResponse.text ?? inputResponse.optionId}\n </p>\n ) : (\n <div className="flex flex-wrap gap-2">\n {inputRequest.options?.map((option) => (\n <Button\n disabled={!canRespond}\n key={option.id}\n onClick={() => {\n void onInputResponses([\n {\n optionId: option.id,\n requestId: inputRequest.requestId,\n },\n ]);\n }}\n size="sm"\n type="button"\n variant={option.style === "danger" ? "destructive" : "default"}\n >\n {option.label}\n </Button>\n ))}\n </div>\n )}\n </div>\n );\n}\n\nfunction partKey(part: EveMessagePart, index: number): string {\n switch (part.type) {\n case "dynamic-tool":\n return part.toolCallId;\n default:\n return `${part.type}:${index}`;\n }\n}\n';
readonly "app/globals.css": '@import "tailwindcss";\n@source "../node_modules/streamdown/dist/*.js";\n\n@theme inline {\n --color-background: var(--background);\n --color-foreground: var(--foreground);\n --color-card: var(--card);\n --color-card-foreground: var(--card-foreground);\n --color-popover: var(--popover);\n --color-popover-foreground: var(--popover-foreground);\n --color-primary: var(--primary);\n --color-primary-foreground: var(--primary-foreground);\n --color-secondary: var(--secondary);\n --color-secondary-foreground: var(--secondary-foreground);\n --color-muted: var(--muted);\n --color-muted-foreground: var(--muted-foreground);\n --color-accent: var(--accent);\n --color-accent-foreground: var(--accent-foreground);\n --color-destructive: var(--destructive);\n --color-border: var(--border);\n --color-input: var(--input);\n --color-ring: var(--ring);\n --radius-sm: calc(var(--radius) - 4px);\n --radius-md: calc(var(--radius) - 2px);\n --radius-lg: var(--radius);\n --radius-xl: calc(var(--radius) + 4px);\n --font-sans: "Geist", "Geist Fallback", ui-sans-serif, system-ui, sans-serif;\n --font-mono: "Geist Mono", "Geist Mono Fallback", ui-monospace, monospace;\n}\n\n:root {\n color-scheme: light;\n /* Soft neutral page with white elevated surfaces so cards/composer pop. */\n --background: oklch(0.971 0 0);\n --foreground: oklch(0.16 0 0);\n --card: oklch(1 0 0);\n --card-foreground: oklch(0.16 0 0);\n --popover: oklch(1 0 0);\n --popover-foreground: oklch(0.16 0 0);\n --primary: oklch(0.19 0 0);\n --primary-foreground: oklch(0.985 0 0);\n --secondary: oklch(0.94 0 0);\n --secondary-foreground: oklch(0.19 0 0);\n --muted: oklch(0.94 0 0);\n --muted-foreground: oklch(0.6 0 0);\n --accent: oklch(0.94 0 0);\n --accent-foreground: oklch(0.19 0 0);\n --destructive: oklch(0.577 0.245 27.325);\n --border: oklch(0.916 0 0);\n --input: oklch(0.916 0 0);\n --ring: oklch(0.708 0 0);\n --radius: 0.625rem;\n}\n\n@media (prefers-color-scheme: dark) {\n :root {\n color-scheme: dark;\n --background: oklch(0.145 0 0);\n --foreground: oklch(0.985 0 0);\n --card: oklch(0.205 0 0);\n --card-foreground: oklch(0.985 0 0);\n --popover: oklch(0.205 0 0);\n --popover-foreground: oklch(0.985 0 0);\n --primary: oklch(0.922 0 0);\n --primary-foreground: oklch(0.205 0 0);\n --secondary: oklch(0.269 0 0);\n --secondary-foreground: oklch(0.985 0 0);\n --muted: oklch(0.269 0 0);\n --muted-foreground: oklch(0.708 0 0);\n --accent: oklch(0.269 0 0);\n --accent-foreground: oklch(0.985 0 0);\n --destructive: oklch(0.704 0.191 22.216);\n --border: oklch(1 0 0 / 10%);\n --input: oklch(1 0 0 / 15%);\n --ring: oklch(0.556 0 0);\n }\n}\n\n* {\n border-color: var(--border);\n}\n\nhtml {\n height: 100%;\n}\n\nbody {\n min-height: 100%;\n margin: 0;\n background: var(--background);\n font-family: var(--font-sans);\n}\n\nbutton,\ninput,\ntextarea {\n font: inherit;\n}\n';
readonly "app/layout.tsx": 'import type { Metadata } from "next";\nimport { Geist, Geist_Mono } from "next/font/google";\nimport type { ReactNode } from "react";\nimport { TooltipProvider } from "@/components/ui/tooltip";\nimport { cn } from "@/lib/utils";\nimport "./globals.css";\n\nconst sans = Geist({\n variable: "--font-sans",\n subsets: ["latin"],\n weight: "variable",\n display: "swap",\n});\n\nconst mono = Geist_Mono({\n variable: "--font-mono",\n subsets: ["latin"],\n weight: "variable",\n display: "swap",\n});\n\nexport const metadata: Metadata = {\n title: "__EVE_INIT_APP_NAME__",\n description: "A Next.js starter for eve agents with AI Elements.",\n};\n\nexport default function RootLayout({ children }: { readonly children: ReactNode }) {\n return (\n <html className={cn(sans.variable, mono.variable)} lang="en">\n <body>\n <TooltipProvider>{children}</TooltipProvider>\n </body>\n </html>\n );\n}\n';
readonly "app/page.tsx": 'import { AgentChat } from "@/app/_components/agent-chat";\n\nexport default function Page() {\n return <AgentChat />;\n}\n';
readonly "components/ai-elements/chain-of-thought.tsx": '"use client";\n\nimport { useControllableState } from "@radix-ui/react-use-controllable-state";\nimport { Badge } from "@/components/ui/badge";\nimport { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@/components/ui/collapsible";\nimport { cn } from "@/lib/utils";\nimport type { LucideIcon } from "lucide-react";\nimport { BrainIcon, ChevronDownIcon, DotIcon } from "lucide-react";\nimport type { ComponentProps, ReactNode } from "react";\nimport { createContext, memo, useContext, useMemo } from "react";\n\ninterface ChainOfThoughtContextValue {\n isOpen: boolean;\n setIsOpen: (open: boolean) => void;\n}\n\nconst ChainOfThoughtContext = createContext<ChainOfThoughtContextValue | null>(null);\n\nconst useChainOfThought = () => {\n const context = useContext(ChainOfThoughtContext);\n if (!context) {\n throw new Error("ChainOfThought components must be used within ChainOfThought");\n }\n return context;\n};\n\nexport type ChainOfThoughtProps = ComponentProps<"div"> & {\n open?: boolean;\n defaultOpen?: boolean;\n onOpenChange?: (open: boolean) => void;\n};\n\nexport const ChainOfThought = memo(\n ({\n className,\n open,\n defaultOpen = false,\n onOpenChange,\n children,\n ...props\n }: ChainOfThoughtProps) => {\n const [isOpen, setIsOpen] = useControllableState({\n defaultProp: defaultOpen,\n onChange: onOpenChange,\n prop: open,\n });\n\n const chainOfThoughtContext = useMemo(() => ({ isOpen, setIsOpen }), [isOpen, setIsOpen]);\n\n return (\n <ChainOfThoughtContext.Provider value={chainOfThoughtContext}>\n <div className={cn("not-prose w-full space-y-4", className)} {...props}>\n {children}\n </div>\n </ChainOfThoughtContext.Provider>\n );\n },\n);\n\nexport type ChainOfThoughtHeaderProps = ComponentProps<typeof CollapsibleTrigger>;\n\nexport const ChainOfThoughtHeader = memo(\n ({ className, children, ...props }: ChainOfThoughtHeaderProps) => {\n const { isOpen, setIsOpen } = useChainOfThought();\n\n return (\n <Collapsible onOpenChange={setIsOpen} open={isOpen}>\n <CollapsibleTrigger\n className={cn(\n "flex w-full items-center gap-2 text-muted-foreground text-sm transition-colors hover:text-foreground",\n className,\n )}\n {...props}\n >\n <BrainIcon className="size-4" />\n <span className="flex-1 text-left">{children ?? "Chain of Thought"}</span>\n <ChevronDownIcon\n className={cn("size-4 transition-transform", isOpen ? "rotate-180" : "rotate-0")}\n />\n </CollapsibleTrigger>\n </Collapsible>\n );\n },\n);\n\nexport type ChainOfThoughtStepProps = ComponentProps<"div"> & {\n icon?: LucideIcon;\n label: ReactNode;\n description?: ReactNode;\n status?: "complete" | "active" | "pending";\n};\n\nconst stepStatusStyles = {\n active: "text-foreground",\n complete: "text-muted-foreground",\n pending: "text-muted-foreground/50",\n};\n\nexport const ChainOfThoughtStep = memo(\n ({\n className,\n icon: Icon = DotIcon,\n label,\n description,\n status = "complete",\n children,\n ...props\n }: ChainOfThoughtStepProps) => (\n <div\n className={cn(\n "flex gap-2 text-sm",\n stepStatusStyles[status],\n "fade-in-0 slide-in-from-top-2 animate-in",\n className,\n )}\n {...props}\n >\n <div className="relative mt-0.5">\n <Icon className="size-4" />\n <div className="absolute top-7 bottom-0 left-1/2 -mx-px w-px bg-border" />\n </div>\n <div className="flex-1 space-y-2 overflow-hidden">\n <div>{label}</div>\n {description && <div className="text-muted-foreground text-xs">{description}</div>}\n {children}\n </div>\n </div>\n ),\n);\n\nexport type ChainOfThoughtSearchResultsProps = ComponentProps<"div">;\n\nexport const ChainOfThoughtSearchResults = memo(\n ({ className, ...props }: ChainOfThoughtSearchResultsProps) => (\n <div className={cn("flex flex-wrap items-center gap-2", className)} {...props} />\n ),\n);\n\nexport type ChainOfThoughtSearchResultProps = ComponentProps<typeof Badge>;\n\nexport const ChainOfThoughtSearchResult = memo(\n ({ className, children, ...props }: ChainOfThoughtSearchResultProps) => (\n <Badge\n className={cn("gap-1 px-2 py-0.5 font-normal text-xs", className)}\n variant="secondary"\n {...props}\n >\n {children}\n </Badge>\n ),\n);\n\nexport type ChainOfThoughtContentProps = ComponentProps<typeof CollapsibleContent>;\n\nexport const ChainOfThoughtContent = memo(\n ({ className, children, ...props }: ChainOfThoughtContentProps) => {\n const { isOpen } = useChainOfThought();\n\n return (\n <Collapsible open={isOpen}>\n <CollapsibleContent\n className={cn(\n "mt-2 space-y-3",\n "data-[state=closed]:fade-out-0 data-[state=closed]:slide-out-to-top-2 data-[state=open]:slide-in-from-top-2 text-popover-foreground outline-none data-[state=closed]:animate-out data-[state=open]:animate-in",\n className,\n )}\n {...props}\n >\n {children}\n </CollapsibleContent>\n </Collapsible>\n );\n },\n);\n\nexport type ChainOfThoughtImageProps = ComponentProps<"div"> & {\n caption?: string;\n};\n\nexport const ChainOfThoughtImage = memo(\n ({ className, children, caption, ...props }: ChainOfThoughtImageProps) => (\n <div className={cn("mt-2 space-y-2", className)} {...props}>\n <div className="relative flex max-h-[22rem] items-center justify-center overflow-hidden rounded-lg bg-muted p-3">\n {children}\n </div>\n {caption && <p className="text-muted-foreground text-xs">{caption}</p>}\n </div>\n ),\n);\n\nChainOfThought.displayName = "ChainOfThought";\nChainOfThoughtHeader.displayName = "ChainOfThoughtHeader";\nChainOfThoughtStep.displayName = "ChainOfThoughtStep";\nChainOfThoughtSearchResults.displayName = "ChainOfThoughtSearchResults";\nChainOfThoughtSearchResult.displayName = "ChainOfThoughtSearchResult";\nChainOfThoughtContent.displayName = "ChainOfThoughtContent";\nChainOfThoughtImage.displayName = "ChainOfThoughtImage";\n';
readonly "components/ai-elements/code-block.tsx": '"use client";\n\nimport { Button } from "@/components/ui/button";\nimport {\n Select,\n SelectContent,\n SelectItem,\n SelectTrigger,\n SelectValue,\n} from "@/components/ui/select";\nimport { cn } from "@/lib/utils";\nimport { CheckIcon, CopyIcon } from "lucide-react";\nimport type { ComponentProps, CSSProperties, HTMLAttributes } from "react";\nimport {\n createContext,\n memo,\n useCallback,\n useContext,\n useEffect,\n useMemo,\n useRef,\n useState,\n} from "react";\nimport type { BundledLanguage, BundledTheme, HighlighterGeneric, ThemedToken } from "shiki";\nimport { createHighlighter } from "shiki";\n\n// Shiki uses bitflags for font styles: 1=italic, 2=bold, 4=underline\n// oxlint-disable-next-line eslint(no-bitwise)\nconst isItalic = (fontStyle: number | undefined) => fontStyle && fontStyle & 1;\n// oxlint-disable-next-line eslint(no-bitwise)\nconst isBold = (fontStyle: number | undefined) => fontStyle && fontStyle & 2;\nconst isUnderline = (fontStyle: number | undefined) =>\n // oxlint-disable-next-line eslint(no-bitwise)\n fontStyle && fontStyle & 4;\n\n// Transform tokens to include pre-computed keys to avoid noArrayIndexKey lint\ninterface KeyedToken {\n token: ThemedToken;\n key: string;\n}\ninterface KeyedLine {\n tokens: KeyedToken[];\n key: string;\n}\n\nconst addKeysToTokens = (lines: ThemedToken[][]): KeyedLine[] =>\n lines.map((line, lineIdx) => ({\n key: `line-${lineIdx}`,\n tokens: line.map((token, tokenIdx) => ({\n key: `line-${lineIdx}-${tokenIdx}`,\n token,\n })),\n }));\n\n// Token rendering component\nconst TokenSpan = ({ token }: { token: ThemedToken }) => (\n <span\n className="dark:!bg-[var(--shiki-dark-bg)] dark:!text-[var(--shiki-dark)]"\n style={\n {\n backgroundColor: token.bgColor,\n color: token.color,\n fontStyle: isItalic(token.fontStyle) ? "italic" : undefined,\n fontWeight: isBold(token.fontStyle) ? "bold" : undefined,\n textDecoration: isUnderline(token.fontStyle) ? "underline" : undefined,\n ...token.htmlStyle,\n } as CSSProperties\n }\n >\n {token.content}\n </span>\n);\n\n// Line number styles using CSS counters\nconst LINE_NUMBER_CLASSES = cn(\n "block",\n "before:content-[counter(line)]",\n "before:inline-block",\n "before:[counter-increment:line]",\n "before:w-8",\n "before:mr-4",\n "before:text-right",\n "before:text-muted-foreground/50",\n "before:font-mono",\n "before:select-none",\n);\n\n// Line rendering component\nconst LineSpan = ({\n keyedLine,\n showLineNumbers,\n}: {\n keyedLine: KeyedLine;\n showLineNumbers: boolean;\n}) => (\n <span className={showLineNumbers ? LINE_NUMBER_CLASSES : "block"}>\n {keyedLine.tokens.length === 0\n ? "\\n"\n : keyedLine.tokens.map(({ token, key }) => <TokenSpan key={key} token={token} />)}\n </span>\n);\n\n// Types\ntype CodeBlockProps = HTMLAttributes<HTMLDivElement> & {\n code: string;\n language: BundledLanguage;\n showLineNumbers?: boolean;\n};\n\ninterface TokenizedCode {\n tokens: ThemedToken[][];\n fg: string;\n bg: string;\n}\n\ninterface CodeBlockContextType {\n code: string;\n}\n\n// Context\nconst CodeBlockContext = createContext<CodeBlockContextType>({\n code: "",\n});\n\n// Highlighter cache (singleton per language)\nconst highlighterCache = new Map<\n string,\n Promise<HighlighterGeneric<BundledLanguage, BundledTheme>>\n>();\n\n// Token cache\nconst tokensCache = new Map<string, TokenizedCode>();\n\n// Subscribers for async token updates\nconst subscribers = new Map<string, Set<(result: TokenizedCode) => void>>();\n\nconst getTokensCacheKey = (code: string, language: BundledLanguage) => {\n const start = code.slice(0, 100);\n const end = code.length > 100 ? code.slice(-100) : "";\n return `${language}:${code.length}:${start}:${end}`;\n};\n\nconst getHighlighter = (\n language: BundledLanguage,\n): Promise<HighlighterGeneric<BundledLanguage, BundledTheme>> => {\n const cached = highlighterCache.get(language);\n if (cached) {\n return cached;\n }\n\n const highlighterPromise = createHighlighter({\n langs: [language],\n themes: ["github-light", "github-dark"],\n });\n\n highlighterCache.set(language, highlighterPromise);\n return highlighterPromise;\n};\n\n// Create raw tokens for immediate display while highlighting loads\nconst createRawTokens = (code: string): TokenizedCode => ({\n bg: "transparent",\n fg: "inherit",\n tokens: code.split("\\n").map((line) =>\n line === ""\n ? []\n : [\n {\n color: "inherit",\n content: line,\n } as ThemedToken,\n ],\n ),\n});\n\n// Synchronous highlight with callback for async results\nexport const highlightCode = (\n code: string,\n language: BundledLanguage,\n // oxlint-disable-next-line eslint-plugin-promise(prefer-await-to-callbacks)\n callback?: (result: TokenizedCode) => void,\n): TokenizedCode | null => {\n const tokensCacheKey = getTokensCacheKey(code, language);\n\n // Return cached result if available\n const cached = tokensCache.get(tokensCacheKey);\n if (cached) {\n return cached;\n }\n\n // Subscribe callback if provided\n if (callback) {\n if (!subscribers.has(tokensCacheKey)) {\n subscribers.set(tokensCacheKey, new Set());\n }\n subscribers.get(tokensCacheKey)?.add(callback);\n }\n\n // Start highlighting in background - fire-and-forget async pattern\n getHighlighter(language)\n // oxlint-disable-next-line eslint-plugin-promise(prefer-await-to-then)\n .then((highlighter) => {\n const availableLangs = highlighter.getLoadedLanguages();\n const langToUse = availableLangs.includes(language) ? language : "text";\n\n const result = highlighter.codeToTokens(code, {\n lang: langToUse,\n themes: {\n dark: "github-dark",\n light: "github-light",\n },\n });\n\n const tokenized: TokenizedCode = {\n bg: result.bg ?? "transparent",\n fg: result.fg ?? "inherit",\n tokens: result.tokens,\n };\n\n // Cache the result\n tokensCache.set(tokensCacheKey, tokenized);\n\n // Notify all subscribers\n const subs = subscribers.get(tokensCacheKey);\n if (subs) {\n for (const sub of subs) {\n sub(tokenized);\n }\n subscribers.delete(tokensCacheKey);\n }\n })\n // oxlint-disable-next-line eslint-plugin-promise(prefer-await-to-then), eslint-plugin-promise(prefer-await-to-callbacks)\n .catch((error) => {\n console.error("Failed to highlight code:", error);\n subscribers.delete(tokensCacheKey);\n });\n\n return null;\n};\n\nconst CodeBlockBody = memo(\n ({\n tokenized,\n showLineNumbers,\n className,\n }: {\n tokenized: TokenizedCode;\n showLineNumbers: boolean;\n className?: string;\n }) => {\n const preStyle = useMemo(\n () => ({\n backgroundColor: tokenized.bg,\n color: tokenized.fg,\n }),\n [tokenized.bg, tokenized.fg],\n );\n\n const keyedLines = useMemo(() => addKeysToTokens(tokenized.tokens), [tokenized.tokens]);\n\n return (\n <pre\n className={cn(\n "dark:!bg-[var(--shiki-dark-bg)] dark:!text-[var(--shiki-dark)] m-0 p-4 text-sm",\n className,\n )}\n style={preStyle}\n >\n <code\n className={cn(\n "font-mono text-sm",\n showLineNumbers && "[counter-increment:line_0] [counter-reset:line]",\n )}\n >\n {keyedLines.map((keyedLine) => (\n <LineSpan key={keyedLine.key} keyedLine={keyedLine} showLineNumbers={showLineNumbers} />\n ))}\n </code>\n </pre>\n );\n },\n (prevProps, nextProps) =>\n prevProps.tokenized === nextProps.tokenized &&\n prevProps.showLineNumbers === nextProps.showLineNumbers &&\n prevProps.className === nextProps.className,\n);\n\nCodeBlockBody.displayName = "CodeBlockBody";\n\nexport const CodeBlockContainer = ({\n className,\n language,\n style,\n ...props\n}: HTMLAttributes<HTMLDivElement> & { language: string }) => (\n <div\n className={cn(\n "group relative w-full overflow-hidden rounded-md border bg-background text-foreground",\n className,\n )}\n data-language={language}\n style={{\n containIntrinsicSize: "auto 200px",\n contentVisibility: "auto",\n ...style,\n }}\n {...props}\n />\n);\n\nexport const CodeBlockHeader = ({\n children,\n className,\n ...props\n}: HTMLAttributes<HTMLDivElement>) => (\n <div\n className={cn(\n "flex items-center justify-between border-b bg-muted/80 px-3 py-2 text-muted-foreground text-xs",\n className,\n )}\n {...props}\n >\n {children}\n </div>\n);\n\nexport const CodeBlockTitle = ({\n children,\n className,\n ...props\n}: HTMLAttributes<HTMLDivElement>) => (\n <div className={cn("flex items-center gap-2", className)} {...props}>\n {children}\n </div>\n);\n\nexport const CodeBlockFilename = ({\n children,\n className,\n ...props\n}: HTMLAttributes<HTMLSpanElement>) => (\n <span className={cn("font-mono", className)} {...props}>\n {children}\n </span>\n);\n\nexport const CodeBlockActions = ({\n children,\n className,\n ...props\n}: HTMLAttributes<HTMLDivElement>) => (\n <div className={cn("-my-1 -mr-1 flex items-center gap-2", className)} {...props}>\n {children}\n </div>\n);\n\nexport const CodeBlockContent = ({\n code,\n language,\n showLineNumbers = false,\n}: {\n code: string;\n language: BundledLanguage;\n showLineNumbers?: boolean;\n}) => {\n // Memoized raw tokens for immediate display\n const rawTokens = useMemo(() => createRawTokens(code), [code]);\n\n // Synchronous cache lookup — avoids setState in effect for cached results\n const syncTokens = useMemo(\n () => highlightCode(code, language) ?? rawTokens,\n [code, language, rawTokens],\n );\n\n // Async highlighting result (populated after shiki loads)\n const [asyncTokens, setAsyncTokens] = useState<TokenizedCode | null>(null);\n const asyncKeyRef = useRef({ code, language });\n\n // Invalidate stale async tokens synchronously during render\n if (asyncKeyRef.current.code !== code || asyncKeyRef.current.language !== language) {\n asyncKeyRef.current = { code, language };\n setAsyncTokens(null);\n }\n\n useEffect(() => {\n let cancelled = false;\n\n highlightCode(code, language, (result) => {\n if (!cancelled) {\n setAsyncTokens(result);\n }\n });\n\n return () => {\n cancelled = true;\n };\n }, [code, language]);\n\n const tokenized = asyncTokens ?? syncTokens;\n\n return (\n <div className="relative overflow-auto">\n <CodeBlockBody showLineNumbers={showLineNumbers} tokenized={tokenized} />\n </div>\n );\n};\n\nexport const CodeBlock = ({\n code,\n language,\n showLineNumbers = false,\n className,\n children,\n ...props\n}: CodeBlockProps) => {\n const contextValue = useMemo(() => ({ code }), [code]);\n\n return (\n <CodeBlockContext.Provider value={contextValue}>\n <CodeBlockContainer className={className} language={language} {...props}>\n {children}\n <CodeBlockContent code={code} language={language} showLineNumbers={showLineNumbers} />\n </CodeBlockContainer>\n </CodeBlockContext.Provider>\n );\n};\n\nexport type CodeBlockCopyButtonProps = ComponentProps<typeof Button> & {\n onCopy?: () => void;\n onError?: (error: Error) => void;\n timeout?: number;\n};\n\nexport const CodeBlockCopyButton = ({\n onCopy,\n onError,\n timeout = 2000,\n children,\n className,\n ...props\n}: CodeBlockCopyButtonProps) => {\n const [isCopied, setIsCopied] = useState(false);\n const timeoutRef = useRef<number>(0);\n const { code } = useContext(CodeBlockContext);\n\n const copyToClipboard = useCallback(async () => {\n if (typeof window === "undefined" || !navigator?.clipboard?.writeText) {\n onError?.(new Error("Clipboard API not available"));\n return;\n }\n\n try {\n if (!isCopied) {\n await navigator.clipboard.writeText(code);\n setIsCopied(true);\n onCopy?.();\n timeoutRef.current = window.setTimeout(() => setIsCopied(false), timeout);\n }\n } catch (error) {\n onError?.(error as Error);\n }\n }, [code, onCopy, onError, timeout, isCopied]);\n\n useEffect(\n () => () => {\n window.clearTimeout(timeoutRef.current);\n },\n [],\n );\n\n const Icon = isCopied ? CheckIcon : CopyIcon;\n\n return (\n <Button\n className={cn("shrink-0", className)}\n onClick={copyToClipboard}\n size="icon"\n variant="ghost"\n {...props}\n >\n {children ?? <Icon size={14} />}\n </Button>\n );\n};\n\nexport type CodeBlockLanguageSelectorProps = ComponentProps<typeof Select>;\n\nexport const CodeBlockLanguageSelector = (props: CodeBlockLanguageSelectorProps) => (\n <Select {...props} />\n);\n\nexport type CodeBlockLanguageSelectorTriggerProps = ComponentProps<typeof SelectTrigger>;\n\nexport const CodeBlockLanguageSelectorTrigger = ({\n className,\n ...props\n}: CodeBlockLanguageSelectorTriggerProps) => (\n <SelectTrigger\n className={cn("h-7 border-none bg-transparent px-2 text-xs shadow-none", className)}\n size="sm"\n {...props}\n />\n);\n\nexport type CodeBlockLanguageSelectorValueProps = ComponentProps<typeof SelectValue>;\n\nexport const CodeBlockLanguageSelectorValue = (props: CodeBlockLanguageSelectorValueProps) => (\n <SelectValue {...props} />\n);\n\nexport type CodeBlockLanguageSelectorContentProps = ComponentProps<typeof SelectContent>;\n\nexport const CodeBlockLanguageSelectorContent = ({\n align = "end",\n ...props\n}: CodeBlockLanguageSelectorContentProps) => <SelectContent align={align} {...props} />;\n\nexport type CodeBlockLanguageSelectorItemProps = ComponentProps<typeof SelectItem>;\n\nexport const CodeBlockLanguageSelectorItem = (props: CodeBlockLanguageSelectorItemProps) => (\n <SelectItem {...props} />\n);\n';
readonly "components/ai-elements/conversation.tsx": '"use client";\n\nimport { Button } from "@/components/ui/button";\nimport { cn } from "@/lib/utils";\nimport type { UIMessage } from "ai";\nimport { ArrowDownIcon, DownloadIcon } from "lucide-react";\nimport type { ComponentProps } from "react";\nimport { useCallback } from "react";\nimport { StickToBottom, useStickToBottomContext } from "use-stick-to-bottom";\n\nexport type ConversationProps = ComponentProps<typeof StickToBottom>;\n\nexport const Conversation = ({ className, ...props }: ConversationProps) => (\n <StickToBottom\n className={cn("relative flex-1 overflow-y-hidden", className)}\n initial="smooth"\n resize="smooth"\n role="log"\n {...props}\n />\n);\n\nexport type ConversationContentProps = ComponentProps<typeof StickToBottom.Content>;\n\nexport const ConversationContent = ({ className, ...props }: ConversationContentProps) => (\n <StickToBottom.Content className={cn("flex flex-col gap-8 p-4", className)} {...props} />\n);\n\nexport type ConversationEmptyStateProps = ComponentProps<"div"> & {\n title?: string;\n description?: string;\n icon?: React.ReactNode;\n};\n\nexport const ConversationEmptyState = ({\n className,\n title = "No messages yet",\n description = "Start a conversation to see messages here",\n icon,\n children,\n ...props\n}: ConversationEmptyStateProps) => (\n <div\n className={cn(\n "flex size-full flex-col items-center justify-center gap-3 p-8 text-center",\n className,\n )}\n {...props}\n >\n {children ?? (\n <>\n {icon && <div className="text-muted-foreground">{icon}</div>}\n <div className="space-y-1">\n <h3 className="font-medium text-sm">{title}</h3>\n {description && <p className="text-muted-foreground text-sm">{description}</p>}\n </div>\n </>\n )}\n </div>\n);\n\nexport type ConversationScrollButtonProps = ComponentProps<typeof Button>;\n\nexport const ConversationScrollButton = ({\n className,\n ...props\n}: ConversationScrollButtonProps) => {\n const { isAtBottom, scrollToBottom } = useStickToBottomContext();\n\n const handleScrollToBottom = useCallback(() => {\n scrollToBottom();\n }, [scrollToBottom]);\n\n return (\n !isAtBottom && (\n <Button\n className={cn(\n "absolute bottom-4 left-[50%] translate-x-[-50%] rounded-full dark:bg-background dark:hover:bg-muted",\n className,\n )}\n onClick={handleScrollToBottom}\n size="icon"\n type="button"\n variant="outline"\n {...props}\n >\n <ArrowDownIcon className="size-4" />\n </Button>\n )\n );\n};\n\nconst getMessageText = (message: UIMessage): string =>\n message.parts\n .filter((part) => part.type === "text")\n .map((part) => part.text)\n .join("");\n\nexport type ConversationDownloadProps = Omit<ComponentProps<typeof Button>, "onClick"> & {\n messages: UIMessage[];\n filename?: string;\n formatMessage?: (message: UIMessage, index: number) => string;\n};\n\nconst defaultFormatMessage = (message: UIMessage): string => {\n const roleLabel = message.role.charAt(0).toUpperCase() + message.role.slice(1);\n return `**${roleLabel}:** ${getMessageText(message)}`;\n};\n\nexport const messagesToMarkdown = (\n messages: UIMessage[],\n formatMessage: (message: UIMessage, index: number) => string = defaultFormatMessage,\n): string => messages.map((msg, i) => formatMessage(msg, i)).join("\\n\\n");\n\nexport const ConversationDownload = ({\n messages,\n filename = "conversation.md",\n formatMessage = defaultFormatMessage,\n className,\n children,\n ...props\n}: ConversationDownloadProps) => {\n const handleDownload = useCallback(() => {\n const markdown = messagesToMarkdown(messages, formatMessage);\n const blob = new Blob([markdown], { type: "text/markdown" });\n const url = URL.createObjectURL(blob);\n const link = document.createElement("a");\n link.href = url;\n link.download = filename;\n document.body.append(link);\n link.click();\n link.remove();\n URL.revokeObjectURL(url);\n }, [messages, filename, formatMessage]);\n\n return (\n <Button\n className={cn(\n "absolute top-4 right-4 rounded-full dark:bg-background dark:hover:bg-muted",\n className,\n )}\n onClick={handleDownload}\n size="icon"\n type="button"\n variant="outline"\n {...props}\n >\n {children ?? <DownloadIcon className="size-4" />}\n </Button>\n );\n};\n';
readonly "components/ai-elements/message.tsx": '"use client";\n\nimport { Button } from "@/components/ui/button";\nimport { ButtonGroup, ButtonGroupText } from "@/components/ui/button-group";\nimport { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip";\nimport { cn } from "@/lib/utils";\nimport { cjk } from "@streamdown/cjk";\nimport { code } from "@streamdown/code";\nimport { math } from "@streamdown/math";\nimport { mermaid } from "@streamdown/mermaid";\nimport type { UIMessage } from "ai";\nimport { ChevronLeftIcon, ChevronRightIcon } from "lucide-react";\nimport type { ComponentProps, HTMLAttributes, ReactElement } from "react";\nimport { createContext, memo, useCallback, useContext, useEffect, useMemo, useState } from "react";\nimport { Streamdown } from "streamdown";\n\nexport type MessageProps = HTMLAttributes<HTMLDivElement> & {\n from: UIMessage["role"];\n};\n\nexport const Message = ({ className, from, ...props }: MessageProps) => (\n <div\n className={cn(\n "group flex w-full max-w-[95%] flex-col gap-2",\n from === "user" ? "is-user ml-auto justify-end" : "is-assistant",\n className,\n )}\n {...props}\n />\n);\n\nexport type MessageContentProps = HTMLAttributes<HTMLDivElement>;\n\nexport const MessageContent = ({ children, className, ...props }: MessageContentProps) => (\n <div\n className={cn(\n "is-user:dark flex w-fit min-w-0 max-w-full flex-col gap-2 overflow-hidden text-sm",\n "group-[.is-user]:ml-auto group-[.is-user]:rounded-2xl group-[.is-user]:bg-primary group-[.is-user]:px-4 group-[.is-user]:py-2.5 group-[.is-user]:text-primary-foreground",\n "group-[.is-assistant]:w-full group-[.is-assistant]:text-foreground",\n "group-data-[optimistic=true]:opacity-70",\n className,\n )}\n {...props}\n >\n {children}\n </div>\n);\n\nexport type MessageActionsProps = ComponentProps<"div">;\n\nexport const MessageActions = ({ className, children, ...props }: MessageActionsProps) => (\n <div className={cn("flex items-center gap-1", className)} {...props}>\n {children}\n </div>\n);\n\nexport type MessageActionProps = ComponentProps<typeof Button> & {\n tooltip?: string;\n label?: string;\n};\n\nexport const MessageAction = ({\n tooltip,\n children,\n label,\n variant = "ghost",\n size = "icon-sm",\n ...props\n}: MessageActionProps) => {\n const button = (\n <Button size={size} type="button" variant={variant} {...props}>\n {children}\n <span className="sr-only">{label || tooltip}</span>\n </Button>\n );\n\n if (tooltip) {\n return (\n <TooltipProvider>\n <Tooltip>\n <TooltipTrigger asChild>{button}</TooltipTrigger>\n <TooltipContent>\n <p>{tooltip}</p>\n </TooltipContent>\n </Tooltip>\n </TooltipProvider>\n );\n }\n\n return button;\n};\n\ninterface MessageBranchContextType {\n currentBranch: number;\n totalBranches: number;\n goToPrevious: () => void;\n goToNext: () => void;\n branches: ReactElement[];\n setBranches: (branches: ReactElement[]) => void;\n}\n\nconst MessageBranchContext = createContext<MessageBranchContextType | null>(null);\n\nconst useMessageBranch = () => {\n const context = useContext(MessageBranchContext);\n\n if (!context) {\n throw new Error("MessageBranch components must be used within MessageBranch");\n }\n\n return context;\n};\n\nexport type MessageBranchProps = HTMLAttributes<HTMLDivElement> & {\n defaultBranch?: number;\n onBranchChange?: (branchIndex: number) => void;\n};\n\nexport const MessageBranch = ({\n defaultBranch = 0,\n onBranchChange,\n className,\n ...props\n}: MessageBranchProps) => {\n const [currentBranch, setCurrentBranch] = useState(defaultBranch);\n const [branches, setBranches] = useState<ReactElement[]>([]);\n\n const handleBranchChange = useCallback(\n (newBranch: number) => {\n setCurrentBranch(newBranch);\n onBranchChange?.(newBranch);\n },\n [onBranchChange],\n );\n\n const goToPrevious = useCallback(() => {\n const newBranch = currentBranch > 0 ? currentBranch - 1 : branches.length - 1;\n handleBranchChange(newBranch);\n }, [currentBranch, branches.length, handleBranchChange]);\n\n const goToNext = useCallback(() => {\n const newBranch = currentBranch < branches.length - 1 ? currentBranch + 1 : 0;\n handleBranchChange(newBranch);\n }, [currentBranch, branches.length, handleBranchChange]);\n\n const contextValue = useMemo<MessageBranchContextType>(\n () => ({\n branches,\n currentBranch,\n goToNext,\n goToPrevious,\n setBranches,\n totalBranches: branches.length,\n }),\n [branches, currentBranch, goToNext, goToPrevious],\n );\n\n return (\n <MessageBranchContext.Provider value={contextValue}>\n <div className={cn("grid w-full gap-2 [&>div]:pb-0", className)} {...props} />\n </MessageBranchContext.Provider>\n );\n};\n\nexport type MessageBranchContentProps = HTMLAttributes<HTMLDivElement>;\n\nexport const MessageBranchContent = ({ children, ...props }: MessageBranchContentProps) => {\n const { currentBranch, setBranches, branches } = useMessageBranch();\n const childrenArray = useMemo(\n () => (Array.isArray(children) ? children : [children]),\n [children],\n );\n\n // Use useEffect to update branches when they change\n useEffect(() => {\n if (branches.length !== childrenArray.length) {\n setBranches(childrenArray);\n }\n }, [childrenArray, branches, setBranches]);\n\n return childrenArray.map((branch, index) => (\n <div\n className={cn(\n "grid gap-2 overflow-hidden [&>div]:pb-0",\n index === currentBranch ? "block" : "hidden",\n )}\n key={branch.key}\n {...props}\n >\n {branch}\n </div>\n ));\n};\n\nexport type MessageBranchSelectorProps = ComponentProps<typeof ButtonGroup>;\n\nexport const MessageBranchSelector = ({ className, ...props }: MessageBranchSelectorProps) => {\n const { totalBranches } = useMessageBranch();\n\n // Don\'t render if there\'s only one branch\n if (totalBranches <= 1) {\n return null;\n }\n\n return (\n <ButtonGroup\n className={cn(\n "[&>*:not(:first-child)]:rounded-l-md [&>*:not(:last-child)]:rounded-r-md",\n className,\n )}\n orientation="horizontal"\n {...props}\n />\n );\n};\n\nexport type MessageBranchPreviousProps = ComponentProps<typeof Button>;\n\nexport const MessageBranchPrevious = ({ children, ...props }: MessageBranchPreviousProps) => {\n const { goToPrevious, totalBranches } = useMessageBranch();\n\n return (\n <Button\n aria-label="Previous branch"\n disabled={totalBranches <= 1}\n onClick={goToPrevious}\n size="icon-sm"\n type="button"\n variant="ghost"\n {...props}\n >\n {children ?? <ChevronLeftIcon size={14} />}\n </Button>\n );\n};\n\nexport type MessageBranchNextProps = ComponentProps<typeof Button>;\n\nexport const MessageBranchNext = ({ children, ...props }: MessageBranchNextProps) => {\n const { goToNext, totalBranches } = useMessageBranch();\n\n return (\n <Button\n aria-label="Next branch"\n disabled={totalBranches <= 1}\n onClick={goToNext}\n size="icon-sm"\n type="button"\n variant="ghost"\n {...props}\n >\n {children ?? <ChevronRightIcon size={14} />}\n </Button>\n );\n};\n\nexport type MessageBranchPageProps = HTMLAttributes<HTMLSpanElement>;\n\nexport const MessageBranchPage = ({ className, ...props }: MessageBranchPageProps) => {\n const { currentBranch, totalBranches } = useMessageBranch();\n\n return (\n <ButtonGroupText\n className={cn("border-none bg-transparent text-muted-foreground shadow-none", className)}\n {...props}\n >\n {currentBranch + 1} of {totalBranches}\n </ButtonGroupText>\n );\n};\n\nexport type MessageResponseProps = ComponentProps<typeof Streamdown>;\n\nconst streamdownPlugins = { cjk, code, math, mermaid };\n\nexport const MessageResponse = memo(\n ({ className, ...props }: MessageResponseProps) => (\n <Streamdown\n className={cn("size-full [&>*:first-child]:mt-0 [&>*:last-child]:mb-0", className)}\n plugins={streamdownPlugins}\n {...props}\n />\n ),\n (prevProps, nextProps) =>\n prevProps.children === nextProps.children && nextProps.isAnimating === prevProps.isAnimating,\n);\n\nMessageResponse.displayName = "MessageResponse";\n\nexport type MessageToolbarProps = ComponentProps<"div">;\n\nexport const MessageToolbar = ({ className, children, ...props }: MessageToolbarProps) => (\n <div className={cn("mt-4 flex w-full items-center justify-between gap-4", className)} {...props}>\n {children}\n </div>\n);\n';
readonly "components/ai-elements/prompt-input.tsx": '"use client";\n\nimport {\n Command,\n CommandEmpty,\n CommandGroup,\n CommandInput,\n CommandItem,\n CommandList,\n CommandSeparator,\n} from "@/components/ui/command";\nimport {\n DropdownMenu,\n DropdownMenuContent,\n DropdownMenuItem,\n DropdownMenuTrigger,\n} from "@/components/ui/dropdown-menu";\nimport { HoverCard, HoverCardContent, HoverCardTrigger } from "@/components/ui/hover-card";\nimport {\n InputGroup,\n InputGroupAddon,\n InputGroupButton,\n InputGroupTextarea,\n} from "@/components/ui/input-group";\nimport {\n Select,\n SelectContent,\n SelectItem,\n SelectTrigger,\n SelectValue,\n} from "@/components/ui/select";\nimport { Spinner } from "@/components/ui/spinner";\nimport { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";\nimport { cn } from "@/lib/utils";\nimport type { ChatStatus, FileUIPart, SourceDocumentUIPart } from "ai";\nimport { ArrowUpIcon, ImageIcon, Monitor, PlusIcon, SquareIcon, XIcon } from "lucide-react";\nimport { nanoid } from "nanoid";\nimport type {\n ChangeEvent,\n ChangeEventHandler,\n ClipboardEventHandler,\n ComponentProps,\n FormEvent,\n FormEventHandler,\n HTMLAttributes,\n KeyboardEventHandler,\n Pro