import { Suspense, useCallback, useState, useEffect, useRef } from "react"; import { useAgent } from "agents/react"; import { useAgentChat } from "@cloudflare/ai-chat/react"; import { isToolUIPart, getToolName } from "ai"; import type { UIMessage } from "ai"; import { Button, Badge, InputArea, Empty, Surface, Text } from "@cloudflare/kumo"; import { ConnectionIndicator, ModeToggle, PoweredByAgents, type ConnectionStatus } from "@cloudflare/agents-ui"; import { PaperPlaneRightIcon, TrashIcon, CheckCircleIcon, XCircleIcon, GearIcon, InfinityIcon } from "@phosphor-icons/react"; function getMessageText(message: UIMessage): string { return message.parts .filter((part) => part.type === "text") .map((part) => (part as { type: "text"; text: string }).text) .join(""); } function Chat() { const [connectionStatus, setConnectionStatus] = useState("connecting"); const [input, setInput] = useState(""); const messagesEndRef = useRef(null); const agent = useAgent({ agent: "ForeverChatAgent", onOpen: useCallback(() => setConnectionStatus("connected"), []), onClose: useCallback(() => setConnectionStatus("disconnected"), []), onError: useCallback( (error: Event) => console.error("WebSocket error:", error), [] ) }); const { messages, sendMessage, clearHistory, addToolApprovalResponse, status } = useAgentChat({ agent, body: { clientVersion: "1.0.0" }, 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(() => { messagesEndRef.current?.scrollIntoView({ behavior: "smooth" }); }, [messages]); const send = useCallback(() => { const text = input.trim(); if (!text || isStreaming) return; setInput(""); sendMessage({ role: "user", parts: [{ type: "text", text }] }); }, [input, isStreaming, sendMessage]); return (
{/* Header */}

Forever Chat

Durable Streaming
{/* Messages */}
{messages.length === 0 && ( } title="Durable AI Chat" description="This chat uses keepAlive — the DO stays alive during streaming, preventing idle eviction during long LLM responses." /> )} {messages.map((message, index) => { const isUser = message.role === "user"; const isLastAssistant = message.role === "assistant" && index === messages.length - 1; return (
{isUser ? (
{getMessageText(message)}
) : (
{getMessageText(message)} {isLastAssistant && isStreaming && ( )}
)} {message.parts .filter((part) => isToolUIPart(part)) .map((part) => { if (!isToolUIPart(part)) return null; const toolName = getToolName(part); if (part.state === "output-available") { return (
{toolName} Done
{JSON.stringify(part.output, null, 2)}
); } if ( "approval" in part && part.state === "approval-requested" ) { const approvalId = (part.approval as { id?: string })?.id; return (
Approval needed: {toolName}
{JSON.stringify(part.input, null, 2)}
); } if ( part.state === "input-available" || part.state === "input-streaming" ) { return (
Running {toolName}...
); } return null; })}
); })}
{/* Input */}
{ e.preventDefault(); send(); }} className="mx-auto max-w-3xl px-5 py-4" >
{ if (e.key === "Enter" && !e.shiftKey) { e.preventDefault(); send(); } }} placeholder="Try: What's the weather in Paris?" disabled={!isConnected || isStreaming} rows={2} className="flex-1 !bg-transparent !shadow-none !outline-none !ring-0 focus:!ring-0" />
); } export default function App() { return ( Loading...
} > ); }