"use client" import * as React from "react" import { useChat } from "@ai-sdk/react" import { isToolUIPart, lastAssistantMessageIsCompleteWithApprovalResponses, type UIDataTypes, type UIMessage, } from "ai" import { createChat } from "@/lib/ai" import { Example, ExampleWrapper, } from "@/registry/bases/base/components/example" import { Badge } from "@/registry/bases/base/ui/badge" import { Bubble, BubbleContent } from "@/registry/bases/base/ui/bubble" import { Button } from "@/registry/bases/base/ui/button" import { CodeBlock, CodeBlockContent, CodeBlockHeader, CodeBlockTitle, } from "@/registry/bases/base/ui/code-block" import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, DialogTrigger, } from "@/registry/bases/base/ui/dialog" import { Item, ItemActions, ItemContent, ItemDescription, ItemMedia, ItemTitle, } from "@/registry/bases/base/ui/item" import { Message, MessageContent } from "@/registry/bases/base/ui/message" import { Sheet, SheetContent, SheetDescription, SheetHeader, SheetTitle, SheetTrigger, } from "@/registry/bases/base/ui/sheet" import { Trace, TraceCollapsible, TraceCollapsibleContent, TraceCollapsibleTrigger, TraceIcon, TraceLine, TraceTitle, } from "@/registry/bases/base/ui/trace" import { IconPlaceholder } from "@/app/(create)/components/icon-placeholder" const REASONING = `The user wants to know why the checkout deploy is slow, so I should start from the build log instead of guessing. The install and compile steps finish in under a minute. Image optimization takes almost four, and it runs on every deploy because the cache key includes the commit hash. Keying the cache on the lockfile and the image directory should let most deploys skip that step entirely.` export default function TraceExample() { return ( ) } type ChatbotMessage = UIMessage< unknown, UIDataTypes, { getWeather: { input: { city: string } output: { location: string; temperature: number; condition: string } } searchWikipedia: { input: { query: string } output: { results: { title: string; url: string }[] } } fetchPage: { input: { url: string } output: { status: number; title: string; excerpt: string } } readFile: { input: { path: string } output: { path: string; content: string } } runCommand: { input: { command: string } output: { stdout: string } } } > type ChatbotPart = ChatbotMessage["parts"][number] type ReasoningPart = Extract type ToolPart = Extract type ApprovalResponse = { id: string; approved: boolean } const README = `# Acme Chat A chat app built with Next.js and the AI SDK. ## Getting started pnpm install pnpm dev ## Chat route app/api/chat/route.ts streams responses through the AI Gateway. Set AI_GATEWAY_API_KEY in .env.local.` const chatbot = createChat() .user("What's the weather in Lisbon and Tokyo?") .sleep(800) .assistant(({ writer }) => { writer.reasoning( "The user wants the weather in two cities. I'll look both up at once, then compare them in a sentence." ) writer .tool("getWeather", { input: { city: "Lisbon" } }) .sleep(1200) .output({ location: "Lisbon, Portugal", temperature: 18, condition: "Mainly clear", }) writer .tool("getWeather", { input: { city: "Tokyo" } }) .sleep(600) .output({ location: "Tokyo, Japan", temperature: 26, condition: "Clear sky", }) writer.text( "Lisbon is 18°C and mainly clear. Tokyo is warmer at 26°C under a clear sky." ) }) .user("Search Wikipedia for the Voyager probes and summarise with sources.") .sleep(800) .assistant(({ writer }) => { writer.reasoning( "Start with a Wikipedia search. The Voyager 1 article should say where the probe is now." ) writer .tool("searchWikipedia", { input: { query: "Voyager probes" } }) .sleep(1000) .output({ results: [ { title: "Voyager program", url: "https://en.wikipedia.org/wiki/Voyager_program", }, { title: "Voyager 1", url: "https://en.wikipedia.org/wiki/Voyager_1", }, { title: "Voyager 2", url: "https://en.wikipedia.org/wiki/Voyager_2", }, ], }) writer .tool("fetchPage", { input: { url: "https://en.wikipedia.org/wiki/Voyager_1" }, }) .sleep(900) .output({ status: 200, title: "Voyager 1 - Wikipedia", excerpt: "Voyager 1 is a space probe launched by NASA on September 5, 1977. It is the most distant human-made object from Earth.", }) writer.text( "NASA launched both Voyager probes in 1977 to study the outer planets. Both have since reached interstellar space, and Voyager 1 is the most distant human-made object from Earth." ) writer.sourceUrl({ url: "https://en.wikipedia.org/wiki/Voyager_program", title: "Voyager program", }) writer.sourceUrl({ url: "https://en.wikipedia.org/wiki/Voyager_1", title: "Voyager 1", }) }) .user("Read the README and the changelog, then check the Node version.") .sleep(800) .assistant(({ writer }) => { writer.reasoning( "Read both files first. Running a command needs the user's approval." ) writer .tool("readFile", { input: { path: "README.md" } }) .sleep(700) .output({ path: "README.md", content: README }) writer .tool("readFile", { input: { path: "CHANGELOG.md" } }) .sleep(500) .error("ENOENT: no such file or directory, open 'CHANGELOG.md'") writer.tool("runCommand", { input: { command: "node --version" }, needsApproval: true, output: { stdout: "v24.18.0" }, }) }) .assistant(({ writer, toolCall }) => { writer.text( toolCall?.approved ? "The README covers setup and the chat route. There is no changelog. The project runs on Node v24.18.0." : "The README covers setup and the chat route. There is no changelog. I didn't run the command." ) }) const chatbotTransport = chatbot.transport({ delayMs: 20 }) function TraceChat() { const { messages, sendMessage, setMessages, status, addToolApprovalResponse, } = useChat({ messages: chatbot.get(0), transport: chatbotTransport, sendAutomaticallyWhen: lastAssistantMessageIsCompleteWithApprovalResponses, }) const nextMessage = chatbot.next(messages) const isBusy = status === "submitted" || status === "streaming" const lastMessage = messages.at(-1) function handleClick() { if (nextMessage) { void sendMessage(nextMessage) return } setMessages(chatbot.get(0)) } return (
{messages.map((message) => ( void addToolApprovalResponse(response) } /> ))} {status === "submitted" && ( } title="Thinking" /> )}
) } function ChatbotMessageItem({ message, isStreaming, onApprovalResponse, }: { message: ChatbotMessage isStreaming: boolean onApprovalResponse: (response: ApprovalResponse) => void }) { if (message.role === "user") { return ( {message.parts .map((part) => (part.type === "text" ? part.text : "")) .join("")} ) } const { runs, sources } = groupParts(message.parts) return ( {runs.map((run) => { if (run.type === "text") { return (

{run.text}

) } if (run.type === "approval") { return ( ) } return ( {run.parts.map((part, index) => part.type === "reasoning" ? ( ) : ( ) )} ) })} {!isStreaming && sources.length > 0 && ( } title={`Used ${sources.length} sources`} > {sources.map((source) => ( ))} )}
) } // Each run of reasoning and tool parts becomes one group. A text part or a // pending approval ends the run. Sources are shown once at the end. function groupParts(parts: ChatbotPart[]) { const runs: ( | { type: "text"; key: string; text: string } | { type: "approval"; key: string; part: ToolPart } | { type: "activity"; key: string; parts: (ReasoningPart | ToolPart)[] } )[] = [] const sources: Extract[] = [] parts.forEach((part, index) => { const last = runs.at(-1) if (part.type === "source-url") { if (!sources.some((source) => source.url === part.url)) { sources.push(part) } return } if (part.type === "text") { if (last?.type === "text") { last.text += part.text } else if (part.text.trim()) { runs.push({ type: "text", key: `text-${index}`, text: part.text }) } return } if (isToolUIPart(part) && part.state === "approval-requested") { runs.push({ type: "approval", key: part.toolCallId, part }) return } if (part.type === "reasoning" || isToolUIPart(part)) { if (last?.type === "activity") { last.parts.push(part) } else { runs.push({ type: "activity", key: `activity-${index}`, parts: [part] }) } } }) return { runs, sources } } function ReasoningActivity({ part, isStreaming, }: { part: ReasoningPart isStreaming: boolean }) { const running = isStreaming && part.state === "streaming" const text = part.text.trim() const [open, setOpen] = React.useState() if (!text) { return ( } title={running ? "Thinking" : "Thought"} /> ) } return ( {running ? "Thinking" : "Thought"} {text} ) } function ToolActivity({ part, isStreaming, }: { part: ToolPart isStreaming: boolean }) { const props = { part, isStreaming } switch (part.type) { case "tool-getWeather": { const city = part.input?.city return ( } title={{ running: `Getting weather for ${city}`, done: `Got weather for ${city}`, failed: `Couldn't get weather for ${city}`, }} > {part.state === "output-available" && ( )} ) } case "tool-searchWikipedia": { const query = part.input?.query return ( } title={{ running: `Searching Wikipedia for “${query}”`, done: `Searched Wikipedia for “${query}”`, failed: `Couldn't search Wikipedia for “${query}”`, }} > {part.state === "output-available" && ( {part.output.results.map((result) => ( ))} )} ) } case "tool-fetchPage": { const url = part.input?.url?.replace(/^https?:\/\//, "") return ( } title={{ running: `Fetching ${url}`, done: `Fetched ${url}`, failed: `Couldn't fetch ${url}`, }} > {part.state === "output-available" && ( )} ) } case "tool-readFile": { if (part.state === "output-available") { return ( ) } const path = part.input?.path return ( } title={{ running: `Reading ${path}`, done: `Read ${path}`, failed: `Couldn't read ${path}`, }} /> ) } case "tool-runCommand": { const command = part.input?.command return ( } title={{ running: `Running ${command}`, done: `Ran ${command}`, failed: `Couldn't run ${command}`, denied: `Denied ${command}`, }} > {part.state === "output-available" && ( {part.output.stdout} )} ) } default: return null } } // Maps a tool part's state to a row. `children` is what the finished row // expands to show. function ToolRow({ part, isStreaming, icon, title, children, }: { part: ToolPart isStreaming: boolean icon: React.ReactNode title: { running: string done: string failed: string denied?: string } children?: React.ReactNode }) { const running = isStreaming ? "running" : undefined const denied = title.denied ?? `Denied: ${title.running}` switch (part.state) { case "input-streaming": case "input-available": return case "approval-requested": // A pending approval renders outside Trace, as ToolApproval. return null case "approval-responded": return part.approval.approved ? ( ) : ( ) case "output-denied": return case "output-error": return ( {part.errorText} ) case "output-available": return ( {children} ) } } // Static when there is nothing to show, expandable otherwise. function ActivityRow({ status, icon, title, children, }: { status?: "running" | "error" icon: React.ReactNode title: React.ReactNode children?: React.ReactNode }) { if (!children) { return ( {icon} {title} ) } return ( {icon} {title} {children} ) } // A pending approval is a prompt, not activity, so it is an Item. Once // answered, the tool part renders as a Trace row again. function ToolApproval({ part, onApprovalResponse, }: { part: ToolPart onApprovalResponse: (response: ApprovalResponse) => void }) { if (part.state !== "approval-requested" || part.type !== "tool-runCommand") { return null } const id = part.approval.id return ( Run {part.input.command}? The command runs in this project. ) } function LinkActivity({ href, title }: { href: string; title: string }) { return ( {title} (opens in a new tab) ) } function FileActivity({ path, content }: { path: string; content: string }) { return ( Read {path} {path} {content.split("\n").length} lines {content} ) } function ToolInputOutput({ input, output, }: { input: unknown output: string }) { return (
Input {JSON.stringify(input)} Output {output}
) } function TraceRows() { return ( Scanning 52 files Ran node --version v24.18.0 Read apps/v4/lib/docs.ts Explored 4 files Explored 4 files Files read while preparing the response.
  • apps/v4/lib/docs.ts
  • apps/v4/lib/components.ts
  • apps/v4/next.config.mjs
  • apps/v4/package.json
Searched the web for cache keys Searched the web for cache keys Three results were used in the response.
  • Caching build outputs
  • Image optimization reference
  • Deploy hooks and cache keys
) } function TraceToolCalls() { return ( Running npm --version $ npm --version Searched the web for app getting started installation example.com/docs/install example.com/blog/getting-started Searched for minimumReleaseAge Ran node --version {`$ node --version\nv24.18.0`} Failed to run pnpm --version command not found: pnpm ) } const chat = createChat() .user("Why is the checkout deploy so slow?") .assistant(({ writer }) => { writer.reasoning(REASONING, { delayMs: 60 }) writer.sleep(400) writer.text( "Image optimization is the bottleneck. It reruns on every deploy because the cache key includes the commit hash. Key it on the lockfile and the image directory instead." ) }) const initialMessages = chat.get(0) const transport = chat.transport({ delayMs: 20, }) function ThinkingTrace({ text, running }: { text: string; running: boolean }) { const [open, setOpen] = React.useState() return ( 0)} onOpenChange={setOpen} > {running ? "Thinking" : "Thought"} {text} ) } function TraceReasoning() { const { messages, sendMessage, setMessages, status } = useChat({ messages: initialMessages, transport, }) const nextMessage = chat.next(messages) const isBusy = status === "submitted" || status === "streaming" function handleClick() { if (nextMessage) { void sendMessage(nextMessage) return } setMessages(initialMessages) } return (
Thought for 12 seconds {REASONING}
{messages.map((message) => { const live = status === "streaming" && message === messages.at(-1) const runs: { trace: boolean; parts: typeof message.parts }[] = [] for (const part of message.parts) { const trace = part.type === "reasoning" if (!trace && part.type !== "text") { continue } const last = runs.at(-1) if (last?.trace === trace) { last.parts.push(part) } else { runs.push({ trace, parts: [part] }) } } return ( {runs.map((run, runIndex) => run.trace ? ( {run.parts.map((part, index) => part.type === "reasoning" ? ( ) : null )} ) : ( run.parts.map((part, index) => part.type === "text" ? (

{part.text}

) : null ) ) )}
) })}
) } function TraceChainOfThought() { return ( Looking into the slow deploy Searched for the checkout build log vercel.com github.com nextjs.org Read the last twelve deploys Image optimization takes almost four minutes on every deploy. The other steps finish in under a minute. Searching for the cache key cache key image optimization ) } function TraceTask() { return ( Found the project files Searched for app/page.tsx Read app/page.tsx Scanned 52 files Read app/layout.tsx ) } const SOURCES = [ { title: "Caching build outputs | example.com", href: "https://example.com/docs/caching", }, { title: "Image optimization reference | example.com", href: "https://example.com/docs/images", }, { title: "Deploy hooks and cache keys | example.com", href: "https://example.com/docs/deploy-hooks", }, ] function TraceSources() { return ( Used {SOURCES.length} sources {SOURCES.map((source) => ( {source.title} (opens in a new tab) ))} ) } function TraceTurn() { return ( Worked for 2m 4s, 1 command failed Thought for 12 seconds {REASONING} Ran 3 commands, 1 failed Ran node --version v24.18.0 Ran mkdir -p dist Failed to run pnpm --version command not found: pnpm

Node is installed and pnpm is not. Reading the docs config.

Read apps/v4/lib/docs.ts
) } function ChevronRightGlyph() { return ( ) } function ArrowUpRightGlyph() { return ( ) } function BrainGlyph() { return ( ) } function SearchGlyph() { return ( ) } function FileGlyph() { return ( ) } function FileSearchGlyph() { return ( ) } function TerminalGlyph() { return ( ) } function SunGlyph() { return ( ) } function BookGlyph() { return ( ) } function LinkGlyph() { return ( ) } function GlobeGlyph() { return ( ) }