/** * Sub-Agents Example — Client * * Chat on the left. Right panel shows the most recent analysis round: * three perspective cards (Technical, Business, Skeptic) and the synthesis. * Each card updates as its facet completes, showing parallel execution. */ import { Suspense, useCallback, useState, useEffect, useRef } from "react"; import { useAgent } from "agents/react"; import { useAgentChat } from "@cloudflare/ai-chat/react"; import { isToolUIPart } 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, UsersThreeIcon, LightbulbIcon, ChartBarIcon, WarningCircleIcon, ArrowsInIcon } from "@phosphor-icons/react"; import { Streamdown } from "streamdown"; import type { SubagentState, AnalysisRound, PerspectiveId // PERSPECTIVES } from "./server"; function getMessageText(message: UIMessage): string { return message.parts .filter((part) => part.type === "text") .map((part) => (part as { type: "text"; text: string }).text) .join(""); } // ─── Perspective Card ────────────────────────────────────────────────────── const perspectiveConfig: Record< PerspectiveId, { icon: React.ReactNode; color: string } > = { technical: { icon: , color: "text-kumo-brand" }, business: { icon: , color: "text-kumo-positive" }, skeptic: { icon: , color: "text-kumo-warning" } }; function PerspectiveCard({ perspectiveId, name, analysis }: { perspectiveId: PerspectiveId; name: string; analysis: string | null; }) { const config = perspectiveConfig[perspectiveId] ?? perspectiveConfig.technical; return (
{config.icon} {name} {analysis ? ( Done ) : ( Thinking... )}
{analysis ? ( {analysis} ) : (
Analyzing...
)}
); } // ─── Analysis Panel ──────────────────────────────────────────────────────── function AnalysisPanel({ analyses }: { analyses: AnalysisRound[] }) { if (analyses.length === 0) { return ( } title="No analyses yet" description='Ask a question — e.g. "Should we rewrite our backend in Rust?"' /> ); } const latest = analyses[0]; return (
{/* Question */}
Question {latest.question}
{/* Three perspective cards */} {(["technical", "business", "skeptic"] as PerspectiveId[]).map((pid) => { const result = latest.perspectives.find((p) => p.perspectiveId === pid); const names: Record = { technical: "Technical Expert", business: "Business Analyst", skeptic: "Devil's Advocate" }; return ( ); })} {/* Synthesis */} {latest.synthesis && (
Synthesis Combined
{latest.synthesis}
)} {/* History */} {analyses.length > 1 && (
Previous ({analyses.length - 1})
{analyses.slice(1).map((round) => ( {round.question} {round.perspectives.length} perspectives {round.synthesis ? " + synthesis" : ""} ))}
)}
); } // ─── Main ────────────────────────────────────────────────────────────────── function ChatPanel() { const [connectionStatus, setConnectionStatus] = useState("connecting"); const [input, setInput] = useState(""); const [agentState, setAgentState] = useState(null); const messagesEndRef = useRef(null); const agent = useAgent({ agent: "CoordinatorAgent", onOpen: useCallback(() => setConnectionStatus("connected"), []), onClose: useCallback(() => setConnectionStatus("disconnected"), []), onError: useCallback( (error: Event) => console.error("WebSocket error:", error), [] ), onStateUpdate: useCallback( (state: SubagentState) => setAgentState(state), [] ) }); const { messages, sendMessage, clearHistory, status } = useAgentChat({ agent }); 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 (
{/* Left: Chat */}
Sub-Agents Multi-Perspective
{messages.length === 0 && ( } title="Ask a question for multi-perspective analysis" description={`Try: "Should we migrate to microservices?" or "Is AI going to replace software engineers?"`} /> )} {messages.map((message, index) => { const isUser = message.role === "user"; const isLastAssistant = message.role === "assistant" && index === messages.length - 1; return (
{isUser ? (
{getMessageText(message)}
) : ( getMessageText(message) && (
{getMessageText(message)}
) )} {message.parts .filter((part) => isToolUIPart(part)) .map((part) => { if (!isToolUIPart(part)) return null; if (part.state === "output-available") { return (
3 perspectives analyzed + synthesis complete
); } if ( part.state === "input-available" || part.state === "input-streaming" ) { return (
Analyzing from 3 perspectives...
); } return null; })}
); })}
{ e.preventDefault(); send(); }} className="max-w-2xl mx-auto px-5 py-4" >
{ if (e.key === "Enter" && !e.shiftKey) { e.preventDefault(); send(); } }} placeholder='Try: "Should we build or buy our auth system?"' disabled={!isConnected || isStreaming} rows={2} className="flex-1 ring-0! focus:ring-0! shadow-none! bg-transparent! outline-none!" />
{/* Right: Analysis panels */}
Perspectives
{agentState ? ( ) : (
Connecting...
)}
); } export default function App() { return ( Loading...
} > ); }