demo.tsx 26 KB

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