"use client"; import { useEffect, useState, useCallback, useRef } from "react"; import { CodeBlock } from "./code-block"; const SIMULATION_PROMPT = "Create a contact form with name, email, and message"; interface UIElement { key: string; type: string; props: Record; children?: string[]; } interface UITree { root: string; elements: Record; } interface SimulationStage { tree: UITree; stream: string; } const SIMULATION_STAGES: SimulationStage[] = [ { tree: { root: "form", elements: { form: { key: "form", type: "Form", props: { title: "Contact Us" }, children: [] } } }, stream: '{"op":"set","path":"/root","value":"form"}', }, { tree: { root: "form", elements: { form: { key: "form", type: "Form", props: { title: "Contact Us" }, children: ["name"] }, name: { key: "name", type: "Input", props: { label: "Name", name: "name" } } } }, stream: '{"op":"add","path":"/elements/form","value":{"key":"form","type":"Form","props":{"title":"Contact Us"},"children":["name"]}}', }, { tree: { root: "form", elements: { form: { key: "form", type: "Form", props: { title: "Contact Us" }, 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: "form", elements: { form: { key: "form", type: "Form", props: { title: "Contact Us" }, 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: "form", elements: { form: { key: "form", type: "Form", props: { title: "Contact Us" }, 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", action: "submit" } } } }, stream: '{"op":"add","path":"/elements/submit","value":{"key":"submit","type":"Button","props":{"label":"Send Message","action":"submit"}}}', }, ]; const CODE_EXAMPLE = `import { createCatalog } from '@json-render/core'; import { z } from 'zod'; export const catalog = createCatalog({ components: { Form: { props: z.object({ title: z.string(), }), hasChildren: true, }, Input: { props: z.object({ label: z.string(), name: z.string(), }), }, Textarea: { props: z.object({ label: z.string(), name: z.string(), }), }, Button: { props: z.object({ label: z.string(), action: z.string(), }), }, }, });`; type Mode = "simulation" | "interactive"; type Phase = "typing" | "streaming" | "complete"; type Tab = "stream" | "json" | "code"; function parsePatch(line: string): { op: string; path: string; value: unknown } | null { try { const trimmed = line.trim(); if (!trimmed || trimmed.startsWith("//")) return null; return JSON.parse(trimmed); } catch { return null; } } function applyPatch(tree: UITree, patch: { op: string; path: string; value: unknown }): UITree { const newTree = { ...tree, elements: { ...tree.elements } }; if (patch.path === "/root") { newTree.root = patch.value as string; return newTree; } if (patch.path.startsWith("/elements/")) { const key = patch.path.slice("/elements/".length).split("/")[0]; if (key && (patch.op === "set" || patch.op === "add")) { newTree.elements[key] = patch.value as UIElement; } } return newTree; } 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 [actionFired, setActionFired] = useState(false); const [tree, setTree] = useState(null); const [isLoading, setIsLoading] = useState(false); const abortRef = useRef(null); const currentSimulationStage = stageIndex >= 0 ? SIMULATION_STAGES[stageIndex] : null; const stopGeneration = useCallback(() => { abortRef.current?.abort(); if (mode === "simulation") { // Skip to interactive mode setMode("interactive"); setPhase("complete"); setTypedPrompt(SIMULATION_PROMPT); setUserPrompt(""); } setIsLoading(false); }, [mode]); // 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]); setTree(stage.tree); } i++; } else { clearInterval(interval); setTimeout(() => { setPhase("complete"); setMode("interactive"); setUserPrompt(""); }, 500); } }, 600); return () => clearInterval(interval); }, [mode, phase]); const handleSubmit = useCallback(async () => { if (!userPrompt.trim() || isLoading) return; abortRef.current?.abort(); abortRef.current = new AbortController(); setIsLoading(true); setStreamLines([]); setTree({ root: "", elements: {} }); try { const response = await fetch("/api/generate", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ prompt: userPrompt }), signal: abortRef.current.signal, }); if (!response.ok) throw new Error(`HTTP error: ${response.status}`); const reader = response.body?.getReader(); if (!reader) throw new Error("No response body"); const decoder = new TextDecoder(); let buffer = ""; let currentTree: UITree = { root: "", elements: {} }; while (true) { const { done, value } = await reader.read(); if (done) break; buffer += decoder.decode(value, { stream: true }); const lines = buffer.split("\n"); buffer = lines.pop() ?? ""; for (const line of lines) { const patch = parsePatch(line); if (patch) { currentTree = applyPatch(currentTree, patch); setTree({ ...currentTree }); setStreamLines((prev) => [...prev, line.trim()]); } } } if (buffer.trim()) { const patch = parsePatch(buffer); if (patch) { currentTree = applyPatch(currentTree, patch); setTree({ ...currentTree }); setStreamLines((prev) => [...prev, buffer.trim()]); } } } catch (err) { if ((err as Error).name !== "AbortError") { console.error("Generation error:", err); } } finally { setIsLoading(false); } }, [userPrompt, isLoading]); const handleAction = () => { setActionFired(true); setTimeout(() => setActionFired(false), 2000); }; // Render preview from tree const renderPreview = () => { const currentTree = mode === "simulation" ? currentSimulationStage?.tree : tree; if (!currentTree || !currentTree.root || !currentTree.elements[currentTree.root]) { return
{isLoading ? "generating..." : "waiting..."}
; } const root = currentTree.elements[currentTree.root]; if (!root) return null; const title = root.props.title as string | undefined; const children = (root.children ?? []).map((key) => currentTree.elements[key]).filter(Boolean) as UIElement[]; return (
{title &&

{title}

}
{children.map((child) => { if (child.type === "Input") { return (
); } if (child.type === "Textarea") { return (
); } if (child.type === "Button") { return ( ); } return null; })}
{actionFired && (
onAction("submit")
)}
); }; const currentTree = mode === "simulation" ? currentSimulationStage?.tree : tree; const jsonCode = currentTree ? JSON.stringify(currentTree, null, 2) : "// waiting..."; const isTypingSimulation = mode === "simulation" && phase === "typing"; const isStreamingSimulation = mode === "simulation" && phase === "streaming"; const showLoadingDots = isStreamingSimulation || isLoading; return (
{/* Prompt input */}
{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" disabled={isLoading} maxLength={140} autoFocus />
)} {(mode === "simulation" || isLoading) ? ( ) : ( )}
Try: "Create a login form" or "Build a feedback form with rating"
{/* Tabbed code/stream/json panel */}
{(["json", "stream", "code"] as const).map((tab) => ( ))}
{activeTab === "stream" && (
{streamLines.map((line, i) => (
{line}
))} {showLoadingDots && (
)} {streamLines.length === 0 && !showLoadingDots && (
waiting...
)}
)}
{/* Rendered output */}
render
{renderPreview()}
); }