import { Suspense, useCallback, useEffect, useRef, useState, type ReactNode } from "react"; import { useAgent } from "agents/react"; import { useAgentChat } from "@cloudflare/ai-chat/react"; import { isToolUIPart, getToolName } from "ai"; import { Button, Surface, Text, InputArea, Empty, Badge } from "@cloudflare/kumo"; import { PaperPlaneRightIcon, TrashIcon, GearIcon, ChatCircleDotsIcon, GlobeIcon, CaretDownIcon, BrainIcon } from "@phosphor-icons/react"; import { Streamdown } from "streamdown"; import { DemoWrapper } from "../../layout"; import { ConnectionStatus, CodeExplanation, type CodeSection } from "../../components"; import { useUserId } from "../../hooks"; const codeSections: CodeSection[] = [ { title: "Create an AI chat agent", description: "Extend AIChatAgent to get built-in message history, streaming, and tool support. Override onChatMessage to handle incoming messages with any AI provider.", code: `import { AIChatAgent } from "@cloudflare/ai-chat"; class ChatAgent extends AIChatAgent { async onChatMessage(onFinish) { const result = streamText({ model: workersai("@cf/moonshotai/kimi-k2.5"), messages: this.messages, onFinish, }); return result.toDataStreamResponse(); } }` }, { title: "Connect with useAgentChat", description: "The useAgentChat hook gives you a complete chat interface — messages array, input handling, submit function, and streaming status. It manages the full lifecycle over WebSocket.", code: `import { useAgent } from "agents/react"; import { useAgentChat } from "@cloudflare/ai-chat/react"; const agent = useAgent({ agent: "chat-agent", name: "my-chat" }); const { messages, input, setInput, handleSubmit, isLoading } = useAgentChat(agent, { onError: (err) => console.error(err), });` } ]; function ReasoningTrace({ text, state }: { text: string; state?: "streaming" | "done"; }) { const [expanded, setExpanded] = useState(true); return (
{expanded && (
{text}
)}
); } function MessageBubble({ align, variant, children }: { align: "left" | "right"; variant: "user" | "assistant"; children: ReactNode; }) { const base = "max-w-[80%] rounded-2xl overflow-hidden"; const userStyle = `${base} rounded-br-md bg-kumo-contrast text-kumo-inverse`; const assistantStyle = `${base} rounded-bl-md ring ring-kumo-line`; return (
{variant === "user" ? (
{children}
) : ( {children} )}
); } function ChatUI() { const userId = useUserId(); const [connectionStatus, setConnectionStatus] = useState< "connected" | "connecting" | "disconnected" >("connecting"); const [input, setInput] = useState(""); const messagesContainerRef = useRef(null); const agent = useAgent({ agent: "ChatAgent", name: `chat-demo-${userId}`, onOpen: useCallback(() => setConnectionStatus("connected"), []), onClose: useCallback(() => setConnectionStatus("disconnected"), []), onError: useCallback(() => setConnectionStatus("disconnected"), []) }); const { messages, sendMessage, clearHistory, status } = useAgentChat({ agent, onToolCall: async ({ toolCall, addToolOutput }) => { if (toolCall.toolName === "getUserTimezone") { addToolOutput({ toolCallId: toolCall.toolCallId, output: { timezone: Intl.DateTimeFormat().resolvedOptions().timeZone, localTime: new Date().toLocaleTimeString() } }); } } }); const isStreaming = status === "streaming"; const isConnected = connectionStatus === "connected"; useEffect(() => { const el = messagesContainerRef.current; if (el) el.scrollTop = el.scrollHeight; }, [messages]); const send = useCallback(() => { const text = input.trim(); if (!text || isStreaming) return; setInput(""); sendMessage({ role: "user", parts: [{ type: "text", text }] }); }, [input, isStreaming, sendMessage]); return ( Extend{" "} AIChatAgent {" "} to get a full chat backend with built-in message history, streaming, and tool support. On the client,{" "} useAgentChat {" "} gives you messages, input handling, and streaming status out of the box. Messages persist in the agent's Durable Object, so they survive page refreshes and reconnections. Try asking about the weather. } statusIndicator={} >
{/* Messages area */}
{messages.length === 0 && ( } title="Start a conversation" description='Try "What is the weather in London?" or "What timezone am I in?"' /> )} {messages.map((message, index) => { const isUser = message.role === "user"; const isLastAssistant = message.role === "assistant" && index === messages.length - 1; return (
{message.parts.map((part, partIdx) => { if (part.type === "text") { if (!part.text || part.text.trim() === "") return null; if (isUser) { return ( {part.text} ); } return ( {part.text} ); } if (part.type === "reasoning") { if (!part.text || part.text.trim() === "") return null; return ( ); } if (isToolUIPart(part)) { const toolName = getToolName(part); if (part.state === "output-available") { return (
{toolName === "getUserTimezone" ? ( ) : ( )} {toolName} Done
                              {JSON.stringify(part.output, null, 2)}
                            
); } if ( part.state === "input-available" || part.state === "input-streaming" ) { return (
Running {toolName}...
); } } return null; })}
); })}
{/* Input area */}
{ e.preventDefault(); send(); }} >
{ if (e.key === "Enter" && !e.shiftKey) { e.preventDefault(); send(); } }} placeholder={ isConnected ? "Ask me anything..." : "Connecting to agent..." } disabled={!isConnected || isStreaming} rows={2} className="flex-1 ring-0! focus:ring-0! shadow-none! bg-transparent! outline-none!" />
); } export function ChatDemo() { return (
Loading chat...
} >
); }