import { useState, useEffect, useRef, useCallback } from "react"; import { Button, Badge, InputArea, Empty } from "@cloudflare/kumo"; import { ConnectionIndicator, ModeToggle, PoweredByAgents, type ConnectionStatus } from "@cloudflare/agents-ui"; import { PaperPlaneRightIcon, TrashIcon, ArrowsClockwiseIcon, ChatCircleDotsIcon, StackIcon } from "@phosphor-icons/react"; import { useAgent } from "agents/react"; import type { ChatAgent } from "./server"; import type { UIMessage } from "ai"; function getMessageText(message: UIMessage): string { return message.parts .filter((p): p is { type: "text"; text: string } => p.type === "text") .map((p) => p.text) .join("\n"); } function Chat() { const [connectionStatus, setConnectionStatus] = useState("connecting"); const [input, setInput] = useState(""); const [messages, setMessages] = useState([]); const [isLoading, setIsLoading] = useState(false); const [isCompacting, setIsCompacting] = useState(false); const messagesEndRef = useRef(null); const hasFetched = useRef(false); const agent = useAgent({ agent: "ChatAgent", name: "default", onOpen: useCallback(() => setConnectionStatus("connected"), []), onClose: useCallback(() => { setConnectionStatus("disconnected"); hasFetched.current = false; }, []) }); // Fetch messages once on connect useEffect(() => { if (connectionStatus !== "connected" || hasFetched.current) return; hasFetched.current = true; const load = async () => { try { await agent.ready; const msgs = await agent.call("getMessages"); setMessages(msgs); } catch (err) { console.error("Failed to fetch messages:", err); } }; load(); }, [connectionStatus, agent]); useEffect(() => { messagesEndRef.current?.scrollIntoView({ behavior: "smooth" }); }, [messages]); const send = useCallback(async () => { const text = input.trim(); if (!text || isLoading) return; setInput(""); setIsLoading(true); const userMsg: UIMessage = { id: `user-${crypto.randomUUID()}`, role: "user", parts: [{ type: "text", text }] }; setMessages((prev) => [...prev, userMsg]); try { const response = await agent.call("chat", [text, userMsg.id]); const assistantMsg: UIMessage = { id: `assistant-${crypto.randomUUID()}`, role: "assistant", parts: [{ type: "text", text: response }] }; setMessages((prev) => [...prev, assistantMsg]); } catch (err) { console.error("Failed to send:", err); } finally { setIsLoading(false); } }, [input, isLoading, agent]); const clearHistory = async () => { try { await agent.call("clearMessages"); setMessages([]); } catch (err) { console.error("Failed to clear:", err); } }; const compactSession = async () => { setIsCompacting(true); try { const result = await agent.call<{ success: boolean; error?: string }>( "compact" ); if (result.success) { const msgs = await agent.call("getMessages"); setMessages(msgs); } else { alert(`Compaction failed: ${result.error}`); } } catch (err) { console.error("Failed to compact:", err); } finally { setIsCompacting(false); } }; const isConnected = connectionStatus === "connected"; return (

Session Memory

Compaction
{messages.length === 0 && !isLoading && ( } title="Start a conversation" description="Messages persist in SQLite. Try compacting after a few exchanges." /> )} {messages.map((message) => { const text = getMessageText(message); if (!text) return null; if (message.role === "user") { return (
{text}
); } return (
{text}
); })} {isLoading && (
)}
{ e.preventDefault(); send(); }} className="max-w-3xl mx-auto px-5 py-4" >
{ if (e.key === "Enter" && !e.shiftKey) { e.preventDefault(); send(); } }} placeholder="Type a message..." disabled={!isConnected || isLoading} rows={2} className="flex-1 !ring-0 focus:!ring-0 !shadow-none !bg-transparent !outline-none" />
); } export default function App() { return ; }