import { useCallback, useEffect, useRef, useState } from "react"; import { useAgent } from "agents/react"; import { createRoot } from "react-dom/client"; import type { AgentInputItem, AgentState, StreamChunk } from "./server"; /** flattened messges for display */ type DisplayMessage = | { role: "user"; content: string } | { role: "assistant"; content: string } | { role: "tool-call"; toolCallId: string; toolName: string; input: unknown } | { role: "tool-result"; toolCallId: string; toolName: string; output: unknown; }; /** AgentInputItem into renderable message */ function toDisplay(item: AgentInputItem): DisplayMessage | null { // User message if ("role" in item && item.role === "user") { const content = typeof item.content === "string" ? item.content : Array.isArray(item.content) ? item.content .filter((c) => c.type === "input_text") .map((c) => ("text" in c ? c.text : "")) .join("") : ""; return { role: "user", content }; } // Assistant message if ("role" in item && item.role === "assistant") { const text = item.content .filter((c) => c.type === "output_text") .map((c) => ("text" in c ? c.text : "")) .join(""); return { role: "assistant", content: text }; } // Tool call if ("type" in item && item.type === "function_call") { return { role: "tool-call", toolCallId: item.callId, toolName: item.name, input: JSON.parse(item.arguments) }; } // tool call result if ("type" in item && item.type === "function_call_result") { return { role: "tool-result", toolCallId: item.callId, toolName: item.name, output: item.output }; } return null; } function parseToolOutput(output: unknown): Record | string { if (typeof output === "string") { try { return parseToolOutput(JSON.parse(output)); } catch { return output; } } if (typeof output === "object" && output !== null) { const obj = output as Record; if (obj.type === "text" && typeof obj.text === "string") { return parseToolOutput(obj.text); } const result: Record = {}; for (const [k, v] of Object.entries(obj)) { result[k] = typeof v === "object" ? JSON.stringify(v) : String(v); } return result; } return String(output); } /** snake_case or kebab-case into Title Case */ function formatKey(key: string): string { return key.replace(/[_-]/g, " ").replace(/\b\w/g, (c) => c.toUpperCase()); } /** Extract tool args as displayable key-value pairs */ function formatArgs(input: unknown): { key: string; value: string }[] { if (typeof input === "object" && input !== null) { return Object.entries(input as Record).map(([k, v]) => ({ key: formatKey(k), value: typeof v === "object" ? JSON.stringify(v) : String(v) })); } return []; } function Chat() { const [input, setInput] = useState(""); const [messages, setMessages] = useState([]); const [isStreaming, setIsStreaming] = useState(false); const messagesEndRef = useRef(null); const agent = useAgent({ agent: "my-agent", onStateUpdate(state: AgentState) { setMessages( state.messages .map(toDisplay) .filter((m): m is DisplayMessage => m !== null) ); } }); useEffect(() => { messagesEndRef.current?.scrollIntoView({ behavior: "smooth" }); }, [messages]); const send = useCallback(() => { const text = input.trim(); if (!text || isStreaming) return; setInput(""); setIsStreaming(true); // Optimistically add the user message and a placeholder assistant message setMessages((prev) => [ ...prev, { role: "user", content: text }, { role: "assistant", content: "" } ]); agent.call("chat", [text], { onChunk(chunk: unknown) { const c = chunk as StreamChunk; if (c.type === "text-delta") { // Append delta to the last assistant message setMessages((prev) => { const updated = [...prev]; const last = updated[updated.length - 1]; if (last?.role === "assistant") { updated[updated.length - 1] = { ...last, content: last.content + c.delta }; } return updated; }); } if (c.type === "tool-call") { // Insert tool-call before the trailing assistant placeholder setMessages((prev) => { const updated = [...prev]; const assistantIdx = updated.length - 1; updated.splice(assistantIdx, 0, { role: "tool-call", toolCallId: c.toolCallId, toolName: c.toolName, input: c.input }); return updated; }); } if (c.type === "tool-result") { // Insert tool-result before the trailing assistant placeholder setMessages((prev) => { const updated = [...prev]; const assistantIdx = updated.length - 1; // Find the matching tool-call to get toolName const call = updated.find( (m) => m.role === "tool-call" && m.toolCallId === c.toolCallId ); updated.splice(assistantIdx, 0, { role: "tool-result", toolCallId: c.toolCallId, toolName: call?.role === "tool-call" ? call.toolName : "unknown", output: c.output }); return updated; }); } }, onDone() { setIsStreaming(false); }, onError(error: string) { console.error("Stream error:", error); setIsStreaming(false); } }); }, [input, isStreaming, agent]); const clearHistory = useCallback(() => { agent.call("clearHistory", []); setMessages([]); }, [agent]); return (
{/* Header */}

OpenAI Agents SDK — Streaming Chat

@callable({ streaming: true }) + @openai/agents

{/* Messages */}
{messages.length === 0 && (

Send a message to start chatting.

Try: "What's the weather in Paris?"

)} {messages.map((message, i) => ( ))}
{/* Input */}
{ e.preventDefault(); send(); }} style={{ maxWidth: "700px", margin: "0 auto", display: "flex", gap: "8px" }} > setInput(e.target.value)} placeholder="Type a message..." disabled={isStreaming} style={{ flex: 1, padding: "10px 14px", fontSize: "14px", border: "1px solid #d1d5db", borderRadius: "8px", outline: "none" }} />
); } function MessageBubble({ message }: { message: DisplayMessage }) { if (message.role === "tool-call") { const args = formatArgs(message.input); return (
Calling{" "} {message.toolName} {args.length > 0 && ( ({args.map((a) => `${a.key}: ${a.value}`).join(", ")}) )}
); } if (message.role === "tool-result") { const parsed = parseToolOutput(message.output); const isObject = typeof parsed === "object"; return (
{/* Header */}
{message.toolName}
{/* Body */} {isObject ? (
{Object.entries(parsed as Record).map( ([key, value]) => (
{formatKey(key)} {value}
) )}
) : (
{parsed}
)}
); } const isUser = message.role === "user"; if (!message.content) return null; return (
{message.content}
); } const root = createRoot(document.getElementById("root")!); root.render();