import { useEffect, useRef, useState } from "react"; import { useAgentChat } from "@cloudflare/ai-chat/react"; import TextareaAutosize from "react-textarea-autosize"; import { GearIcon, CaretDownIcon } from "@phosphor-icons/react"; import { Button } from "@cloudflare/kumo"; import Footer from "./components/Footer"; import Header from "./components/Header"; import { SparkleIcon, WorkersAILogo } from "./components/Icons"; import { McpServers } from "./components/McpServers"; import UnifiedModelSelector from "./components/UnifiedModelSelector"; import ViewCodeModal from "./components/ViewCodeModal"; import { ToolCallCard } from "./components/ToolCallCard"; import { ReasoningCard } from "./components/ReasoningCard"; import { isToolUIPart, type UIMessage } from "ai"; import { useAgent } from "agents/react"; import type { MCPServersState } from "agents"; import { nanoid } from "nanoid"; import type { Playground, PlaygroundState } from "./server"; import type { Model } from "./models"; import type { McpServersComponentState } from "./components/McpServers"; import { Streamdown } from "streamdown"; const STORAGE_KEY = "playground_session_id"; const MAX_MCP_LOGS = 200; const DEFAULT_PARAMS = { model: "@cf/moonshotai/kimi-k2.5", temperature: 0, stream: true, system: "You are a helpful assistant that can do various tasks using MCP tools." }; const DEFAULT_EXTERNAL_MODELS: Record = { openai: "openai/gpt-5.2", anthropic: "anthropic/claude-sonnet-4-5-20250929", google: "google-ai-studio/gemini-3-pro-preview", xai: "xai/grok-4-1-fast-reasoning" }; const DEFAULT_MCP_STATUS: McpServersComponentState = { servers: [], tools: [], prompts: [], resources: [] }; function getOrCreateSessionId(): string { let sessionId = localStorage.getItem(STORAGE_KEY); if (!sessionId) { sessionId = nanoid(); localStorage.setItem(STORAGE_KEY, sessionId); } return sessionId; } const App = () => { const [codeVisible, setCodeVisible] = useState(false); const [settingsVisible, setSettingsVisible] = useState(false); const [parametersOpen, setParametersOpen] = useState(false); const [models, setModels] = useState([]); const [isLoadingModels, setIsLoadingModels] = useState(true); const [params, setParams] = useState({ ...DEFAULT_PARAMS, useExternalProvider: false, externalProvider: "openai", authMethod: "provider-key" }); const [mcp, setMcp] = useState(DEFAULT_MCP_STATUS); const [mcpLogs, setMcpLogs] = useState< Array<{ timestamp: number; status: string; serverUrl?: string }> >([]); const [sessionId, setSessionId] = useState(() => getOrCreateSessionId() ); const agent = useAgent({ agent: "playground", name: `Cloudflare-AI-Playground-${sessionId}`, onError(event) { console.error("[App] onError callback triggered with event:", event); }, onStateUpdate(state: PlaygroundState) { setParams(state); }, onMcpUpdate(mcpState: MCPServersState) { const servers = Object.entries(mcpState.servers || {}).map( ([id, server]) => ({ id, name: server.name, url: server.server_url, state: server.state, error: server.error }) ); setMcp({ servers, tools: mcpState.tools || [], prompts: mcpState.prompts || [], resources: mcpState.resources || [] }); for (const server of servers) { if (server.state) { setMcpLogs((prev) => { const next = [ ...prev, { timestamp: Date.now(), status: server.state, serverUrl: server.url } ]; return next.length > MAX_MCP_LOGS ? next.slice(-MAX_MCP_LOGS) : next; }); } } } }); // ── State update helper ── // Builds the full state from current params, then applies overrides. const updateState = (updates: Partial) => { agent.setState({ model: params.useExternalProvider ? params.externalModel || params.model : params.model, temperature: params.temperature, stream: params.stream, system: params.system, useExternalProvider: params.useExternalProvider, externalProvider: params.externalProvider, externalModel: params.externalModel, authMethod: params.authMethod, providerApiKey: params.providerApiKey, gatewayAccountId: params.gatewayAccountId, gatewayId: params.gatewayId, gatewayApiKey: params.gatewayApiKey, ...updates }); }; const [agentInput, setAgentInput] = useState(""); useEffect(() => { const getModels = async () => { try { const models = await agent.stub.getModels(); setModels(models as Model[]); } finally { setIsLoadingModels(false); } }; getModels(); }, [agent.stub]); const handleAgentSubmit = async ( e: React.FormEvent | React.KeyboardEvent | React.MouseEvent, extraData: Record = {} ) => { e.preventDefault(); if (!agentInput.trim()) return; const message = agentInput; setAgentInput(""); await sendMessage( { role: "user", parts: [{ type: "text", text: message }] }, { body: extraData } ); }; const { messages, clearHistory, status, sendMessage, stop } = useAgentChat< PlaygroundState, UIMessage<{ createdAt: string }> >({ agent, experimental_throttle: 50 }); const loading = status === "submitted"; const streaming = status === "streaming"; const handleReset = () => { const newSessionId = nanoid(); localStorage.setItem(STORAGE_KEY, newSessionId); clearHistory(); setSessionId(newSessionId); setMcp(DEFAULT_MCP_STATUS); setMcpLogs([]); }; const messageElement = useRef(null); useEffect(() => { if (messageElement.current && messages.length > 0) { messageElement.current.scrollTop = messageElement.current.scrollHeight; } }, [messages]); const activeModelName = params.useExternalProvider ? params.externalModel || params.model : (params.model ?? DEFAULT_PARAMS.model); const activeModel = params.useExternalProvider ? undefined : models.find((model) => model.name === activeModelName); return (
{ e.stopPropagation(); setCodeVisible(false); }} />
{/* ── Left sidebar ── */}
{/* ── Title bar ── */}
Workers AI Playground
{/* ── Model selector ── */}
{ const selectedModel = useExternal ? DEFAULT_EXTERNAL_MODELS[ params.externalProvider || "openai" ] || params.model : params.model || DEFAULT_PARAMS.model; updateState({ model: selectedModel, useExternalProvider: useExternal, externalModel: useExternal ? selectedModel : undefined }); }} onWorkersAiModelSelect={(model) => { updateState({ model: model ? model.name : DEFAULT_PARAMS.model, useExternalProvider: false, externalModel: undefined }); }} onExternalProviderChange={(provider) => { const selectedModel = DEFAULT_EXTERNAL_MODELS[provider] || params.model; updateState({ model: selectedModel, useExternalProvider: true, externalProvider: provider, externalModel: selectedModel }); }} onExternalModelSelect={(modelId) => { updateState({ model: modelId, useExternalProvider: true, externalModel: modelId }); }} onAuthMethodChange={(method) => { updateState({ useExternalProvider: true, authMethod: method }); }} onProviderApiKeyChange={(key) => { updateState({ useExternalProvider: true, authMethod: "provider-key", providerApiKey: key }); }} onGatewayAccountIdChange={(accountId) => { updateState({ useExternalProvider: true, authMethod: "gateway", gatewayAccountId: accountId }); }} onGatewayIdChange={(gwId) => { updateState({ useExternalProvider: true, authMethod: "gateway", gatewayId: gwId }); }} onGatewayApiKeyChange={(apiKey) => { updateState({ useExternalProvider: true, authMethod: "gateway", gatewayApiKey: apiKey }); }} />
{/* ── Parameters (collapsible) ── */}
{(parametersOpen || settingsVisible) && (
updateState({ system: e.target.value })} />
updateState({ temperature: Number.parseFloat(e.target.value) }) } /> {params.temperature.toFixed(1)}
)}
{/* ── MCP Servers ── */}
{/* ── Chat panel ── */}
    {messages.map((message) => { const renderedParts = message.parts .map((part, i) => { if (part.type === "text") { if (!part.text || part.text.trim() === "") return null; return (
  • {part.text}
  • ); } if (part.type === "reasoning") { if (!part.text || part.text.trim() === "") return null; return (
  • ); } if (isToolUIPart(part)) { return (
  • ); } if ( part.type === "file" && part.mediaType.startsWith("image/") ) { return (
  • Tool call response
  • ); } return null; }) .filter(Boolean); if (renderedParts.length === 0) return null; return
    {renderedParts}
    ; })} {(loading || streaming) && (messages[messages.length - 1].role !== "assistant" || messages[messages.length - 1].parts.length === 0) ? (
  • ) : null}
{/* ── Input bar ── */}
setAgentInput(e.target.value)} onKeyDown={(e) => { if (e.key === "Enter" && !e.shiftKey) { e.preventDefault(); handleAgentSubmit(e); } }} />
{loading || streaming ? ( ) : ( )}
); }; export default App;