demo.tsx 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341
  1. "use client";
  2. import React, { useEffect, useState, useCallback, useRef } from "react";
  3. import { Renderer, useUIStream, JSONUIProvider } from "@json-render/react";
  4. import type { UITree } from "@json-render/core";
  5. import { CodeBlock } from "./code-block";
  6. import { demoRegistry, fallbackComponent, useInteractiveState } from "./demo/index";
  7. const SIMULATION_PROMPT = "Create a contact form with name, email, and message";
  8. interface SimulationStage {
  9. tree: UITree;
  10. stream: string;
  11. }
  12. const SIMULATION_STAGES: SimulationStage[] = [
  13. {
  14. tree: { root: "card", elements: { card: { key: "card", type: "Card", props: { title: "Contact Us", maxWidth: "md" }, children: [] } } },
  15. stream: '{"op":"set","path":"/root","value":"card"}',
  16. },
  17. {
  18. 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" } } } },
  19. stream: '{"op":"add","path":"/elements/card","value":{"key":"card","type":"Card","props":{"title":"Contact Us","maxWidth":"md"},"children":["name"]}}',
  20. },
  21. {
  22. 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" } } } },
  23. stream: '{"op":"add","path":"/elements/email","value":{"key":"email","type":"Input","props":{"label":"Email","name":"email"}}}',
  24. },
  25. {
  26. 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" } } } },
  27. stream: '{"op":"add","path":"/elements/message","value":{"key":"message","type":"Textarea","props":{"label":"Message","name":"message"}}}',
  28. },
  29. {
  30. 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" } } } },
  31. stream: '{"op":"add","path":"/elements/submit","value":{"key":"submit","type":"Button","props":{"label":"Send Message","variant":"primary"}}}',
  32. },
  33. ];
  34. const CODE_EXAMPLE = `import { Renderer, useUIStream } from '@json-render/react';
  35. import { registry } from './registry';
  36. function App() {
  37. const { tree, isStreaming, send } = useUIStream({
  38. api: '/api/generate',
  39. });
  40. return (
  41. <Renderer
  42. tree={tree}
  43. registry={registry}
  44. loading={isStreaming}
  45. />
  46. );
  47. }`;
  48. type Mode = "simulation" | "interactive";
  49. type Phase = "typing" | "streaming" | "complete";
  50. type Tab = "stream" | "json" | "code";
  51. export function Demo() {
  52. const [mode, setMode] = useState<Mode>("simulation");
  53. const [phase, setPhase] = useState<Phase>("typing");
  54. const [typedPrompt, setTypedPrompt] = useState("");
  55. const [userPrompt, setUserPrompt] = useState("");
  56. const [stageIndex, setStageIndex] = useState(-1);
  57. const [streamLines, setStreamLines] = useState<string[]>([]);
  58. const [activeTab, setActiveTab] = useState<Tab>("json");
  59. const [actionFired, setActionFired] = useState(false);
  60. const [simulationTree, setSimulationTree] = useState<UITree | null>(null);
  61. // Use the library's useUIStream hook for real API calls
  62. const {
  63. tree: apiTree,
  64. isStreaming,
  65. send,
  66. clear,
  67. } = useUIStream({
  68. api: "/api/generate",
  69. onError: (err: Error) => console.error("Generation error:", err),
  70. } as Parameters<typeof useUIStream>[0]);
  71. // Initialize interactive state for Select components
  72. useInteractiveState();
  73. const currentSimulationStage = stageIndex >= 0 ? SIMULATION_STAGES[stageIndex] : null;
  74. // Determine which tree to display
  75. const currentTree = mode === "simulation" ? (currentSimulationStage?.tree || simulationTree) : apiTree;
  76. const stopGeneration = useCallback(() => {
  77. if (mode === "simulation") {
  78. setMode("interactive");
  79. setPhase("complete");
  80. setTypedPrompt(SIMULATION_PROMPT);
  81. setUserPrompt("");
  82. }
  83. clear();
  84. }, [mode, clear]);
  85. // Typing effect for simulation
  86. useEffect(() => {
  87. if (mode !== "simulation" || phase !== "typing") return;
  88. let i = 0;
  89. const interval = setInterval(() => {
  90. if (i < SIMULATION_PROMPT.length) {
  91. setTypedPrompt(SIMULATION_PROMPT.slice(0, i + 1));
  92. i++;
  93. } else {
  94. clearInterval(interval);
  95. setTimeout(() => setPhase("streaming"), 500);
  96. }
  97. }, 20);
  98. return () => clearInterval(interval);
  99. }, [mode, phase]);
  100. // Streaming effect for simulation
  101. useEffect(() => {
  102. if (mode !== "simulation" || phase !== "streaming") return;
  103. let i = 0;
  104. const interval = setInterval(() => {
  105. if (i < SIMULATION_STAGES.length) {
  106. const stage = SIMULATION_STAGES[i];
  107. if (stage) {
  108. setStageIndex(i);
  109. setStreamLines((prev) => [...prev, stage.stream]);
  110. setSimulationTree(stage.tree);
  111. }
  112. i++;
  113. } else {
  114. clearInterval(interval);
  115. setTimeout(() => {
  116. setPhase("complete");
  117. setMode("interactive");
  118. setUserPrompt("");
  119. }, 500);
  120. }
  121. }, 600);
  122. return () => clearInterval(interval);
  123. }, [mode, phase]);
  124. // Track stream lines from real API
  125. useEffect(() => {
  126. if (mode === "interactive" && apiTree) {
  127. // Convert tree to stream line for display
  128. const streamLine = JSON.stringify({ tree: apiTree });
  129. if (!streamLines.includes(streamLine) && Object.keys(apiTree.elements).length > 0) {
  130. setStreamLines((prev) => {
  131. const lastLine = prev[prev.length - 1];
  132. if (lastLine !== streamLine) {
  133. return [...prev, streamLine];
  134. }
  135. return prev;
  136. });
  137. }
  138. }
  139. }, [mode, apiTree, streamLines]);
  140. const handleSubmit = useCallback(async () => {
  141. if (!userPrompt.trim() || isStreaming) return;
  142. setStreamLines([]);
  143. await send(userPrompt);
  144. }, [userPrompt, isStreaming, send]);
  145. // Expose action handler for registry components
  146. useEffect(() => {
  147. (window as unknown as { __demoAction?: () => void }).__demoAction = () => {
  148. setActionFired(true);
  149. setTimeout(() => setActionFired(false), 2000);
  150. };
  151. return () => {
  152. delete (window as unknown as { __demoAction?: () => void }).__demoAction;
  153. };
  154. }, []);
  155. const jsonCode = currentTree ? JSON.stringify(currentTree, null, 2) : "// waiting...";
  156. const isTypingSimulation = mode === "simulation" && phase === "typing";
  157. const isStreamingSimulation = mode === "simulation" && phase === "streaming";
  158. const showLoadingDots = isStreamingSimulation || isStreaming;
  159. return (
  160. <div className="w-full max-w-4xl mx-auto">
  161. {/* Prompt input */}
  162. <div className="mb-6">
  163. <div className="border border-border rounded p-3 bg-card font-mono text-sm min-h-[44px] flex items-center justify-between">
  164. {mode === "simulation" ? (
  165. <div className="flex items-center flex-1">
  166. <span className="inline-flex items-center h-5">{typedPrompt}</span>
  167. {isTypingSimulation && (
  168. <span className="inline-block w-2 h-4 bg-foreground ml-0.5 animate-pulse" />
  169. )}
  170. </div>
  171. ) : (
  172. <form
  173. className="flex items-center flex-1"
  174. onSubmit={(e) => {
  175. e.preventDefault();
  176. handleSubmit();
  177. }}
  178. >
  179. <input
  180. type="text"
  181. value={userPrompt}
  182. onChange={(e) => setUserPrompt(e.target.value)}
  183. placeholder="Describe what you want to build..."
  184. className="flex-1 bg-transparent outline-none placeholder:text-muted-foreground/50"
  185. disabled={isStreaming}
  186. maxLength={140}
  187. autoFocus
  188. />
  189. </form>
  190. )}
  191. {(mode === "simulation" || isStreaming) ? (
  192. <button
  193. onClick={stopGeneration}
  194. className="ml-2 p-1 text-muted-foreground hover:text-foreground transition-colors"
  195. aria-label="Stop"
  196. >
  197. <svg
  198. width="14"
  199. height="14"
  200. viewBox="0 0 24 24"
  201. fill="currentColor"
  202. stroke="none"
  203. >
  204. <rect x="6" y="6" width="12" height="12" rx="1" />
  205. </svg>
  206. </button>
  207. ) : (
  208. <button
  209. onClick={handleSubmit}
  210. disabled={!userPrompt.trim()}
  211. className="ml-2 p-1 text-muted-foreground hover:text-foreground transition-colors disabled:opacity-30"
  212. aria-label="Submit"
  213. >
  214. <svg
  215. width="14"
  216. height="14"
  217. viewBox="0 0 24 24"
  218. fill="none"
  219. stroke="currentColor"
  220. strokeWidth="2"
  221. strokeLinecap="round"
  222. strokeLinejoin="round"
  223. >
  224. <path d="M12 5v14" />
  225. <path d="M19 12l-7 7-7-7" />
  226. </svg>
  227. </button>
  228. )}
  229. </div>
  230. <div className="mt-2 text-xs text-muted-foreground">
  231. Try: &quot;Create a login form&quot; or &quot;Build a feedback form with rating&quot;
  232. </div>
  233. </div>
  234. <div className="grid lg:grid-cols-2 gap-4">
  235. {/* Tabbed code/stream/json panel */}
  236. <div>
  237. <div className="flex gap-4 mb-2">
  238. {(["json", "stream", "code"] as const).map((tab) => (
  239. <button
  240. key={tab}
  241. onClick={() => setActiveTab(tab)}
  242. className={`text-xs font-mono transition-colors ${
  243. activeTab === tab ? "text-foreground" : "text-muted-foreground hover:text-foreground"
  244. }`}
  245. >
  246. {tab}
  247. </button>
  248. ))}
  249. </div>
  250. <div className="border border-border rounded p-3 bg-card font-mono text-xs h-96 overflow-auto text-left">
  251. {activeTab === "stream" && (
  252. <div className="space-y-1">
  253. {streamLines.map((line, i) => (
  254. <div
  255. key={i}
  256. className="text-muted-foreground truncate animate-in fade-in slide-in-from-bottom-1 duration-200"
  257. >
  258. {line}
  259. </div>
  260. ))}
  261. {showLoadingDots && (
  262. <div className="flex gap-1 mt-2">
  263. <span className="w-1 h-1 bg-muted-foreground rounded-full animate-pulse" />
  264. <span className="w-1 h-1 bg-muted-foreground rounded-full animate-pulse [animation-delay:75ms]" />
  265. <span className="w-1 h-1 bg-muted-foreground rounded-full animate-pulse [animation-delay:150ms]" />
  266. </div>
  267. )}
  268. {streamLines.length === 0 && !showLoadingDots && (
  269. <div className="text-muted-foreground/50">waiting...</div>
  270. )}
  271. </div>
  272. )}
  273. <div className={activeTab === "json" ? "" : "hidden"}>
  274. <CodeBlock code={jsonCode} lang="json" />
  275. </div>
  276. <div className={activeTab === "code" ? "" : "hidden"}>
  277. <CodeBlock code={CODE_EXAMPLE} lang="tsx" />
  278. </div>
  279. </div>
  280. </div>
  281. {/* Rendered output using json-render */}
  282. <div>
  283. <div className="text-xs text-muted-foreground mb-2 font-mono">render</div>
  284. <div className="border border-border rounded p-3 bg-card h-96 overflow-auto flex flex-col">
  285. {currentTree && currentTree.root ? (
  286. <div className="animate-in fade-in duration-200 w-full flex flex-col items-center py-4">
  287. <div className="my-auto">
  288. <JSONUIProvider registry={demoRegistry as Parameters<typeof JSONUIProvider>[0]['registry']}>
  289. <Renderer
  290. tree={currentTree}
  291. registry={demoRegistry as Parameters<typeof Renderer>[0]['registry']}
  292. loading={isStreaming || isStreamingSimulation}
  293. fallback={fallbackComponent as Parameters<typeof Renderer>[0]['fallback']}
  294. />
  295. </JSONUIProvider>
  296. {actionFired && (
  297. <div className="mt-3 text-xs font-mono text-muted-foreground text-center animate-in fade-in slide-in-from-bottom-2">
  298. onAction()
  299. </div>
  300. )}
  301. </div>
  302. </div>
  303. ) : (
  304. <div className="h-full flex items-center justify-center text-muted-foreground/50 text-sm">
  305. {isStreaming ? "generating..." : "waiting..."}
  306. </div>
  307. )}
  308. </div>
  309. </div>
  310. </div>
  311. </div>
  312. );
  313. }