"use client"; import React, { 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: "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 { 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 [openSelect, setOpenSelect] = useState(null); const [selectValues, setSelectValues] = useState>({}); 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 a single element const renderElement = (element: UIElement, elements: Record): React.ReactNode => { const { type, props, children: childKeys = [] } = element; const renderChildren = () => childKeys.map((key) => { const child = elements[key]; return child ? renderElement(child, elements) : null; }); const baseClass = "animate-in fade-in slide-in-from-bottom-1 duration-200"; switch (type) { // Layout case "Card": const maxWidthClass = props.maxWidth === "sm" ? "max-w-xs min-w-[280px]" : props.maxWidth === "md" ? "max-w-sm min-w-[320px]" : props.maxWidth === "lg" ? "max-w-md min-w-[360px]" : ""; const centeredClass = props.centered ? "mx-auto" : ""; return (
{props.title ?
{props.title as string}
: null} {props.description ?
{props.description as string}
: null}
{renderChildren()}
); case "Stack": const isHorizontal = props.direction === "horizontal"; const stackGap = props.gap === "lg" ? "gap-3" : props.gap === "sm" ? "gap-1" : "gap-2"; return (
{renderChildren()}
); case "Grid": const cols = props.columns === 4 ? "grid-cols-4" : props.columns === 3 ? "grid-cols-3" : "grid-cols-2"; const gridGap = props.gap === "lg" ? "gap-3" : props.gap === "sm" ? "gap-1" : "gap-2"; return (
{renderChildren()}
); case "Divider": return
; // Form Inputs case "Input": return (
{props.label ? : null}
); case "Textarea": const rows = (props.rows as number) || 3; return (
{props.label ? : null}