import { Suspense, useCallback, useState, useEffect, useRef, useMemo } from "react"; import { useAgent } from "agents/react"; import { useAgentChat, type AITool, type OnToolCallCallback } 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, GearIcon, PlugIcon, InfoIcon, ToggleLeftIcon, ToggleRightIcon } from "@phosphor-icons/react"; /** * Available tools that a "third-party developer" could register. * In a real SDK, these would be passed as props to a chat widget. */ const AVAILABLE_TOOLS: Record< string, { tool: AITool; label: string; description: string } > = { getPageTitle: { label: "getPageTitle", description: "Returns the current page title from the browser", tool: { description: "Get the current page title from the user's browser", parameters: { type: "object", properties: {}, required: [] }, execute: async () => ({ title: document.title }) } }, getCurrentTime: { label: "getCurrentTime", description: "Returns the user's local time and timezone", tool: { description: "Get the user's current local time and timezone", parameters: { type: "object", properties: {}, required: [] }, execute: async () => ({ time: new Date().toLocaleTimeString(), timezone: Intl.DateTimeFormat().resolvedOptions().timeZone }) } }, getScreenInfo: { label: "getScreenInfo", description: "Returns screen dimensions and pixel ratio", tool: { description: "Get the user's screen dimensions and device pixel ratio", parameters: { type: "object", properties: {}, required: [] }, execute: async () => ({ width: window.innerWidth, height: window.innerHeight, pixelRatio: window.devicePixelRatio }) } }, getColorScheme: { label: "getColorScheme", description: "Returns the user's preferred color scheme", tool: { description: "Get whether the user prefers light or dark mode", parameters: { type: "object", properties: {}, required: [] }, execute: async () => ({ scheme: window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light", current: document.documentElement.getAttribute("data-mode") || "unknown" }) } } }; 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); // Track which tools are enabled — simulates an SDK user toggling tools const [enabledTools, setEnabledTools] = useState>( new Set(Object.keys(AVAILABLE_TOOLS)) ); const toggleTool = useCallback((name: string) => { setEnabledTools((prev) => { const next = new Set(prev); if (next.has(name)) { next.delete(name); } else { next.add(name); } return next; }); }, []); // Build the active tools record from enabled set const activeTools = useMemo(() => { const tools: Record = {}; for (const name of enabledTools) { const entry = AVAILABLE_TOOLS[name]; if (entry) { tools[name] = entry.tool; } } return Object.keys(tools).length > 0 ? tools : undefined; }, [enabledTools]); const agent = useAgent({ agent: "DynamicToolsAgent", onOpen: useCallback(() => setConnectionStatus("connected"), []), onClose: useCallback(() => setConnectionStatus("disconnected"), []), onError: useCallback( (error: Event) => console.error("WebSocket error:", error), [] ) }); const { messages, sendMessage, clearHistory, status } = useAgentChat({ agent, // Dynamic tools — schemas are sent to the server automatically tools: activeTools, // Execute tool calls routed back from the server onToolCall: useCallback( async ({ toolCall, addToolOutput }) => { const tool = activeTools?.[toolCall.toolName]; if (tool?.execute) { const output = await tool.execute(toolCall.input); addToolOutput({ toolCallId: toolCall.toolCallId, output }); } }, [activeTools] ) }); 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 */}

Dynamic Tools

SDK Pattern
{/* Tool sidebar */} {/* Chat area */}
{messages.length === 0 && ( } title="Dynamic tools are ready" description='Toggle tools in the sidebar, then ask something like "What page am I on?", "What time is it?", or "What is my screen size?"' /> )} {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 && ( )}
)} {/* Tool parts */} {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 ( part.state === "input-available" || part.state === "input-streaming" ) { return (
Running {toolName}...
); } return null; })}
); })}
{/* Input */}
{ e.preventDefault(); send(); }} className="max-w-3xl mx-auto px-5 py-4" >
{ if (e.key === "Enter" && !e.shiftKey) { e.preventDefault(); send(); } }} placeholder={ enabledTools.size > 0 ? 'Try "What page am I on?" or "What time is it?"' : "No tools enabled — toggle some in the sidebar" } disabled={!isConnected || isStreaming} rows={2} className="flex-1 !ring-0 focus:!ring-0 !shadow-none !bg-transparent !outline-none" />
); } export default function App() { return ( Loading...
} > ); }