"use client"; import { useEffect, useState, useCallback, useRef, useMemo } from "react"; import { Renderer, useUIStream, JSONUIProvider } from "@json-render/react"; import type { UITree } from "@json-render/core"; import { collectUsedComponents, serializeProps } from "@json-render/codegen"; import { toast } from "sonner"; import { ResizablePanelGroup, ResizablePanel, ResizableHandle, } from "@/components/ui/resizable"; import { CodeBlock } from "./code-block"; import { CopyButton } from "./copy-button"; import { Toaster } from "./ui/sonner"; import { Header } from "./header"; import { demoRegistry, fallbackComponent, useInteractiveState, } from "./demo/index"; type Tab = "json" | "stream"; type RenderView = "preview" | "code"; type MobilePane = "chat" | "code" | "preview"; interface Version { id: string; prompt: string; tree: UITree | null; status: "generating" | "complete"; } const EXAMPLE_PROMPTS = [ "Create a login form", "Build a pricing page", "Design a user profile card", "Make a contact form", ]; export function Playground() { const [versions, setVersions] = useState([]); const [selectedVersionId, setSelectedVersionId] = useState( null, ); const [inputValue, setInputValue] = useState(""); const [streamLines, setStreamLines] = useState([]); const [activeTab, setActiveTab] = useState("json"); const [renderView, setRenderView] = useState("preview"); const [mobilePane, setMobilePane] = useState("chat"); const inputRef = useRef(null); const versionsEndRef = useRef(null); // Track the currently generating version ID const generatingVersionIdRef = useRef(null); // Track the current tree for use as previousTree in next generation const currentTreeRef = useRef(null); const { tree: apiTree, isStreaming, send, clear, } = useUIStream({ api: "/api/generate", onError: (err: Error) => console.error("Generation error:", err), } as Parameters[0]); useInteractiveState(); // Get the selected version const selectedVersion = versions.find((v) => v.id === selectedVersionId); // Determine which tree to display: // - If streaming and selected version is the generating one, show apiTree // - Otherwise show the selected version's tree const isSelectedVersionGenerating = selectedVersionId === generatingVersionIdRef.current && isStreaming; const hasValidApiTree = apiTree && apiTree.root && Object.keys(apiTree.elements).length > 0; const currentTree = isSelectedVersionGenerating && hasValidApiTree ? apiTree : (selectedVersion?.tree ?? (isSelectedVersionGenerating ? apiTree : null)); // Keep the ref updated with the current tree for use in handleSubmit if ( currentTree && currentTree.root && Object.keys(currentTree.elements).length > 0 ) { currentTreeRef.current = currentTree; } // Scroll to bottom when versions change useEffect(() => { versionsEndRef.current?.scrollIntoView({ behavior: "smooth" }); }, [versions]); useEffect(() => { if (apiTree) { 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; }); } } }, [apiTree, streamLines]); // Update version when streaming completes useEffect(() => { if ( !isStreaming && apiTree && apiTree.root && generatingVersionIdRef.current ) { const completedVersionId = generatingVersionIdRef.current; setVersions((prev) => prev.map((v) => v.id === completedVersionId ? { ...v, tree: apiTree, status: "complete" as const } : v, ), ); generatingVersionIdRef.current = null; } }, [isStreaming, apiTree]); const handleSubmit = useCallback(async () => { if (!inputValue.trim() || isStreaming) return; const newVersionId = Date.now().toString(); const newVersion: Version = { id: newVersionId, prompt: inputValue.trim(), tree: null, status: "generating", }; generatingVersionIdRef.current = newVersionId; setVersions((prev) => [...prev, newVersion]); setSelectedVersionId(newVersionId); setInputValue(""); setStreamLines([]); // Reset stream lines for new generation // Pass the current tree as context so the API can iterate on it await send(inputValue.trim(), { previousTree: currentTreeRef.current }); }, [inputValue, isStreaming, send]); const handleKeyDown = useCallback( (e: React.KeyboardEvent) => { if (e.key === "Enter" && !e.shiftKey) { e.preventDefault(); handleSubmit(); } }, [handleSubmit], ); 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 generatedCode = useMemo(() => { if (!currentTree || !currentTree.root) { return "// Generate a UI to see the code"; } const tree = currentTree; const components = collectUsedComponents(tree); function generateJSX(key: string, indent: number): string { const element = tree.elements[key]; if (!element) return ""; const spaces = " ".repeat(indent); const componentName = element.type; const propsObj: Record = {}; for (const [k, v] of Object.entries(element.props)) { if (v !== null && v !== undefined) { propsObj[k] = v; } } const propsStr = serializeProps(propsObj); const hasChildren = element.children && element.children.length > 0; if (!hasChildren) { return propsStr ? `${spaces}<${componentName} ${propsStr} />` : `${spaces}<${componentName} />`; } const lines: string[] = []; lines.push( propsStr ? `${spaces}<${componentName} ${propsStr}>` : `${spaces}<${componentName}>`, ); for (const childKey of element.children!) { lines.push(generateJSX(childKey, indent + 1)); } lines.push(`${spaces}`); return lines.join("\n"); } const jsx = generateJSX(tree.root, 2); const imports = Array.from(components).sort().join(", "); return `"use client"; import { ${imports} } from "@/components/ui"; export default function Page() { return (
${jsx}
); }`; }, [currentTree]); // Chat pane content const chatPane = (
versions
0 ? "overflow-y-auto space-y-1" : "flex"}`} > {versions.length === 0 ? (

Describe what you want to build, then iterate on it.

{EXAMPLE_PROMPTS.map((prompt) => ( ))}
) : ( versions.map((version, index) => ( )) )}
{ // Focus textarea unless clicking a button or the textarea itself const target = e.target as HTMLElement; if (!target.closest("button") && target.tagName !== "TEXTAREA") { e.preventDefault(); inputRef.current?.focus(); } }} >