UNPKG

eve

Version:

Filesystem-first framework for durable backend AI agents that run anywhere.

1,711 lines (1,525 loc) 152 kB
const WEB_APP_TEMPLATE_FILES={"agent/channels/eve.ts":`import { eveChannel } from "eve/channels/eve"; import { localDev, placeholderAuth, vercelOidc } from "eve/channels/auth"; export default eveChannel({ auth: [ // Lets the eve TUI and your Vercel deployments reach the deployed agent. vercelOidc(), // Open on localhost for \`eve dev\` and the REPL; ignored in production. localDev(), // This placeholder will not allow browser requests in production. // Replace it with your app's auth provider, like Auth.js or Clerk, // or use none() for a public demo. placeholderAuth(), ], }); `,"app/_components/agent-chat.tsx":`"use client"; import type { UserContent } from "ai"; import { Client, type HandleMessageStreamEvent } from "eve/client"; import { useEveAgent } from "eve/react"; import { AlertCircleIcon } from "lucide-react"; import { useCallback, useRef, useState } from "react"; import { Conversation, ConversationContent, ConversationScrollButton, } from "@/components/ai-elements/conversation"; import { PromptInput, type PromptInputMessage, PromptInputSubmit, PromptInputTextarea, } from "@/components/ai-elements/prompt-input"; import { cn } from "@/lib/utils"; import { AgentMessage } from "./agent-message"; const AGENT_NAME = "__EVE_INIT_APP_NAME__"; type AgentStatus = ReturnType<typeof useEveAgent>["status"]; type CancellationState = "idle" | "requested" | "cancelling"; type Cancellation = { requested: boolean; sentTurnId?: string; turnId?: string; }; export function AgentChat() { const [session] = useState(() => new Client({ host: "", preserveCompletedSessions: true }).session(), ); const cancellationRef = useRef<Cancellation>({ requested: false }); const [cancellationError, setCancellationError] = useState<string>(); const [cancellationState, setCancellationState] = useState<CancellationState>("idle"); const cancelTurn = useCallback( (turnId: string) => { const cancellation = cancellationRef.current; if (!cancellation.requested || cancellation.sentTurnId === turnId) { return; } cancellation.sentTurnId = turnId; setCancellationState("cancelling"); void session.cancel({ turnId }).catch((error: unknown) => { if (cancellationRef.current !== cancellation) { return; } cancellation.requested = false; cancellation.sentTurnId = undefined; setCancellationError(toErrorMessage(error)); setCancellationState("idle"); }); }, [session], ); const handleEvent = useCallback( (event: HandleMessageStreamEvent) => { if (event.type !== "turn.started") { return; } const cancellation = cancellationRef.current; cancellation.turnId = event.data.turnId; cancelTurn(event.data.turnId); }, [cancelTurn], ); const agent = useEveAgent({ onEvent: handleEvent, session }); const isBusy = agent.status === "submitted" || agent.status === "streaming"; const isEmpty = agent.data.messages.length === 0; const errorMessage = cancellationError ?? agent.error?.message; const submitStatus = isBusy && cancellationState !== "idle" ? "submitted" : agent.status; const prepareTurn = () => { cancellationRef.current = { requested: false }; setCancellationError(undefined); setCancellationState("idle"); }; const requestCancellation = () => { if (!isBusy || cancellationState !== "idle") { return; } const cancellation = cancellationRef.current; cancellation.requested = true; setCancellationError(undefined); setCancellationState("requested"); if (cancellation.turnId !== undefined) { cancelTurn(cancellation.turnId); } }; const handleSubmit = async (message: PromptInputMessage) => { const text = message.text.trim(); if ((text.length === 0 && message.files.length === 0) || isBusy) return; prepareTurn(); if (message.files.length === 0) { await agent.send({ message: text }); return; } const parts: UserContent = []; if (text.length > 0) { parts.push({ text, type: "text" }); } for (const file of message.files) { parts.push({ data: file.url, filename: file.filename, mediaType: file.mediaType, type: "file", }); } await agent.send({ message: parts }); }; const composer = ( <PromptInput onSubmit={handleSubmit}> <PromptInputTextarea placeholder="Send a message…" /> <PromptInputSubmit onStop={requestCancellation} status={submitStatus} /> </PromptInput> ); return ( <main className="flex h-dvh flex-col overflow-hidden bg-background text-foreground"> {isEmpty ? null : ( <header className="flex h-14 shrink-0 items-center justify-center gap-3 pl-4 pr-2"> <span className="flex min-w-0 items-center gap-2"> <span className="truncate text-muted-foreground text-sm">{AGENT_NAME}</span> <StatusDot status={agent.status} /> </span> </header> )} {errorMessage ? ( <div className="mx-auto w-full max-w-3xl shrink-0 px-4 pt-2 sm:px-6"> <div className="flex items-start gap-3 rounded-lg border border-destructive/30 bg-destructive/5 px-3 py-2.5 text-sm"> <AlertCircleIcon className="mt-0.5 size-4 shrink-0 text-destructive" /> <div> <p className="font-medium">Request failed</p> <p className="mt-0.5 text-muted-foreground">{errorMessage}</p> </div> </div> </div> ) : null} {isEmpty ? null : ( <Conversation className="min-h-0 flex-1"> <ConversationContent className="mx-auto w-full max-w-3xl gap-6 px-4 py-6 sm:px-6"> {agent.data.messages.map((message, index) => ( <AgentMessage canRespond={!isBusy} isStreaming={ agent.status === "streaming" && index === agent.data.messages.length - 1 } key={message.id} message={message} onInputResponses={(inputResponses) => { prepareTurn(); return agent.send({ inputResponses }); }} /> ))} </ConversationContent> <ConversationScrollButton /> </Conversation> )} <div className={cn( "mx-auto w-full px-4 sm:px-6", isEmpty ? "flex max-w-xl flex-1 flex-col items-center justify-center gap-8 pb-[10vh]" : "max-w-3xl shrink-0 pb-6", )} > {isEmpty ? ( <div className="flex flex-col items-center gap-3 text-center"> <h1 className="font-medium text-5xl tracking-tighter">{AGENT_NAME}</h1> </div> ) : null} <div className="w-full">{composer}</div> </div> </main> ); } function toErrorMessage(error: unknown): string { return error instanceof Error ? error.message : "Unable to cancel the response."; } function StatusDot({ status }: { readonly status: AgentStatus }) { const isLive = status === "submitted" || status === "streaming"; const tone = status === "error" ? "bg-destructive" : isLive ? "bg-emerald-500" : status === "ready" ? "bg-muted-foreground" : "bg-muted-foreground/50"; return ( <span className="relative flex size-1"> {isLive ? ( <span className={cn( "absolute inline-flex size-full animate-ping rounded-full opacity-75", tone, )} /> ) : null} <span className={cn("relative inline-flex size-1 rounded-full transition-colors", tone)} /> </span> ); } `,"app/_components/agent-message.tsx":`"use client"; import type { EveAuthorizationPart, EveDynamicToolPart, EveMessage, EveMessagePart, } from "eve/react"; import { CheckCircleIcon, ExternalLinkIcon, FileIcon, ImageIcon, KeyRoundIcon, XCircleIcon, } from "lucide-react"; import { Message, MessageContent, MessageResponse } from "@/components/ai-elements/message"; import { Reasoning, ReasoningContent, ReasoningTrigger } from "@/components/ai-elements/reasoning"; import { Tool, ToolContent, ToolHeader, ToolInput, ToolOutput, } from "@/components/ai-elements/tool"; import { Button } from "@/components/ui/button"; import { cn } from "@/lib/utils"; export type AgentInputResponse = { readonly optionId?: string; readonly requestId: string; readonly text?: string; }; type EveFilePart = Extract<EveMessagePart, { type: "file" }>; export function AgentMessage({ canRespond, isStreaming, message, onInputResponses, }: { readonly canRespond: boolean; readonly isStreaming: boolean; readonly message: EveMessage; readonly onInputResponses: (responses: readonly AgentInputResponse[]) => void | Promise<void>; }) { const lastTextIndex = message.parts.reduce( (last, part, index) => (part.type === "text" ? index : last), -1, ); return ( <Message data-optimistic={message.metadata?.optimistic ? "true" : undefined} from={message.role} > <MessageContent> {message.parts.map((part, index) => ( <AgentMessagePart canRespond={canRespond} key={partKey(part, index)} onInputResponses={onInputResponses} part={part} showCaret={isStreaming && message.role === "assistant" && index === lastTextIndex} /> ))} </MessageContent> </Message> ); } function AgentMessagePart({ canRespond, onInputResponses, part, showCaret, }: { readonly canRespond: boolean; readonly onInputResponses: (responses: readonly AgentInputResponse[]) => void | Promise<void>; readonly part: EveMessagePart; readonly showCaret: boolean; }) { switch (part.type) { case "step-start": return null; case "text": return ( <MessageResponse caret="block" isAnimating={showCaret}> {part.text} </MessageResponse> ); case "reasoning": return ( <Reasoning defaultOpen isStreaming={part.state === "streaming"}> <ReasoningTrigger /> <ReasoningContent>{part.text}</ReasoningContent> </Reasoning> ); case "file": return <AttachmentPart part={part} />; case "authorization": return <AuthorizationPrompt part={part} />; case "dynamic-tool": return ( <Tool defaultOpen={part.state === "approval-requested" || part.state === "approval-responded"} > <ToolHeader state={part.state} title={part.toolName} toolName={part.toolName} type="dynamic-tool" /> <ToolContent> <ToolInput input={part.input} /> <InputRequestActions canRespond={canRespond} part={part} onInputResponses={onInputResponses} /> <ToolOutput errorText={part.errorText} output={part.output} /> </ToolContent> </Tool> ); } } function AttachmentPart({ part }: { readonly part: EveFilePart }) { const label = part.filename ?? "Attachment"; const detail = [part.mediaType, formatBytes(part.size)].filter(Boolean).join(" - "); const isImage = part.mediaType.startsWith("image/") && part.url !== undefined; const Icon = isImage ? ImageIcon : FileIcon; const body = ( <span className="flex max-w-sm items-center gap-3 rounded-md border bg-background/60 p-2 text-sm"> {isImage ? ( <img alt={label} className="size-12 shrink-0 rounded-sm object-cover" src={part.url} /> ) : ( <span className="flex size-10 shrink-0 items-center justify-center rounded-sm bg-muted text-muted-foreground"> <Icon className="size-4" /> </span> )} <span className="min-w-0 flex-1"> <span className="block truncate font-medium">{label}</span> {detail ? <span className="block truncate text-muted-foreground">{detail}</span> : null} </span> {part.url ? <ExternalLinkIcon className="size-4 shrink-0 text-muted-foreground" /> : null} </span> ); return part.url ? ( <a href={part.url} rel="noreferrer" target="_blank"> {body} </a> ) : ( body ); } function AuthorizationPrompt({ part }: { readonly part: EveAuthorizationPart }) { const isAuthorized = part.state === "completed" && part.outcome === "authorized"; const isCompleted = part.state === "completed"; const Icon = isAuthorized ? CheckCircleIcon : isCompleted ? XCircleIcon : KeyRoundIcon; const instructions = part.authorization?.instructions; const shouldShowInstructions = instructions !== undefined && instructions !== part.description; return ( <div className={cn( "space-y-3 rounded-md border p-3", isAuthorized ? "border-emerald-500/30 bg-emerald-500/5" : isCompleted ? "border-destructive/30 bg-destructive/5" : "border-blue-500/30 bg-blue-500/5", )} > <div className="flex items-start gap-3"> <span className={cn( "mt-0.5 flex size-8 shrink-0 items-center justify-center rounded-full", isAuthorized ? "bg-emerald-500/10 text-emerald-700 dark:text-emerald-300" : isCompleted ? "bg-destructive/10 text-destructive" : "bg-blue-500/10 text-blue-700 dark:text-blue-300", )} > <Icon className="size-4" /> </span> <div className="min-w-0 flex-1 space-y-2"> <p className="font-medium text-sm">{authorizationTitle(part)}</p> <p className="text-muted-foreground text-sm">{authorizationDescription(part)}</p> {shouldShowInstructions ? ( <p className="text-muted-foreground text-sm">{instructions}</p> ) : null} {part.state === "required" && part.authorization?.userCode ? ( <div className="flex flex-wrap items-center gap-2 text-sm"> <span className="text-muted-foreground">Code</span> <code className="rounded-md bg-background px-2 py-1 font-mono"> {part.authorization.userCode} </code> </div> ) : null} {part.state === "required" && part.authorization?.url ? ( <Button asChild size="sm"> <a href={part.authorization.url} rel="noreferrer" target="_blank"> <ExternalLinkIcon className="size-4" /> Sign in with {part.displayName} </a> </Button> ) : null} </div> </div> </div> ); } function authorizationTitle(part: EveAuthorizationPart): string { if (part.state === "required") { return \`Connect \${part.displayName}\`; } if (part.outcome === "authorized") { return \`\${part.displayName} connected\`; } return \`\${part.displayName} authorization \${formatAuthorizationOutcome(part.outcome)}\`; } function authorizationDescription(part: EveAuthorizationPart): string { if (part.state === "required") { return part.description; } if (part.outcome === "authorized") { return \`\${part.displayName} connected.\`; } const tail = part.reason !== undefined ? \` (\${part.reason})\` : ""; return \`\${part.displayName} authorization \${formatAuthorizationOutcome(part.outcome)}\${tail}.\`; } function formatAuthorizationOutcome(outcome: NonNullable<EveAuthorizationPart["outcome"]>): string { switch (outcome) { case "authorized": return "authorized"; case "declined": return "declined"; case "failed": return "failed"; case "timed-out": return "timed out"; } } function formatBytes(size: number | undefined): string | undefined { if (size === undefined) { return undefined; } if (size < 1024) { return \`\${size} B\`; } if (size < 1024 * 1024) { return \`\${(size / 1024).toFixed(1)} KB\`; } return \`\${(size / (1024 * 1024)).toFixed(1)} MB\`; } function InputRequestActions({ canRespond, onInputResponses, part, }: { readonly canRespond: boolean; readonly onInputResponses: (responses: readonly AgentInputResponse[]) => void | Promise<void>; readonly part: EveDynamicToolPart; }) { const inputRequest = part.toolMetadata?.eve?.inputRequest; if (!inputRequest) { return null; } const inputResponse = part.toolMetadata?.eve?.inputResponse; const selectedOption = inputRequest.options?.find( (option) => option.id === inputResponse?.optionId, ); return ( <div className="space-y-3 rounded-md border border-yellow-500/30 bg-yellow-500/5 p-3"> <p className="text-muted-foreground text-sm">{inputRequest.prompt}</p> {inputResponse ? ( <p className="font-medium text-sm"> Responded: {selectedOption?.label ?? inputResponse.text ?? inputResponse.optionId} </p> ) : ( <div className="flex flex-wrap gap-2"> {inputRequest.options?.map((option) => ( <Button disabled={!canRespond} key={option.id} onClick={() => { void onInputResponses([ { optionId: option.id, requestId: inputRequest.requestId, }, ]); }} size="sm" type="button" variant={option.style === "danger" ? "destructive" : "default"} > {option.label} </Button> ))} </div> )} </div> ); } function partKey(part: EveMessagePart, index: number): string { switch (part.type) { case "authorization": return \`authorization:\${part.turnId}:\${part.stepIndex}:\${part.name}\`; case "dynamic-tool": return part.toolCallId; default: return \`\${part.type}:\${index}\`; } } `,"app/globals.css":`@import "tailwindcss"; @source "../node_modules/streamdown/dist/*.js"; @theme inline { --color-background: var(--background); --color-foreground: var(--foreground); --color-card: var(--card); --color-card-foreground: var(--card-foreground); --color-popover: var(--popover); --color-popover-foreground: var(--popover-foreground); --color-primary: var(--primary); --color-primary-foreground: var(--primary-foreground); --color-secondary: var(--secondary); --color-secondary-foreground: var(--secondary-foreground); --color-muted: var(--muted); --color-muted-foreground: var(--muted-foreground); --color-accent: var(--accent); --color-accent-foreground: var(--accent-foreground); --color-destructive: var(--destructive); --color-border: var(--border); --color-input: var(--input); --color-ring: var(--ring); --radius-sm: calc(var(--radius) - 4px); --radius-md: calc(var(--radius) - 2px); --radius-lg: var(--radius); --radius-xl: calc(var(--radius) + 4px); --font-sans: "Geist", "Geist Fallback", ui-sans-serif, system-ui, sans-serif; --font-mono: "Geist Mono", "Geist Mono Fallback", ui-monospace, monospace; } :root { color-scheme: light; /* Soft neutral page with white elevated surfaces so cards/composer pop. */ --background: oklch(0.971 0 0); --foreground: oklch(0.16 0 0); --card: oklch(1 0 0); --card-foreground: oklch(0.16 0 0); --popover: oklch(1 0 0); --popover-foreground: oklch(0.16 0 0); --primary: oklch(0.19 0 0); --primary-foreground: oklch(0.985 0 0); --secondary: oklch(0.94 0 0); --secondary-foreground: oklch(0.19 0 0); --muted: oklch(0.94 0 0); --muted-foreground: oklch(0.6 0 0); --accent: oklch(0.94 0 0); --accent-foreground: oklch(0.19 0 0); --destructive: oklch(0.577 0.245 27.325); --border: oklch(0.916 0 0); --input: oklch(0.916 0 0); --ring: oklch(0.708 0 0); --radius: 0.625rem; } @media (prefers-color-scheme: dark) { :root { color-scheme: dark; --background: oklch(0.145 0 0); --foreground: oklch(0.985 0 0); --card: oklch(0.205 0 0); --card-foreground: oklch(0.985 0 0); --popover: oklch(0.205 0 0); --popover-foreground: oklch(0.985 0 0); --primary: oklch(0.922 0 0); --primary-foreground: oklch(0.205 0 0); --secondary: oklch(0.269 0 0); --secondary-foreground: oklch(0.985 0 0); --muted: oklch(0.269 0 0); --muted-foreground: oklch(0.708 0 0); --accent: oklch(0.269 0 0); --accent-foreground: oklch(0.985 0 0); --destructive: oklch(0.704 0.191 22.216); --border: oklch(1 0 0 / 10%); --input: oklch(1 0 0 / 15%); --ring: oklch(0.556 0 0); } } * { border-color: var(--border); } html { height: 100%; } body { min-height: 100%; margin: 0; background: var(--background); font-family: var(--font-sans); } button, input, textarea { font: inherit; } `,"app/layout.tsx":`import type { Metadata } from "next"; import { Geist, Geist_Mono } from "next/font/google"; import type { ReactNode } from "react"; import { TooltipProvider } from "@/components/ui/tooltip"; import { cn } from "@/lib/utils"; import "./globals.css"; const sans = Geist({ variable: "--font-sans", subsets: ["latin"], weight: "variable", display: "swap", }); const mono = Geist_Mono({ variable: "--font-mono", subsets: ["latin"], weight: "variable", display: "swap", }); export const metadata: Metadata = { title: "__EVE_INIT_APP_NAME__", description: "A Next.js starter for eve agents with AI Elements.", }; export default function RootLayout({ children }: { readonly children: ReactNode }) { return ( <html className={cn(sans.variable, mono.variable)} lang="en"> <body> <TooltipProvider>{children}</TooltipProvider> </body> </html> ); } `,"app/page.tsx":`import { AgentChat } from "@/app/_components/agent-chat"; export default function Page() { return <AgentChat />; } `,"components/ai-elements/chain-of-thought.tsx":`"use client"; import { useControllableState } from "@radix-ui/react-use-controllable-state"; import { Badge } from "@/components/ui/badge"; import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@/components/ui/collapsible"; import { cn } from "@/lib/utils"; import type { LucideIcon } from "lucide-react"; import { BrainIcon, ChevronDownIcon, DotIcon } from "lucide-react"; import type { ComponentProps, ReactNode } from "react"; import { createContext, memo, useContext, useMemo } from "react"; interface ChainOfThoughtContextValue { isOpen: boolean; setIsOpen: (open: boolean) => void; } const ChainOfThoughtContext = createContext<ChainOfThoughtContextValue | null>(null); const useChainOfThought = () => { const context = useContext(ChainOfThoughtContext); if (!context) { throw new Error("ChainOfThought components must be used within ChainOfThought"); } return context; }; export type ChainOfThoughtProps = ComponentProps<"div"> & { open?: boolean; defaultOpen?: boolean; onOpenChange?: (open: boolean) => void; }; export const ChainOfThought = memo( ({ className, open, defaultOpen = false, onOpenChange, children, ...props }: ChainOfThoughtProps) => { const [isOpen, setIsOpen] = useControllableState({ defaultProp: defaultOpen, onChange: onOpenChange, prop: open, }); const chainOfThoughtContext = useMemo(() => ({ isOpen, setIsOpen }), [isOpen, setIsOpen]); return ( <ChainOfThoughtContext.Provider value={chainOfThoughtContext}> <div className={cn("not-prose w-full space-y-4", className)} {...props}> {children} </div> </ChainOfThoughtContext.Provider> ); }, ); export type ChainOfThoughtHeaderProps = ComponentProps<typeof CollapsibleTrigger>; export const ChainOfThoughtHeader = memo( ({ className, children, ...props }: ChainOfThoughtHeaderProps) => { const { isOpen, setIsOpen } = useChainOfThought(); return ( <Collapsible onOpenChange={setIsOpen} open={isOpen}> <CollapsibleTrigger className={cn( "flex w-full items-center gap-2 text-muted-foreground text-sm transition-colors hover:text-foreground", className, )} {...props} > <BrainIcon className="size-4" /> <span className="flex-1 text-left">{children ?? "Chain of Thought"}</span> <ChevronDownIcon className={cn("size-4 transition-transform", isOpen ? "rotate-180" : "rotate-0")} /> </CollapsibleTrigger> </Collapsible> ); }, ); export type ChainOfThoughtStepProps = ComponentProps<"div"> & { icon?: LucideIcon; label: ReactNode; description?: ReactNode; status?: "complete" | "active" | "pending"; }; const stepStatusStyles = { active: "text-foreground", complete: "text-muted-foreground", pending: "text-muted-foreground/50", }; export const ChainOfThoughtStep = memo( ({ className, icon: Icon = DotIcon, label, description, status = "complete", children, ...props }: ChainOfThoughtStepProps) => ( <div className={cn( "flex gap-2 text-sm", stepStatusStyles[status], "fade-in-0 slide-in-from-top-2 animate-in", className, )} {...props} > <div className="relative mt-0.5"> <Icon className="size-4" /> <div className="absolute top-7 bottom-0 left-1/2 -mx-px w-px bg-border" /> </div> <div className="flex-1 space-y-2 overflow-hidden"> <div>{label}</div> {description && <div className="text-muted-foreground text-xs">{description}</div>} {children} </div> </div> ), ); export type ChainOfThoughtSearchResultsProps = ComponentProps<"div">; export const ChainOfThoughtSearchResults = memo( ({ className, ...props }: ChainOfThoughtSearchResultsProps) => ( <div className={cn("flex flex-wrap items-center gap-2", className)} {...props} /> ), ); export type ChainOfThoughtSearchResultProps = ComponentProps<typeof Badge>; export const ChainOfThoughtSearchResult = memo( ({ className, children, ...props }: ChainOfThoughtSearchResultProps) => ( <Badge className={cn("gap-1 px-2 py-0.5 font-normal text-xs", className)} variant="secondary" {...props} > {children} </Badge> ), ); export type ChainOfThoughtContentProps = ComponentProps<typeof CollapsibleContent>; export const ChainOfThoughtContent = memo( ({ className, children, ...props }: ChainOfThoughtContentProps) => { const { isOpen } = useChainOfThought(); return ( <Collapsible open={isOpen}> <CollapsibleContent className={cn( "mt-2 space-y-3", "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", className, )} {...props} > {children} </CollapsibleContent> </Collapsible> ); }, ); export type ChainOfThoughtImageProps = ComponentProps<"div"> & { caption?: string; }; export const ChainOfThoughtImage = memo( ({ className, children, caption, ...props }: ChainOfThoughtImageProps) => ( <div className={cn("mt-2 space-y-2", className)} {...props}> <div className="relative flex max-h-[22rem] items-center justify-center overflow-hidden rounded-lg bg-muted p-3"> {children} </div> {caption && <p className="text-muted-foreground text-xs">{caption}</p>} </div> ), ); ChainOfThought.displayName = "ChainOfThought"; ChainOfThoughtHeader.displayName = "ChainOfThoughtHeader"; ChainOfThoughtStep.displayName = "ChainOfThoughtStep"; ChainOfThoughtSearchResults.displayName = "ChainOfThoughtSearchResults"; ChainOfThoughtSearchResult.displayName = "ChainOfThoughtSearchResult"; ChainOfThoughtContent.displayName = "ChainOfThoughtContent"; ChainOfThoughtImage.displayName = "ChainOfThoughtImage"; `,"components/ai-elements/code-block.tsx":`"use client"; import { Button } from "@/components/ui/button"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "@/components/ui/select"; import { cn } from "@/lib/utils"; import { CheckIcon, CopyIcon } from "lucide-react"; import type { ComponentProps, CSSProperties, HTMLAttributes } from "react"; import { createContext, memo, useCallback, useContext, useEffect, useMemo, useRef, useState, } from "react"; import type { BundledLanguage, BundledTheme, HighlighterGeneric, ThemedToken } from "shiki"; import { createHighlighter } from "shiki"; // Shiki uses bitflags for font styles: 1=italic, 2=bold, 4=underline // oxlint-disable-next-line eslint(no-bitwise) const isItalic = (fontStyle: number | undefined) => fontStyle && fontStyle & 1; // oxlint-disable-next-line eslint(no-bitwise) const isBold = (fontStyle: number | undefined) => fontStyle && fontStyle & 2; const isUnderline = (fontStyle: number | undefined) => // oxlint-disable-next-line eslint(no-bitwise) fontStyle && fontStyle & 4; // Transform tokens to include pre-computed keys to avoid noArrayIndexKey lint interface KeyedToken { token: ThemedToken; key: string; } interface KeyedLine { tokens: KeyedToken[]; key: string; } const addKeysToTokens = (lines: ThemedToken[][]): KeyedLine[] => lines.map((line, lineIdx) => ({ key: \`line-\${lineIdx}\`, tokens: line.map((token, tokenIdx) => ({ key: \`line-\${lineIdx}-\${tokenIdx}\`, token, })), })); // Token rendering component const TokenSpan = ({ token }: { token: ThemedToken }) => ( <span className="dark:!bg-[var(--shiki-dark-bg)] dark:!text-[var(--shiki-dark)]" style={ { backgroundColor: token.bgColor, color: token.color, fontStyle: isItalic(token.fontStyle) ? "italic" : undefined, fontWeight: isBold(token.fontStyle) ? "bold" : undefined, textDecoration: isUnderline(token.fontStyle) ? "underline" : undefined, ...token.htmlStyle, } as CSSProperties } > {token.content} </span> ); // Line number styles using CSS counters const LINE_NUMBER_CLASSES = cn( "block", "before:content-[counter(line)]", "before:inline-block", "before:[counter-increment:line]", "before:w-8", "before:mr-4", "before:text-right", "before:text-muted-foreground/50", "before:font-mono", "before:select-none", ); // Line rendering component const LineSpan = ({ keyedLine, showLineNumbers, }: { keyedLine: KeyedLine; showLineNumbers: boolean; }) => ( <span className={showLineNumbers ? LINE_NUMBER_CLASSES : "block"}> {keyedLine.tokens.length === 0 ? "\\n" : keyedLine.tokens.map(({ token, key }) => <TokenSpan key={key} token={token} />)} </span> ); // Types type CodeBlockProps = HTMLAttributes<HTMLDivElement> & { code: string; language: BundledLanguage; showLineNumbers?: boolean; }; interface TokenizedCode { tokens: ThemedToken[][]; fg: string; bg: string; } interface CodeBlockContextType { code: string; } // Context const CodeBlockContext = createContext<CodeBlockContextType>({ code: "", }); // Highlighter cache (singleton per language) const highlighterCache = new Map< string, Promise<HighlighterGeneric<BundledLanguage, BundledTheme>> >(); // Token cache const tokensCache = new Map<string, TokenizedCode>(); // Subscribers for async token updates const subscribers = new Map<string, Set<(result: TokenizedCode) => void>>(); const getTokensCacheKey = (code: string, language: BundledLanguage) => { const start = code.slice(0, 100); const end = code.length > 100 ? code.slice(-100) : ""; return \`\${language}:\${code.length}:\${start}:\${end}\`; }; const getHighlighter = ( language: BundledLanguage, ): Promise<HighlighterGeneric<BundledLanguage, BundledTheme>> => { const cached = highlighterCache.get(language); if (cached) { return cached; } const highlighterPromise = createHighlighter({ langs: [language], themes: ["github-light", "github-dark"], }); highlighterCache.set(language, highlighterPromise); return highlighterPromise; }; // Create raw tokens for immediate display while highlighting loads const createRawTokens = (code: string): TokenizedCode => ({ bg: "transparent", fg: "inherit", tokens: code.split("\\n").map((line) => line === "" ? [] : [ { color: "inherit", content: line, } as ThemedToken, ], ), }); // Synchronous highlight with callback for async results export const highlightCode = ( code: string, language: BundledLanguage, // oxlint-disable-next-line eslint-plugin-promise(prefer-await-to-callbacks) callback?: (result: TokenizedCode) => void, ): TokenizedCode | null => { const tokensCacheKey = getTokensCacheKey(code, language); // Return cached result if available const cached = tokensCache.get(tokensCacheKey); if (cached) { return cached; } // Subscribe callback if provided if (callback) { if (!subscribers.has(tokensCacheKey)) { subscribers.set(tokensCacheKey, new Set()); } subscribers.get(tokensCacheKey)?.add(callback); } // Start highlighting in background - fire-and-forget async pattern getHighlighter(language) // oxlint-disable-next-line eslint-plugin-promise(prefer-await-to-then) .then((highlighter) => { const availableLangs = highlighter.getLoadedLanguages(); const langToUse = availableLangs.includes(language) ? language : "text"; const result = highlighter.codeToTokens(code, { lang: langToUse, themes: { dark: "github-dark", light: "github-light", }, }); const tokenized: TokenizedCode = { bg: result.bg ?? "transparent", fg: result.fg ?? "inherit", tokens: result.tokens, }; // Cache the result tokensCache.set(tokensCacheKey, tokenized); // Notify all subscribers const subs = subscribers.get(tokensCacheKey); if (subs) { for (const sub of subs) { sub(tokenized); } subscribers.delete(tokensCacheKey); } }) // oxlint-disable-next-line eslint-plugin-promise(prefer-await-to-then), eslint-plugin-promise(prefer-await-to-callbacks) .catch((error) => { console.error("Failed to highlight code:", error); subscribers.delete(tokensCacheKey); }); return null; }; const CodeBlockBody = memo( ({ tokenized, showLineNumbers, className, }: { tokenized: TokenizedCode; showLineNumbers: boolean; className?: string; }) => { const preStyle = useMemo( () => ({ backgroundColor: tokenized.bg, color: tokenized.fg, }), [tokenized.bg, tokenized.fg], ); const keyedLines = useMemo(() => addKeysToTokens(tokenized.tokens), [tokenized.tokens]); return ( <pre className={cn( "dark:!bg-[var(--shiki-dark-bg)] dark:!text-[var(--shiki-dark)] m-0 p-4 text-sm", className, )} style={preStyle} > <code className={cn( "font-mono text-sm", showLineNumbers && "[counter-increment:line_0] [counter-reset:line]", )} > {keyedLines.map((keyedLine) => ( <LineSpan key={keyedLine.key} keyedLine={keyedLine} showLineNumbers={showLineNumbers} /> ))} </code> </pre> ); }, (prevProps, nextProps) => prevProps.tokenized === nextProps.tokenized && prevProps.showLineNumbers === nextProps.showLineNumbers && prevProps.className === nextProps.className, ); CodeBlockBody.displayName = "CodeBlockBody"; export const CodeBlockContainer = ({ className, language, style, ...props }: HTMLAttributes<HTMLDivElement> & { language: string }) => ( <div className={cn( "group relative w-full overflow-hidden rounded-md border bg-background text-foreground", className, )} data-language={language} style={{ containIntrinsicSize: "auto 200px", contentVisibility: "auto", ...style, }} {...props} /> ); export const CodeBlockHeader = ({ children, className, ...props }: HTMLAttributes<HTMLDivElement>) => ( <div className={cn( "flex items-center justify-between border-b bg-muted/80 px-3 py-2 text-muted-foreground text-xs", className, )} {...props} > {children} </div> ); export const CodeBlockTitle = ({ children, className, ...props }: HTMLAttributes<HTMLDivElement>) => ( <div className={cn("flex items-center gap-2", className)} {...props}> {children} </div> ); export const CodeBlockFilename = ({ children, className, ...props }: HTMLAttributes<HTMLSpanElement>) => ( <span className={cn("font-mono", className)} {...props}> {children} </span> ); export const CodeBlockActions = ({ children, className, ...props }: HTMLAttributes<HTMLDivElement>) => ( <div className={cn("-my-1 -mr-1 flex items-center gap-2", className)} {...props}> {children} </div> ); export const CodeBlockContent = ({ code, language, showLineNumbers = false, }: { code: string; language: BundledLanguage; showLineNumbers?: boolean; }) => { // Memoized raw tokens for immediate display const rawTokens = useMemo(() => createRawTokens(code), [code]); // Synchronous cache lookup — avoids setState in effect for cached results const syncTokens = useMemo( () => highlightCode(code, language) ?? rawTokens, [code, language, rawTokens], ); // Async highlighting result (populated after shiki loads) const [asyncTokens, setAsyncTokens] = useState<TokenizedCode | null>(null); const asyncKeyRef = useRef({ code, language }); // Invalidate stale async tokens synchronously during render if (asyncKeyRef.current.code !== code || asyncKeyRef.current.language !== language) { asyncKeyRef.current = { code, language }; setAsyncTokens(null); } useEffect(() => { let cancelled = false; highlightCode(code, language, (result) => { if (!cancelled) { setAsyncTokens(result); } }); return () => { cancelled = true; }; }, [code, language]); const tokenized = asyncTokens ?? syncTokens; return ( <div className="relative overflow-auto"> <CodeBlockBody showLineNumbers={showLineNumbers} tokenized={tokenized} /> </div> ); }; export const CodeBlock = ({ code, language, showLineNumbers = false, className, children, ...props }: CodeBlockProps) => { const contextValue = useMemo(() => ({ code }), [code]); return ( <CodeBlockContext.Provider value={contextValue}> <CodeBlockContainer className={className} language={language} {...props}> {children} <CodeBlockContent code={code} language={language} showLineNumbers={showLineNumbers} /> </CodeBlockContainer> </CodeBlockContext.Provider> ); }; export type CodeBlockCopyButtonProps = ComponentProps<typeof Button> & { onCopy?: () => void; onError?: (error: Error) => void; timeout?: number; }; export const CodeBlockCopyButton = ({ onCopy, onError, timeout = 2000, children, className, ...props }: CodeBlockCopyButtonProps) => { const [isCopied, setIsCopied] = useState(false); const timeoutRef = useRef<number>(0); const { code } = useContext(CodeBlockContext); const copyToClipboard = useCallback(async () => { if (typeof window === "undefined" || !navigator?.clipboard?.writeText) { onError?.(new Error("Clipboard API not available")); return; } try { if (!isCopied) { await navigator.clipboard.writeText(code); setIsCopied(true); onCopy?.(); timeoutRef.current = window.setTimeout(() => setIsCopied(false), timeout); } } catch (error) { onError?.(error as Error); } }, [code, onCopy, onError, timeout, isCopied]); useEffect( () => () => { window.clearTimeout(timeoutRef.current); }, [], ); const Icon = isCopied ? CheckIcon : CopyIcon; return ( <Button className={cn("shrink-0", className)} onClick={copyToClipboard} size="icon" variant="ghost" {...props} > {children ?? <Icon size={14} />} </Button> ); }; export type CodeBlockLanguageSelectorProps = ComponentProps<typeof Select>; export const CodeBlockLanguageSelector = (props: CodeBlockLanguageSelectorProps) => ( <Select {...props} /> ); export type CodeBlockLanguageSelectorTriggerProps = ComponentProps<typeof SelectTrigger>; export const CodeBlockLanguageSelectorTrigger = ({ className, ...props }: CodeBlockLanguageSelectorTriggerProps) => ( <SelectTrigger className={cn("h-7 border-none bg-transparent px-2 text-xs shadow-none", className)} size="sm" {...props} /> ); export type CodeBlockLanguageSelectorValueProps = ComponentProps<typeof SelectValue>; export const CodeBlockLanguageSelectorValue = (props: CodeBlockLanguageSelectorValueProps) => ( <SelectValue {...props} /> ); export type CodeBlockLanguageSelectorContentProps = ComponentProps<typeof SelectContent>; export const CodeBlockLanguageSelectorContent = ({ align = "end", ...props }: CodeBlockLanguageSelectorContentProps) => <SelectContent align={align} {...props} />; export type CodeBlockLanguageSelectorItemProps = ComponentProps<typeof SelectItem>; export const CodeBlockLanguageSelectorItem = (props: CodeBlockLanguageSelectorItemProps) => ( <SelectItem {...props} /> ); `,"components/ai-elements/conversation.tsx":`"use client"; import { Button } from "@/components/ui/button"; import { cn } from "@/lib/utils"; import type { UIMessage } from "ai"; import { ArrowDownIcon, DownloadIcon } from "lucide-react"; import type { ComponentProps } from "react"; import { useCallback } from "react"; import { StickToBottom, useStickToBottomContext } from "use-stick-to-bottom"; export type ConversationProps = ComponentProps<typeof StickToBottom>; export const Conversation = ({ className, ...props }: ConversationProps) => ( <StickToBottom className={cn("relative flex-1 overflow-y-hidden", className)} initial="smooth" resize="smooth" role="log" {...props} /> ); export type ConversationContentProps = ComponentProps<typeof StickToBottom.Content>; export const ConversationContent = ({ className, ...props }: ConversationContentProps) => ( <StickToBottom.Content className={cn("flex flex-col gap-8 p-4", className)} {...props} /> ); export type ConversationEmptyStateProps = ComponentProps<"div"> & { title?: string; description?: string; icon?: React.ReactNode; }; export const ConversationEmptyState = ({ className, title = "No messages yet", description = "Start a conversation to see messages here", icon, children, ...props }: ConversationEmptyStateProps) => ( <div className={cn( "flex size-full flex-col items-center justify-center gap-3 p-8 text-center", className, )} {...props} > {children ?? ( <> {icon && <div className="text-muted-foreground">{icon}</div>} <div className="space-y-1"> <h3 className="font-medium text-sm">{title}</h3> {description && <p className="text-muted-foreground text-sm">{description}</p>} </div> </> )} </div> ); export type ConversationScrollButtonProps = ComponentProps<typeof Button>; export const ConversationScrollButton = ({ className, ...props }: ConversationScrollButtonProps) => { const { isAtBottom, scrollToBottom } = useStickToBottomContext(); const handleScrollToBottom = useCallback(() => { scrollToBottom(); }, [scrollToBottom]); return ( !isAtBottom && ( <Button className={cn( "absolute bottom-4 left-[50%] translate-x-[-50%] rounded-full dark:bg-background dark:hover:bg-muted", className, )} onClick={handleScrollToBottom} size="icon" type="button" variant="outline" {...props} > <ArrowDownIcon className="size-4" /> </Button> ) ); }; const getMessageText = (message: UIMessage): string => message.parts .filter((part) => part.type === "text") .map((part) => part.text) .join(""); export type ConversationDownloadProps = Omit<ComponentProps<typeof Button>, "onClick"> & { messages: UIMessage[]; filename?: string; formatMessage?: (message: UIMessage, index: number) => string; }; const defaultFormatMessage = (message: UIMessage): string => { const roleLabel = message.role.charAt(0).toUpperCase() + message.role.slice(1); return \`**\${roleLabel}:** \${getMessageText(message)}\`; }; export const messagesToMarkdown = ( messages: UIMessage[], formatMessage: (message: UIMessage, index: number) => string = defaultFormatMessage, ): string => messages.map((msg, i) => formatMessage(msg, i)).join("\\n\\n"); export const ConversationDownload = ({ messages, filename = "conversation.md", formatMessage = defaultFormatMessage, className, children, ...props }: ConversationDownloadProps) => { const handleDownload = useCallback(() => { const markdown = messagesToMarkdown(messages, formatMessage); const blob = new Blob([markdown], { type: "text/markdown" }); const url = URL.createObjectURL(blob); const link = document.createElement("a"); link.href = url; link.download = filename; document.body.append(link); link.click(); link.remove(); URL.revokeObjectURL(url); }, [messages, filename, formatMessage]); return ( <Button className={cn( "absolute top-4 right-4 rounded-full dark:bg-background dark:hover:bg-muted", className, )} onClick={handleDownload} size="icon" type="button" variant="outline" {...props} > {children ?? <DownloadIcon className="size-4" />} </Button> ); }; `,"components/ai-elements/message.tsx":`"use client"; import { Button } from "@/components/ui/button"; import { ButtonGroup, ButtonGroupText } from "@/components/ui/button-group"; import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip"; import { cn } from "@/lib/utils"; import { cjk } from "@streamdown/cjk"; import { code } from "@streamdown/code"; import { math } from "@streamdown/math"; import { mermaid } from "@streamdown/mermaid"; import type { UIMessage } from "ai"; import { ChevronLeftIcon, ChevronRightIcon } from "lucide-react"; import type { ComponentProps, HTMLAttributes, ReactElement } from "react"; import { createContext, memo, useCallback, useContext, useEffect, useMemo, useState } from "react"; import { Streamdown } from "streamdown"; export type MessageProps = HTMLAttributes<HTMLDivElement> & { from: UIMessage["role"]; }; export const Message = ({ className, from, ...props }: MessageProps) => ( <div className={cn( "group flex w-full max-w-[95%] flex-col gap-2", from === "user" ? "is-user ml-auto justify-end" : "is-assistant", className, )} {...props} /> ); export type MessageContentProps = HTMLAttributes<HTMLDivElement>; export const MessageContent = ({ children, className, ...props }: MessageContentProps) => ( <div className={cn( "is-user:dark flex w-fit min-w-0 max-w-full flex-col gap-2 overflow-hidden text-sm", "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", "group-[.is-assistant]:w-full group-[.is-assistant]:text-foreground", "group-data-[optimistic=true]:opacity-70", className, )} {...props} > {children} </div> ); export type MessageActionsProps = ComponentProps<"div">; export const MessageActions = ({ className, children, ...props }: MessageActionsProps) => ( <div className={cn("flex items-center gap-1", className)} {...props}> {children} </div> ); export type MessageActionProps = ComponentProps<typeof Button> & { tooltip?: string; label?: string; }; export const MessageAction = ({ tooltip, children, label, variant = "ghost", size = "icon-sm", ...props }: MessageActionProps) => { const button = ( <Button size={size} type="button" variant={variant} {...props}> {children} <span className="sr-only">{label || tooltip}</span> </Button> ); if (tooltip) { return ( <TooltipProvider> <Tooltip> <TooltipTrigger asChild>{button}</TooltipTrigger> <TooltipContent> <p>{tooltip}</p> </TooltipContent> </Tooltip> </TooltipProvider> ); } return button; }; interface MessageBranchContextType { currentBranch: number; totalBranches: number; goToPrevious: () => void; goToNext: () => void; branches: ReactElement[]; setBranches: (branches: ReactElement[]) => void; } const MessageBranchContext = createContext<MessageBranchContextType | null>(null); const useMessageBranch = () => { const context = useContext(MessageBranchContext); if (!context) { throw new Error("MessageBranch components must be used within MessageBranch"); } return context; }; export type MessageBranchProps = HTMLAttributes<HTMLDivElement> & { defaultBranch?: number; onBranchChange?: (branchIndex: number) => void; }; export const MessageBranch = ({ defaultBranch = 0, onBranchChange, className, ...props }: MessageBranchProps) => { const [currentBranch, setCurrentBranch] = useState(defaultBranch); const [branches, setBranches] = useState<ReactElement[]>([]); const handleBranchChange = useCallback(