"use client"; import * as React from "react"; import { UseAgentUpdate, useAgent, useCopilotKit, useFrontendTool, } from "@copilotkit/react-core/v2"; import { ArrowUpIcon, MessageCircleDashedIcon, PaperclipIcon, PlusIcon, RotateCwIcon, } from "lucide-react"; import { MessageAnimated, MessageAnimatedLoading, MessageAnimatedMessagesProvider, } from "@/components/message-animated"; import { LineChartCard, LineChartCardSkeleton, lineChartSchema, } from "@/components/generative-ui/line-chart"; import { MakeItRain } from "@/components/generative-ui/make-it-rain"; import { Attachment, AttachmentContent, AttachmentDescription, AttachmentMedia, AttachmentTitle, } from "@/components/ui/attachment"; import { Button } from "@/components/ui/button"; import { Card, CardAction, CardContent, CardDescription, CardFooter, CardHeader, CardTitle, } from "@/components/ui/card"; import { Empty, EmptyDescription, EmptyHeader, EmptyMedia, EmptyTitle, } from "@/components/ui/empty"; import { InputGroup, InputGroupAddon, InputGroupButton, } from "@/components/ui/input-group"; import { Marker, MarkerContent } from "@/components/ui/marker"; import { MessageScroller, MessageScrollerButton, MessageScrollerContent, MessageScrollerProvider, MessageScrollerViewport, } from "@/components/ui/message-scroller"; import { Tooltip, TooltipContent, TooltipTrigger, } from "@/components/ui/tooltip"; type AgentMessage = { id?: string; role?: string; content?: unknown; toolCallId?: string; toolCalls?: AgentToolCall[]; }; type AgentToolCall = { id: string; type: "function"; function: { name: string; arguments: string; }; }; const queuedMessages = [ "Explain to me briefly what ShadCN is and how I can use it.", "Render one simple line chart.", "Show a small human-in-the-loop taco rain picker.", ]; const BASE_CHAT_WIDTH = 384; const BASE_CARD_HEIGHT = 560; const BASE_CHAT_STACK_HEIGHT = 608; const VIEWPORT_MARGIN = 32; function messageText(content: unknown) { if (typeof content === "string") { return content; } if (Array.isArray(content)) { return content .map((part) => { if (typeof part === "string") { return part; } if ( part && typeof part === "object" && "text" in part && typeof part.text === "string" ) { return part.text; } return ""; }) .filter(Boolean) .join("\n"); } return content ? JSON.stringify(content, null, 2) : ""; } function messageRole(message: AgentMessage): "user" | "assistant" | "system" { if (message.role === "user") { return "user"; } if (message.role === "system") { return "system"; } return "assistant"; } function hasToolCalls(message: AgentMessage) { return Array.isArray(message.toolCalls) && message.toolCalls.length > 0; } function isVisibleMessage(message: AgentMessage) { if (message.role === "tool") { return false; } return ( messageText(message.content).trim().length > 0 || hasToolCalls(message) ); } function isWaitingForAssistant(messages: AgentMessage[]) { const lastVisibleMessage = messages.at(-1); return Boolean( lastVisibleMessage && messageRole(lastVisibleMessage) === "user", ); } function calculateChatScale() { if (typeof window === "undefined") { return 1; } const targetHalfViewport = (window.innerHeight * 0.5) / BASE_CARD_HEIGHT; const fitWidth = (window.innerWidth - VIEWPORT_MARGIN) / BASE_CHAT_WIDTH; const fitHeight = (window.innerHeight - VIEWPORT_MARGIN) / BASE_CHAT_STACK_HEIGHT; return Math.max( 0.72, Math.min(Math.max(1, targetHalfViewport), fitWidth, fitHeight), ); } function useResponsiveChatScale() { const [scale, setScale] = React.useState(1); React.useEffect(() => { function updateScale() { setScale(calculateChatScale()); } updateScale(); window.addEventListener("resize", updateScale); window.visualViewport?.addEventListener("resize", updateScale); return () => { window.removeEventListener("resize", updateScale); window.visualViewport?.removeEventListener("resize", updateScale); }; }, []); return scale; } export function SimpleChat() { return ; } function ChatPanel() { const [error, setError] = React.useState(null); const [uploadedFile, setUploadedFile] = React.useState(null); const fileInputRef = React.useRef(null); const chatScale = useResponsiveChatScale(); const { copilotkit } = useCopilotKit(); const { agent } = useAgent({ agentId: "default", updates: [ UseAgentUpdate.OnMessagesChanged, UseAgentUpdate.OnRunStatusChanged, ], throttleMs: 50, }); useFrontendTool( { name: "renderLineChart", agentId: "default", parameters: lineChartSchema, handler: async () => "Line chart rendered.", render: ({ args, status }) => status === "complete" ? ( ) : ( ), followUp: false, description: "Render exactly one compact line chart. Use 2 to 12 ordered finite numeric points and short labels.", }, [], ); const messages = (agent.messages ?? []) as AgentMessage[]; const visibleMessages = messages.filter(isVisibleMessage); const isRunning = Boolean(agent.isRunning); const showAssistantLoading = isRunning && isWaitingForAssistant(visibleMessages); const nextMessage = queuedMessages[ messages.filter((message) => messageRole(message) === "user").length ] ?? null; async function sendMessage() { if (!nextMessage || isRunning) { return; } setError(null); try { agent.addMessage({ id: crypto.randomUUID(), role: "user", content: nextMessage, } as never); await copilotkit.runAgent({ agent }); } catch (caughtError) { setError( caughtError instanceof Error ? caughtError.message : "The assistant could not be reached.", ); } } function resetConversation() { if (isRunning) { agent.abortRun(); } agent.setMessages([]); setUploadedFile(null); if (fileInputRef.current) { fileInputRef.current.value = ""; } setError(null); } function handleFileUpload(event: React.ChangeEvent) { setUploadedFile(event.currentTarget.files?.[0] ?? null); } return (
New Chat How can I help you today?

Reset

{visibleMessages.length === 0 ? ( Ready when you are Press send to run the first example. ) : ( {visibleMessages.map((message, index) => ( ))} {showAssistantLoading ? ( ) : null} )} {uploadedFile ? ( {uploadedFile.name} {formatFileSize(uploadedFile.size)} ) : null}
{ event.preventDefault(); void sendMessage(); }} className="w-full" >
{nextMessage ? ( nextMessage ) : ( All examples complete. Reset to replay. )}
fileInputRef.current?.click()} > Send
{error ? ( {error} ) : null}
{nextMessage ? "Press send to run the next example." : "Reset to replay the examples."}
); } function formatFileSize(bytes: number) { if (bytes < 1024) { return `${bytes} B`; } const units = ["KB", "MB", "GB"] as const; let size = bytes / 1024; let unitIndex = 0; while (size >= 1024 && unitIndex < units.length - 1) { size /= 1024; unitIndex += 1; } return `${size.toFixed(size >= 10 ? 0 : 1)} ${units[unitIndex]}`; }