"use client"; import React, { useEffect, useState, useCallback, useRef } from "react"; import { Renderer, useUIStream, JSONUIProvider } from "@json-render/react"; import type { UITree } from "@json-render/core"; import { toast } from "sonner"; import { CodeBlock } from "./code-block"; import { Toaster } from "./ui/sonner"; import { demoRegistry, fallbackComponent, useInteractiveState, } from "./demo/index"; const SIMULATION_PROMPT = "Create a contact form with name, email, and message"; interface SimulationStage { tree: UITree; stream: string; } const SIMULATION_STAGES: SimulationStage[] = [ { tree: { root: "card", elements: { card: { key: "card", type: "Card", props: { title: "Contact Us", maxWidth: "md" }, children: [], }, }, }, stream: '{"op":"set","path":"/root","value":"card"}', }, { tree: { root: "card", elements: { card: { key: "card", type: "Card", props: { title: "Contact Us", maxWidth: "md" }, children: ["name"], }, name: { key: "name", type: "Input", props: { label: "Name", name: "name" }, }, }, }, stream: '{"op":"add","path":"/elements/card","value":{"key":"card","type":"Card","props":{"title":"Contact Us","maxWidth":"md"},"children":["name"]}}', }, { tree: { root: "card", elements: { card: { key: "card", type: "Card", props: { title: "Contact Us", maxWidth: "md" }, children: ["name", "email"], }, name: { key: "name", type: "Input", props: { label: "Name", name: "name" }, }, email: { key: "email", type: "Input", props: { label: "Email", name: "email" }, }, }, }, stream: '{"op":"add","path":"/elements/email","value":{"key":"email","type":"Input","props":{"label":"Email","name":"email"}}}', }, { tree: { root: "card", elements: { card: { key: "card", type: "Card", props: { title: "Contact Us", maxWidth: "md" }, children: ["name", "email", "message"], }, name: { key: "name", type: "Input", props: { label: "Name", name: "name" }, }, email: { key: "email", type: "Input", props: { label: "Email", name: "email" }, }, message: { key: "message", type: "Textarea", props: { label: "Message", name: "message" }, }, }, }, stream: '{"op":"add","path":"/elements/message","value":{"key":"message","type":"Textarea","props":{"label":"Message","name":"message"}}}', }, { tree: { root: "card", elements: { card: { key: "card", type: "Card", props: { title: "Contact Us", maxWidth: "md" }, children: ["name", "email", "message", "submit"], }, name: { key: "name", type: "Input", props: { label: "Name", name: "name" }, }, email: { key: "email", type: "Input", props: { label: "Email", name: "email" }, }, message: { key: "message", type: "Textarea", props: { label: "Message", name: "message" }, }, submit: { key: "submit", type: "Button", props: { label: "Send Message", variant: "primary" }, }, }, }, stream: '{"op":"add","path":"/elements/submit","value":{"key":"submit","type":"Button","props":{"label":"Send Message","variant":"primary"}}}', }, ]; const CODE_EXAMPLE = `import { Renderer, useUIStream } from '@json-render/react'; import { registry } from './registry'; function App() { const { tree, isStreaming, send } = useUIStream({ api: '/api/generate', }); return ( ); }`; type Mode = "simulation" | "interactive"; type Phase = "typing" | "streaming" | "complete"; type Tab = "stream" | "json" | "code"; export function Demo() { const [mode, setMode] = useState("simulation"); const [phase, setPhase] = useState("typing"); const [typedPrompt, setTypedPrompt] = useState(""); const [userPrompt, setUserPrompt] = useState(""); const [stageIndex, setStageIndex] = useState(-1); const [streamLines, setStreamLines] = useState([]); const [activeTab, setActiveTab] = useState("json"); const [simulationTree, setSimulationTree] = useState(null); const [isFullscreen, setIsFullscreen] = useState(false); const inputRef = useRef(null); // Use the library's useUIStream hook for real API calls const { tree: apiTree, isStreaming, send, clear, } = useUIStream({ api: "/api/generate", onError: (err: Error) => console.error("Generation error:", err), } as Parameters[0]); // Initialize interactive state for Select components useInteractiveState(); const currentSimulationStage = stageIndex >= 0 ? SIMULATION_STAGES[stageIndex] : null; // Determine which tree to display - keep simulation tree until new API response const currentTree = mode === "simulation" ? currentSimulationStage?.tree || simulationTree : apiTree || simulationTree; const stopGeneration = useCallback(() => { if (mode === "simulation") { setMode("interactive"); setPhase("complete"); setTypedPrompt(SIMULATION_PROMPT); setUserPrompt(""); } clear(); }, [mode, clear]); // Typing effect for simulation useEffect(() => { if (mode !== "simulation" || phase !== "typing") return; let i = 0; const interval = setInterval(() => { if (i < SIMULATION_PROMPT.length) { setTypedPrompt(SIMULATION_PROMPT.slice(0, i + 1)); i++; } else { clearInterval(interval); setTimeout(() => setPhase("streaming"), 500); } }, 20); return () => clearInterval(interval); }, [mode, phase]); // Streaming effect for simulation useEffect(() => { if (mode !== "simulation" || phase !== "streaming") return; let i = 0; const interval = setInterval(() => { if (i < SIMULATION_STAGES.length) { const stage = SIMULATION_STAGES[i]; if (stage) { setStageIndex(i); setStreamLines((prev) => [...prev, stage.stream]); setSimulationTree(stage.tree); } i++; } else { clearInterval(interval); setTimeout(() => { setPhase("complete"); setMode("interactive"); setUserPrompt(""); }, 500); } }, 600); return () => clearInterval(interval); }, [mode, phase]); // Track stream lines from real API useEffect(() => { if (mode === "interactive" && apiTree) { // Convert tree to stream line for display const streamLine = JSON.stringify({ tree: apiTree }); if ( !streamLines.includes(streamLine) && Object.keys(apiTree.elements).length > 0 ) { setStreamLines((prev) => { const lastLine = prev[prev.length - 1]; if (lastLine !== streamLine) { return [...prev, streamLine]; } return prev; }); } } }, [mode, apiTree, streamLines]); const handleSubmit = useCallback(async () => { if (!userPrompt.trim() || isStreaming) return; setStreamLines([]); await send(userPrompt); }, [userPrompt, isStreaming, send]); // Expose action handler for registry components - shows toast with text useEffect(() => { ( window as unknown as { __demoAction?: (text: string) => void } ).__demoAction = (text: string) => { toast(text); }; return () => { delete (window as unknown as { __demoAction?: (text: string) => void }) .__demoAction; }; }, []); const jsonCode = currentTree ? JSON.stringify(currentTree, null, 2) : "// waiting..."; const isTypingSimulation = mode === "simulation" && phase === "typing"; const isStreamingSimulation = mode === "simulation" && phase === "streaming"; const showLoadingDots = isStreamingSimulation || isStreaming; return (
{/* Prompt input */}
{ if (mode === "simulation") { setMode("interactive"); setPhase("complete"); setUserPrompt(""); setTimeout(() => inputRef.current?.focus(), 0); } else { inputRef.current?.focus(); } }} > {mode === "simulation" ? (
{typedPrompt} {isTypingSimulation && ( )}
) : (
{ e.preventDefault(); handleSubmit(); }} > setUserPrompt(e.target.value)} placeholder="Describe what you want to build..." className="flex-1 bg-transparent outline-none placeholder:text-muted-foreground/50 text-base" disabled={isStreaming} maxLength={140} />
)} {mode === "simulation" || isStreaming ? ( ) : ( )}
Try: "Create a login form" or "Build a feedback form with rating"
{/* Tabbed code/stream/json panel */}
{(["json", "stream", "code"] as const).map((tab) => ( ))}
{streamLines.length > 0 ? ( <> {showLoadingDots && (
)} ) : (
{showLoadingDots ? "streaming..." : "waiting..."}
)}
{/* Rendered output using json-render */}
render
{currentTree && currentTree.root ? (
[0]["registry"] } > [0]["registry"] } loading={isStreaming || isStreamingSimulation} fallback={ fallbackComponent as Parameters< typeof Renderer >[0]["fallback"] } />
) : (
{isStreaming ? "generating..." : "waiting..."}
)}
{/* Fullscreen modal */} {isFullscreen && (
render
{currentTree && currentTree.root ? (
[0]["registry"] } > [0]["registry"] } loading={isStreaming || isStreamingSimulation} fallback={ fallbackComponent as Parameters< typeof Renderer >[0]["fallback"] } />
) : (
{isStreaming ? "generating..." : "waiting..."}
)}
)}
); }