demo.tsx 24 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607
  1. "use client";
  2. import React, { useEffect, useState, useCallback, useRef } from "react";
  3. import { CodeBlock } from "./code-block";
  4. const SIMULATION_PROMPT = "Create a contact form with name, email, and message";
  5. interface UIElement {
  6. key: string;
  7. type: string;
  8. props: Record<string, unknown>;
  9. children?: string[];
  10. }
  11. interface UITree {
  12. root: string;
  13. elements: Record<string, UIElement>;
  14. }
  15. interface SimulationStage {
  16. tree: UITree;
  17. stream: string;
  18. }
  19. const SIMULATION_STAGES: SimulationStage[] = [
  20. {
  21. tree: { root: "form", elements: { form: { key: "form", type: "Form", props: { title: "Contact Us" }, children: [] } } },
  22. stream: '{"op":"set","path":"/root","value":"form"}',
  23. },
  24. {
  25. 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" } } } },
  26. stream: '{"op":"add","path":"/elements/form","value":{"key":"form","type":"Form","props":{"title":"Contact Us"},"children":["name"]}}',
  27. },
  28. {
  29. 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" } } } },
  30. stream: '{"op":"add","path":"/elements/email","value":{"key":"email","type":"Input","props":{"label":"Email","name":"email"}}}',
  31. },
  32. {
  33. 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" } } } },
  34. stream: '{"op":"add","path":"/elements/message","value":{"key":"message","type":"Textarea","props":{"label":"Message","name":"message"}}}',
  35. },
  36. {
  37. 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" } } } },
  38. stream: '{"op":"add","path":"/elements/submit","value":{"key":"submit","type":"Button","props":{"label":"Send Message","action":"submit"}}}',
  39. },
  40. ];
  41. const CODE_EXAMPLE = `import { createCatalog } from '@json-render/core';
  42. import { z } from 'zod';
  43. export const catalog = createCatalog({
  44. components: {
  45. Form: {
  46. props: z.object({
  47. title: z.string(),
  48. }),
  49. hasChildren: true,
  50. },
  51. Input: {
  52. props: z.object({
  53. label: z.string(),
  54. name: z.string(),
  55. }),
  56. },
  57. Textarea: {
  58. props: z.object({
  59. label: z.string(),
  60. name: z.string(),
  61. }),
  62. },
  63. Button: {
  64. props: z.object({
  65. label: z.string(),
  66. action: z.string(),
  67. }),
  68. },
  69. },
  70. });`;
  71. type Mode = "simulation" | "interactive";
  72. type Phase = "typing" | "streaming" | "complete";
  73. type Tab = "stream" | "json" | "code";
  74. function parsePatch(line: string): { op: string; path: string; value: unknown } | null {
  75. try {
  76. const trimmed = line.trim();
  77. if (!trimmed || trimmed.startsWith("//")) return null;
  78. return JSON.parse(trimmed);
  79. } catch {
  80. return null;
  81. }
  82. }
  83. function applyPatch(tree: UITree, patch: { op: string; path: string; value: unknown }): UITree {
  84. const newTree = { ...tree, elements: { ...tree.elements } };
  85. if (patch.path === "/root") {
  86. newTree.root = patch.value as string;
  87. return newTree;
  88. }
  89. if (patch.path.startsWith("/elements/")) {
  90. const key = patch.path.slice("/elements/".length).split("/")[0];
  91. if (key && (patch.op === "set" || patch.op === "add")) {
  92. newTree.elements[key] = patch.value as UIElement;
  93. }
  94. }
  95. return newTree;
  96. }
  97. export function Demo() {
  98. const [mode, setMode] = useState<Mode>("simulation");
  99. const [phase, setPhase] = useState<Phase>("typing");
  100. const [typedPrompt, setTypedPrompt] = useState("");
  101. const [userPrompt, setUserPrompt] = useState("");
  102. const [stageIndex, setStageIndex] = useState(-1);
  103. const [streamLines, setStreamLines] = useState<string[]>([]);
  104. const [activeTab, setActiveTab] = useState<Tab>("json");
  105. const [actionFired, setActionFired] = useState(false);
  106. const [tree, setTree] = useState<UITree | null>(null);
  107. const [isLoading, setIsLoading] = useState(false);
  108. const abortRef = useRef<AbortController | null>(null);
  109. const currentSimulationStage = stageIndex >= 0 ? SIMULATION_STAGES[stageIndex] : null;
  110. const stopGeneration = useCallback(() => {
  111. abortRef.current?.abort();
  112. if (mode === "simulation") {
  113. // Skip to interactive mode
  114. setMode("interactive");
  115. setPhase("complete");
  116. setTypedPrompt(SIMULATION_PROMPT);
  117. setUserPrompt("");
  118. }
  119. setIsLoading(false);
  120. }, [mode]);
  121. // Typing effect for simulation
  122. useEffect(() => {
  123. if (mode !== "simulation" || phase !== "typing") return;
  124. let i = 0;
  125. const interval = setInterval(() => {
  126. if (i < SIMULATION_PROMPT.length) {
  127. setTypedPrompt(SIMULATION_PROMPT.slice(0, i + 1));
  128. i++;
  129. } else {
  130. clearInterval(interval);
  131. setTimeout(() => setPhase("streaming"), 500);
  132. }
  133. }, 20);
  134. return () => clearInterval(interval);
  135. }, [mode, phase]);
  136. // Streaming effect for simulation
  137. useEffect(() => {
  138. if (mode !== "simulation" || phase !== "streaming") return;
  139. let i = 0;
  140. const interval = setInterval(() => {
  141. if (i < SIMULATION_STAGES.length) {
  142. const stage = SIMULATION_STAGES[i];
  143. if (stage) {
  144. setStageIndex(i);
  145. setStreamLines((prev) => [...prev, stage.stream]);
  146. setTree(stage.tree);
  147. }
  148. i++;
  149. } else {
  150. clearInterval(interval);
  151. setTimeout(() => {
  152. setPhase("complete");
  153. setMode("interactive");
  154. setUserPrompt("");
  155. }, 500);
  156. }
  157. }, 600);
  158. return () => clearInterval(interval);
  159. }, [mode, phase]);
  160. const handleSubmit = useCallback(async () => {
  161. if (!userPrompt.trim() || isLoading) return;
  162. abortRef.current?.abort();
  163. abortRef.current = new AbortController();
  164. setIsLoading(true);
  165. setStreamLines([]);
  166. setTree({ root: "", elements: {} });
  167. try {
  168. const response = await fetch("/api/generate", {
  169. method: "POST",
  170. headers: { "Content-Type": "application/json" },
  171. body: JSON.stringify({ prompt: userPrompt }),
  172. signal: abortRef.current.signal,
  173. });
  174. if (!response.ok) throw new Error(`HTTP error: ${response.status}`);
  175. const reader = response.body?.getReader();
  176. if (!reader) throw new Error("No response body");
  177. const decoder = new TextDecoder();
  178. let buffer = "";
  179. let currentTree: UITree = { root: "", elements: {} };
  180. while (true) {
  181. const { done, value } = await reader.read();
  182. if (done) break;
  183. buffer += decoder.decode(value, { stream: true });
  184. const lines = buffer.split("\n");
  185. buffer = lines.pop() ?? "";
  186. for (const line of lines) {
  187. const patch = parsePatch(line);
  188. if (patch) {
  189. currentTree = applyPatch(currentTree, patch);
  190. setTree({ ...currentTree });
  191. setStreamLines((prev) => [...prev, line.trim()]);
  192. }
  193. }
  194. }
  195. if (buffer.trim()) {
  196. const patch = parsePatch(buffer);
  197. if (patch) {
  198. currentTree = applyPatch(currentTree, patch);
  199. setTree({ ...currentTree });
  200. setStreamLines((prev) => [...prev, buffer.trim()]);
  201. }
  202. }
  203. } catch (err) {
  204. if ((err as Error).name !== "AbortError") {
  205. console.error("Generation error:", err);
  206. }
  207. } finally {
  208. setIsLoading(false);
  209. }
  210. }, [userPrompt, isLoading]);
  211. const handleAction = () => {
  212. setActionFired(true);
  213. setTimeout(() => setActionFired(false), 2000);
  214. };
  215. // Render a single element
  216. const renderElement = (element: UIElement, elements: Record<string, UIElement>): React.ReactNode => {
  217. const { type, props, children: childKeys = [] } = element;
  218. const renderChildren = () => childKeys.map((key) => {
  219. const child = elements[key];
  220. return child ? renderElement(child, elements) : null;
  221. });
  222. const baseClass = "animate-in fade-in slide-in-from-bottom-1 duration-200";
  223. switch (type) {
  224. // Layout
  225. case "Card":
  226. return (
  227. <div key={element.key} className={`border border-border rounded-lg p-3 bg-background ${baseClass}`}>
  228. {props.title && <div className="font-semibold text-sm mb-1">{props.title as string}</div>}
  229. {props.description && <div className="text-[10px] text-muted-foreground mb-2">{props.description as string}</div>}
  230. <div className="space-y-2">{renderChildren()}</div>
  231. </div>
  232. );
  233. case "Stack":
  234. const isHorizontal = props.direction === "horizontal";
  235. const stackGap = props.gap === "lg" ? "gap-3" : props.gap === "sm" ? "gap-1" : "gap-2";
  236. return (
  237. <div key={element.key} className={`flex ${isHorizontal ? "flex-row items-center" : "flex-col"} ${stackGap} ${baseClass}`}>
  238. {renderChildren()}
  239. </div>
  240. );
  241. case "Grid":
  242. const cols = props.columns === 4 ? "grid-cols-4" : props.columns === 3 ? "grid-cols-3" : "grid-cols-2";
  243. const gridGap = props.gap === "lg" ? "gap-3" : props.gap === "sm" ? "gap-1" : "gap-2";
  244. return (
  245. <div key={element.key} className={`grid ${cols} ${gridGap} ${baseClass}`}>
  246. {renderChildren()}
  247. </div>
  248. );
  249. case "Divider":
  250. return <hr key={element.key} className={`border-border my-2 ${baseClass}`} />;
  251. // Form Inputs
  252. case "Input":
  253. return (
  254. <div key={element.key} className={baseClass}>
  255. {props.label && <label className="text-[10px] text-muted-foreground block mb-0.5">{props.label as string}</label>}
  256. <div className="h-7 w-full bg-card border border-border rounded px-2 text-xs flex items-center text-muted-foreground/50">
  257. {props.placeholder as string || ""}
  258. </div>
  259. </div>
  260. );
  261. case "Textarea":
  262. const rows = (props.rows as number) || 3;
  263. return (
  264. <div key={element.key} className={baseClass}>
  265. {props.label && <label className="text-[10px] text-muted-foreground block mb-0.5">{props.label as string}</label>}
  266. <div className={`w-full bg-card border border-border rounded px-2 py-1 text-xs text-muted-foreground/50`} style={{ minHeight: rows * 16 }}>
  267. {props.placeholder as string || ""}
  268. </div>
  269. </div>
  270. );
  271. case "Select":
  272. return (
  273. <div key={element.key} className={baseClass}>
  274. {props.label && <label className="text-[10px] text-muted-foreground block mb-0.5">{props.label as string}</label>}
  275. <div className="h-7 w-full bg-card border border-border rounded px-2 text-xs flex items-center justify-between text-muted-foreground/50">
  276. <span>{props.placeholder as string || "Select..."}</span>
  277. <span>v</span>
  278. </div>
  279. </div>
  280. );
  281. case "Checkbox":
  282. return (
  283. <label key={element.key} className={`flex items-center gap-2 text-xs ${baseClass}`}>
  284. <div className={`w-3.5 h-3.5 border border-border rounded-sm ${props.checked ? "bg-foreground" : "bg-card"}`} />
  285. {props.label as string}
  286. </label>
  287. );
  288. case "Radio":
  289. const options = (props.options as string[]) || [];
  290. return (
  291. <div key={element.key} className={`space-y-1 ${baseClass}`}>
  292. {props.label && <div className="text-[10px] text-muted-foreground mb-1">{props.label as string}</div>}
  293. {options.map((opt, i) => (
  294. <label key={i} className="flex items-center gap-2 text-xs">
  295. <div className={`w-3.5 h-3.5 border border-border rounded-full ${i === 0 ? "bg-foreground" : "bg-card"}`} />
  296. {opt}
  297. </label>
  298. ))}
  299. </div>
  300. );
  301. case "Switch":
  302. return (
  303. <label key={element.key} className={`flex items-center justify-between gap-2 text-xs ${baseClass}`}>
  304. <span>{props.label as string}</span>
  305. <div className={`w-8 h-4 rounded-full relative ${props.checked ? "bg-foreground" : "bg-border"}`}>
  306. <div className={`absolute w-3 h-3 rounded-full bg-background top-0.5 transition-all ${props.checked ? "right-0.5" : "left-0.5"}`} />
  307. </div>
  308. </label>
  309. );
  310. // Actions
  311. case "Button":
  312. const variant = props.variant as string;
  313. const btnClass = variant === "danger" ? "bg-red-500 text-white" : variant === "secondary" ? "bg-card border border-border text-foreground" : "bg-foreground text-background";
  314. return (
  315. <button key={element.key} onClick={handleAction} className={`px-3 py-1.5 rounded text-xs font-medium hover:opacity-90 transition-opacity ${btnClass} ${baseClass}`}>
  316. {props.label as string}
  317. </button>
  318. );
  319. case "Link":
  320. return (
  321. <span key={element.key} className={`text-xs text-blue-500 underline cursor-pointer ${baseClass}`}>
  322. {props.label as string}
  323. </span>
  324. );
  325. // Typography
  326. case "Heading":
  327. const level = (props.level as number) || 2;
  328. const headingClass = level === 1 ? "text-lg font-bold" : level === 3 ? "text-xs font-semibold" : level === 4 ? "text-[10px] font-semibold" : "text-sm font-semibold";
  329. return <div key={element.key} className={`${headingClass} ${baseClass}`}>{props.text as string}</div>;
  330. case "Text":
  331. const textVariant = props.variant as string;
  332. const textClass = textVariant === "caption" ? "text-[10px]" : textVariant === "muted" ? "text-xs text-muted-foreground" : "text-xs";
  333. return <p key={element.key} className={`${textClass} ${baseClass}`}>{props.content as string}</p>;
  334. // Data Display
  335. case "Image":
  336. return (
  337. <div key={element.key} className={`bg-card border border-border rounded flex items-center justify-center text-[10px] text-muted-foreground ${baseClass}`} style={{ width: (props.width as number) || 80, height: (props.height as number) || 60 }}>
  338. {props.alt as string || "img"}
  339. </div>
  340. );
  341. case "Avatar":
  342. const name = props.name as string || "?";
  343. const initials = name.split(" ").map(n => n[0]).join("").slice(0, 2).toUpperCase();
  344. const avatarSize = props.size === "lg" ? "w-10 h-10 text-sm" : props.size === "sm" ? "w-6 h-6 text-[8px]" : "w-8 h-8 text-[10px]";
  345. return (
  346. <div key={element.key} className={`${avatarSize} rounded-full bg-muted flex items-center justify-center font-medium ${baseClass}`}>
  347. {initials}
  348. </div>
  349. );
  350. case "Badge":
  351. const badgeVariant = props.variant as string;
  352. const badgeClass = badgeVariant === "success" ? "bg-green-100 text-green-800" : badgeVariant === "warning" ? "bg-yellow-100 text-yellow-800" : badgeVariant === "danger" ? "bg-red-100 text-red-800" : "bg-muted text-foreground";
  353. return <span key={element.key} className={`px-1.5 py-0.5 rounded text-[10px] font-medium ${badgeClass} ${baseClass}`}>{props.text as string}</span>;
  354. case "Alert":
  355. const alertType = props.type as string;
  356. const alertClass = alertType === "success" ? "bg-green-50 border-green-200" : alertType === "warning" ? "bg-yellow-50 border-yellow-200" : alertType === "error" ? "bg-red-50 border-red-200" : "bg-blue-50 border-blue-200";
  357. return (
  358. <div key={element.key} className={`p-2 rounded border ${alertClass} ${baseClass}`}>
  359. <div className="text-xs font-medium">{props.title as string}</div>
  360. {props.message && <div className="text-[10px] mt-0.5">{props.message as string}</div>}
  361. </div>
  362. );
  363. case "Progress":
  364. const value = Math.min(100, Math.max(0, (props.value as number) || 0));
  365. return (
  366. <div key={element.key} className={baseClass}>
  367. {props.label && <div className="text-[10px] text-muted-foreground mb-1">{props.label as string}</div>}
  368. <div className="h-2 bg-muted rounded-full overflow-hidden">
  369. <div className="h-full bg-foreground rounded-full transition-all" style={{ width: `${value}%` }} />
  370. </div>
  371. </div>
  372. );
  373. case "Rating":
  374. const ratingValue = (props.value as number) || 0;
  375. const maxRating = (props.max as number) || 5;
  376. return (
  377. <div key={element.key} className={baseClass}>
  378. {props.label && <div className="text-[10px] text-muted-foreground mb-1">{props.label as string}</div>}
  379. <div className="flex gap-0.5">
  380. {Array.from({ length: maxRating }).map((_, i) => (
  381. <span key={i} className={`text-sm ${i < ratingValue ? "text-yellow-400" : "text-muted"}`}>*</span>
  382. ))}
  383. </div>
  384. </div>
  385. );
  386. // Fallback for Form type (legacy)
  387. case "Form":
  388. return (
  389. <div key={element.key} className={`border border-border rounded-lg p-3 bg-background ${baseClass}`}>
  390. {props.title && <div className="font-semibold text-sm mb-2">{props.title as string}</div>}
  391. <div className="space-y-2">{renderChildren()}</div>
  392. </div>
  393. );
  394. default:
  395. return <div key={element.key} className={`text-[10px] text-muted-foreground ${baseClass}`}>[{type}]</div>;
  396. }
  397. };
  398. // Render preview from tree
  399. const renderPreview = () => {
  400. const currentTree = mode === "simulation" ? currentSimulationStage?.tree : tree;
  401. if (!currentTree || !currentTree.root || !currentTree.elements[currentTree.root]) {
  402. return <div className="text-muted-foreground/50 text-sm">{isLoading ? "generating..." : "waiting..."}</div>;
  403. }
  404. const root = currentTree.elements[currentTree.root];
  405. if (!root) return null;
  406. return (
  407. <div className="text-center animate-in fade-in duration-200 w-full max-w-[240px]">
  408. {renderElement(root, currentTree.elements)}
  409. {actionFired && (
  410. <div className="mt-3 text-xs font-mono text-muted-foreground animate-in fade-in slide-in-from-bottom-2">
  411. onAction()
  412. </div>
  413. )}
  414. </div>
  415. );
  416. };
  417. const currentTree = mode === "simulation" ? currentSimulationStage?.tree : tree;
  418. const jsonCode = currentTree ? JSON.stringify(currentTree, null, 2) : "// waiting...";
  419. const isTypingSimulation = mode === "simulation" && phase === "typing";
  420. const isStreamingSimulation = mode === "simulation" && phase === "streaming";
  421. const showLoadingDots = isStreamingSimulation || isLoading;
  422. return (
  423. <div className="w-full max-w-4xl mx-auto">
  424. {/* Prompt input */}
  425. <div className="mb-6">
  426. <div className="border border-border rounded p-3 bg-card font-mono text-sm min-h-[44px] flex items-center justify-between">
  427. {mode === "simulation" ? (
  428. <div className="flex items-center flex-1">
  429. <span className="inline-flex items-center h-5">{typedPrompt}</span>
  430. {isTypingSimulation && (
  431. <span className="inline-block w-2 h-4 bg-foreground ml-0.5 animate-pulse" />
  432. )}
  433. </div>
  434. ) : (
  435. <form
  436. className="flex items-center flex-1"
  437. onSubmit={(e) => {
  438. e.preventDefault();
  439. handleSubmit();
  440. }}
  441. >
  442. <input
  443. type="text"
  444. value={userPrompt}
  445. onChange={(e) => setUserPrompt(e.target.value)}
  446. placeholder="Describe what you want to build..."
  447. className="flex-1 bg-transparent outline-none placeholder:text-muted-foreground/50"
  448. disabled={isLoading}
  449. maxLength={140}
  450. autoFocus
  451. />
  452. </form>
  453. )}
  454. {(mode === "simulation" || isLoading) ? (
  455. <button
  456. onClick={stopGeneration}
  457. className="ml-2 p-1 text-muted-foreground hover:text-foreground transition-colors"
  458. aria-label="Stop"
  459. >
  460. <svg
  461. width="14"
  462. height="14"
  463. viewBox="0 0 24 24"
  464. fill="currentColor"
  465. stroke="none"
  466. >
  467. <rect x="6" y="6" width="12" height="12" rx="1" />
  468. </svg>
  469. </button>
  470. ) : (
  471. <button
  472. onClick={handleSubmit}
  473. disabled={!userPrompt.trim()}
  474. className="ml-2 p-1 text-muted-foreground hover:text-foreground transition-colors disabled:opacity-30"
  475. aria-label="Submit"
  476. >
  477. <svg
  478. width="14"
  479. height="14"
  480. viewBox="0 0 24 24"
  481. fill="none"
  482. stroke="currentColor"
  483. strokeWidth="2"
  484. strokeLinecap="round"
  485. strokeLinejoin="round"
  486. >
  487. <path d="M12 5v14" />
  488. <path d="M19 12l-7 7-7-7" />
  489. </svg>
  490. </button>
  491. )}
  492. </div>
  493. <div className="mt-2 text-xs text-muted-foreground">
  494. Try: &quot;Create a login form&quot; or &quot;Build a feedback form with rating&quot;
  495. </div>
  496. </div>
  497. <div className="grid lg:grid-cols-2 gap-4">
  498. {/* Tabbed code/stream/json panel */}
  499. <div>
  500. <div className="flex gap-4 mb-2">
  501. {(["json", "stream", "code"] as const).map((tab) => (
  502. <button
  503. key={tab}
  504. onClick={() => setActiveTab(tab)}
  505. className={`text-xs font-mono transition-colors ${
  506. activeTab === tab ? "text-foreground" : "text-muted-foreground hover:text-foreground"
  507. }`}
  508. >
  509. {tab}
  510. </button>
  511. ))}
  512. </div>
  513. <div className="border border-border rounded p-3 bg-card font-mono text-xs h-96 overflow-auto text-left">
  514. {activeTab === "stream" && (
  515. <div className="space-y-1">
  516. {streamLines.map((line, i) => (
  517. <div
  518. key={i}
  519. className="text-muted-foreground truncate animate-in fade-in slide-in-from-bottom-1 duration-200"
  520. >
  521. {line}
  522. </div>
  523. ))}
  524. {showLoadingDots && (
  525. <div className="flex gap-1 mt-2">
  526. <span className="w-1 h-1 bg-muted-foreground rounded-full animate-pulse" />
  527. <span className="w-1 h-1 bg-muted-foreground rounded-full animate-pulse [animation-delay:75ms]" />
  528. <span className="w-1 h-1 bg-muted-foreground rounded-full animate-pulse [animation-delay:150ms]" />
  529. </div>
  530. )}
  531. {streamLines.length === 0 && !showLoadingDots && (
  532. <div className="text-muted-foreground/50">waiting...</div>
  533. )}
  534. </div>
  535. )}
  536. <div className={activeTab === "json" ? "" : "hidden"}>
  537. <CodeBlock code={jsonCode} lang="json" />
  538. </div>
  539. <div className={activeTab === "code" ? "" : "hidden"}>
  540. <CodeBlock code={CODE_EXAMPLE} lang="tsx" />
  541. </div>
  542. </div>
  543. </div>
  544. {/* Rendered output */}
  545. <div>
  546. <div className="text-xs text-muted-foreground mb-2 font-mono">render</div>
  547. <div className="border border-border rounded p-3 bg-card h-96 flex items-center justify-center">
  548. {renderPreview()}
  549. </div>
  550. </div>
  551. </div>
  552. </div>
  553. );
  554. }