/**
* 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 && (
{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 */}
{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;
})}
);
})}
{/* Right: Analysis panels */}
Perspectives
{agentState ? (
) : (
Connecting...
)}
);
}
export default function App() {
return (
Loading...
}
>
);
}