demo.tsx 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281
  1. "use client";
  2. import { useEffect, useState, useCallback } from "react";
  3. import { CodeBlock } from "./code-block";
  4. const PROMPT = "Create a contact form with name, email, and message";
  5. interface StageJson {
  6. key: string;
  7. type: string;
  8. props: Record<string, unknown>;
  9. children?: StageJson[];
  10. }
  11. interface Stage {
  12. json: StageJson;
  13. stream: string;
  14. }
  15. const STAGES: Stage[] = [
  16. { json: { key: "form", type: "Form", props: { title: "Contact Us" }, children: [] }, stream: '{"op":"set","path":"/form","value":{"type":"Form","props":{"title":"Contact Us"}}}' },
  17. { json: { key: "form", type: "Form", props: { title: "Contact Us" }, children: [{ key: "name", type: "Input", props: { label: "Name", name: "name" } }] }, stream: '{"op":"add","path":"/form/children/0","value":{"type":"Input","props":{"label":"Name"}}}' },
  18. { json: { key: "form", type: "Form", props: { title: "Contact Us" }, children: [{ key: "name", type: "Input", props: { label: "Name", name: "name" } }, { key: "email", type: "Input", props: { label: "Email", name: "email" } }] }, stream: '{"op":"add","path":"/form/children/1","value":{"type":"Input","props":{"label":"Email"}}}' },
  19. { json: { key: "form", type: "Form", props: { title: "Contact Us" }, children: [{ key: "name", type: "Input", props: { label: "Name", name: "name" } }, { key: "email", type: "Input", props: { label: "Email", name: "email" } }, { key: "message", type: "Textarea", props: { label: "Message", name: "message" } }] }, stream: '{"op":"add","path":"/form/children/2","value":{"type":"Textarea","props":{"label":"Message"}}}' },
  20. { json: { key: "form", type: "Form", props: { title: "Contact Us" }, children: [{ key: "name", type: "Input", props: { label: "Name", name: "name" } }, { key: "email", type: "Input", props: { label: "Email", name: "email" } }, { key: "message", type: "Textarea", props: { label: "Message", name: "message" } }, { key: "submit", type: "Button", props: { label: "Send Message", action: "submit" } }] }, stream: '{"op":"add","path":"/form/children/3","value":{"type":"Button","props":{"label":"Send Message"}}}' },
  21. ];
  22. const CODE_EXAMPLE = `import { createCatalog } from '@json-render/core';
  23. import { z } from 'zod';
  24. export const catalog = createCatalog({
  25. components: {
  26. Form: {
  27. props: z.object({
  28. title: z.string(),
  29. }),
  30. hasChildren: true,
  31. },
  32. Input: {
  33. props: z.object({
  34. label: z.string(),
  35. name: z.string(),
  36. }),
  37. },
  38. Textarea: {
  39. props: z.object({
  40. label: z.string(),
  41. name: z.string(),
  42. }),
  43. },
  44. Button: {
  45. props: z.object({
  46. label: z.string(),
  47. action: z.string(),
  48. }),
  49. },
  50. },
  51. });`;
  52. type Phase = "typing" | "streaming" | "complete";
  53. type Tab = "stream" | "json" | "code";
  54. export function Demo() {
  55. const [phase, setPhase] = useState<Phase>("typing");
  56. const [typedPrompt, setTypedPrompt] = useState("");
  57. const [stageIndex, setStageIndex] = useState(-1);
  58. const [streamLines, setStreamLines] = useState<string[]>([]);
  59. const [activeTab, setActiveTab] = useState<Tab>("json");
  60. const [actionFired, setActionFired] = useState(false);
  61. const currentStage = stageIndex >= 0 ? STAGES[stageIndex] : null;
  62. const reset = useCallback(() => {
  63. setPhase("typing");
  64. setTypedPrompt("");
  65. setStageIndex(-1);
  66. setStreamLines([]);
  67. setActionFired(false);
  68. }, []);
  69. // Typing effect
  70. useEffect(() => {
  71. if (phase !== "typing") return;
  72. let i = 0;
  73. const interval = setInterval(() => {
  74. if (i < PROMPT.length) {
  75. setTypedPrompt(PROMPT.slice(0, i + 1));
  76. i++;
  77. } else {
  78. clearInterval(interval);
  79. setTimeout(() => setPhase("streaming"), 500);
  80. }
  81. }, 20);
  82. return () => clearInterval(interval);
  83. }, [phase]);
  84. // Streaming effect
  85. useEffect(() => {
  86. if (phase !== "streaming") return;
  87. let i = 0;
  88. const interval = setInterval(() => {
  89. if (i < STAGES.length) {
  90. const currentIndex = i;
  91. const stage = STAGES[currentIndex];
  92. if (stage) {
  93. setStageIndex(currentIndex);
  94. setStreamLines((prev) => [...prev, stage.stream]);
  95. }
  96. i++;
  97. } else {
  98. clearInterval(interval);
  99. setTimeout(() => {
  100. setPhase("complete");
  101. }, 500);
  102. }
  103. }, 600);
  104. return () => clearInterval(interval);
  105. }, [phase]);
  106. const handleAction = () => {
  107. setActionFired(true);
  108. setTimeout(() => setActionFired(false), 2000);
  109. };
  110. // Progressive render based on current stage
  111. const renderPreview = () => {
  112. if (!currentStage) {
  113. return <div className="text-muted-foreground/50 text-sm">waiting...</div>;
  114. }
  115. const { props, children = [] } = currentStage.json;
  116. const title = props.title as string | undefined;
  117. const nameField = children.find(c => c.key === "name");
  118. const emailField = children.find(c => c.key === "email");
  119. const messageField = children.find(c => c.key === "message");
  120. const submitBtn = children.find(c => c.key === "submit");
  121. return (
  122. <div className="text-center animate-in fade-in duration-200">
  123. <div className="border border-border rounded-lg p-4 bg-background inline-block text-left w-56">
  124. {title && (
  125. <h3 className="font-semibold mb-3 text-sm">{title}</h3>
  126. )}
  127. <div className="space-y-2">
  128. {nameField && (
  129. <div className="animate-in fade-in slide-in-from-bottom-1 duration-200">
  130. <label className="text-[10px] text-muted-foreground block mb-0.5">{nameField.props.label as string}</label>
  131. <div className="h-7 w-full bg-card border border-border rounded px-2 text-xs" />
  132. </div>
  133. )}
  134. {emailField && (
  135. <div className="animate-in fade-in slide-in-from-bottom-1 duration-200">
  136. <label className="text-[10px] text-muted-foreground block mb-0.5">{emailField.props.label as string}</label>
  137. <div className="h-7 w-full bg-card border border-border rounded px-2 text-xs" />
  138. </div>
  139. )}
  140. {messageField && (
  141. <div className="animate-in fade-in slide-in-from-bottom-1 duration-200">
  142. <label className="text-[10px] text-muted-foreground block mb-0.5">{messageField.props.label as string}</label>
  143. <div className="h-14 w-full bg-card border border-border rounded px-2 text-xs" />
  144. </div>
  145. )}
  146. {submitBtn && (
  147. <button
  148. onClick={handleAction}
  149. className="w-full px-3 py-1.5 bg-foreground text-background rounded text-xs font-medium hover:opacity-90 transition-opacity animate-in fade-in slide-in-from-bottom-1 duration-200"
  150. >
  151. {submitBtn.props.label as string}
  152. </button>
  153. )}
  154. </div>
  155. </div>
  156. {actionFired && (
  157. <div className="mt-3 text-xs font-mono text-muted-foreground animate-in fade-in slide-in-from-bottom-2">
  158. onAction(&quot;submit&quot;)
  159. </div>
  160. )}
  161. </div>
  162. );
  163. };
  164. const jsonCode = currentStage
  165. ? JSON.stringify(currentStage.json, null, 2)
  166. : "// waiting...";
  167. return (
  168. <div className="w-full max-w-4xl mx-auto">
  169. {/* Prompt input */}
  170. <div className="mb-6">
  171. <div className="border border-border rounded p-3 bg-card font-mono text-sm min-h-[44px] flex items-center justify-between">
  172. <div className="flex items-center">
  173. <span className="inline-flex items-center h-5">{typedPrompt}</span>
  174. {phase === "typing" && (
  175. <span className="inline-block w-2 h-4 bg-foreground ml-0.5 animate-pulse" />
  176. )}
  177. </div>
  178. <button
  179. onClick={reset}
  180. className="ml-2 p-1 text-muted-foreground hover:text-foreground transition-colors"
  181. aria-label="Replay demo"
  182. >
  183. <svg
  184. width="14"
  185. height="14"
  186. viewBox="0 0 24 24"
  187. fill="none"
  188. stroke="currentColor"
  189. strokeWidth="2"
  190. strokeLinecap="round"
  191. strokeLinejoin="round"
  192. >
  193. <path d="M21 12a9 9 0 1 1-9-9c2.52 0 4.93 1 6.74 2.74L21 8" />
  194. <path d="M21 3v5h-5" />
  195. </svg>
  196. </button>
  197. </div>
  198. </div>
  199. <div className="grid lg:grid-cols-2 gap-4">
  200. {/* Tabbed code/stream/json panel */}
  201. <div>
  202. <div className="flex gap-4 mb-2">
  203. {(["json", "stream", "code"] as const).map((tab) => (
  204. <button
  205. key={tab}
  206. onClick={() => setActiveTab(tab)}
  207. className={`text-xs font-mono transition-colors ${
  208. activeTab === tab ? "text-foreground" : "text-muted-foreground hover:text-foreground"
  209. }`}
  210. >
  211. {tab}
  212. </button>
  213. ))}
  214. </div>
  215. <div className="border border-border rounded p-3 bg-card font-mono text-xs h-96 overflow-auto text-left">
  216. {activeTab === "stream" && (
  217. <div className="space-y-1">
  218. {streamLines.map((line, i) => (
  219. <div
  220. key={i}
  221. className="text-muted-foreground truncate animate-in fade-in slide-in-from-bottom-1 duration-200"
  222. >
  223. {line}
  224. </div>
  225. ))}
  226. {phase === "streaming" && streamLines.length < STAGES.length && (
  227. <div className="flex gap-1 mt-2">
  228. <span className="w-1 h-1 bg-muted-foreground rounded-full animate-pulse" />
  229. <span className="w-1 h-1 bg-muted-foreground rounded-full animate-pulse [animation-delay:75ms]" />
  230. <span className="w-1 h-1 bg-muted-foreground rounded-full animate-pulse [animation-delay:150ms]" />
  231. </div>
  232. )}
  233. {streamLines.length === 0 && phase !== "streaming" && (
  234. <div className="text-muted-foreground/50">waiting...</div>
  235. )}
  236. </div>
  237. )}
  238. <div className={activeTab === "json" ? "" : "hidden"}>
  239. <CodeBlock code={jsonCode} lang="json" />
  240. </div>
  241. <div className={activeTab === "code" ? "" : "hidden"}>
  242. <CodeBlock code={CODE_EXAMPLE} lang="tsx" />
  243. </div>
  244. </div>
  245. </div>
  246. {/* Rendered output */}
  247. <div>
  248. <div className="text-xs text-muted-foreground mb-2 font-mono">render</div>
  249. <div className="border border-border rounded p-3 bg-card h-96 flex items-center justify-center">
  250. {renderPreview()}
  251. </div>
  252. </div>
  253. </div>
  254. </div>
  255. );
  256. }