demo.tsx 8.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250
  1. "use client";
  2. import { useEffect, useState, useCallback } from "react";
  3. import { CodeBlock } from "./code-block";
  4. const PROMPT = "Create a welcome card with a get started button";
  5. interface StageJson {
  6. key: string;
  7. type: string;
  8. props: {
  9. title?: string;
  10. description?: string;
  11. label?: string;
  12. action?: string;
  13. };
  14. children?: StageJson[];
  15. }
  16. interface Stage {
  17. json: StageJson;
  18. stream: string;
  19. }
  20. const STAGES: Stage[] = [
  21. { json: { key: "root", type: "Card", props: {} }, stream: '{"op":"set","path":"/root","value":{"key":"root","type":"Card"}}' },
  22. { json: { key: "root", type: "Card", props: { title: "Welcome" } }, stream: '{"op":"replace","path":"/root/props/title","value":"Welcome"}' },
  23. { json: { key: "root", type: "Card", props: { title: "Welcome", description: "Get started with json-render" } }, stream: '{"op":"replace","path":"/root/props/description","value":"Get started..."}' },
  24. { json: { key: "root", type: "Card", props: { title: "Welcome", description: "Get started with json-render" }, children: [{ key: "btn", type: "Button", props: {} }] }, stream: '{"op":"add","path":"/root/children","value":{"key":"btn","type":"Button"}}' },
  25. { json: { key: "root", type: "Card", props: { title: "Welcome", description: "Get started with json-render" }, children: [{ key: "btn", type: "Button", props: { label: "Get Started", action: "start" } }] }, stream: '{"op":"replace","path":"/root/children/0/props","value":{"label":"Get Started"}}' },
  26. ];
  27. const CODE_EXAMPLE = `const registry = {
  28. Card: ({ element, children }) => (
  29. <div className="card">
  30. <h3>{element.props.title}</h3>
  31. <p>{element.props.description}</p>
  32. {children}
  33. </div>
  34. ),
  35. Button: ({ element, onAction }) => (
  36. <button onClick={() => onAction(element.props.action)}>
  37. {element.props.label}
  38. </button>
  39. ),
  40. };
  41. <Renderer tree={tree} registry={registry} />`;
  42. type Phase = "typing" | "streaming" | "complete";
  43. type Tab = "stream" | "json" | "code";
  44. export function Demo() {
  45. const [phase, setPhase] = useState<Phase>("typing");
  46. const [typedPrompt, setTypedPrompt] = useState("");
  47. const [stageIndex, setStageIndex] = useState(-1);
  48. const [streamLines, setStreamLines] = useState<string[]>([]);
  49. const [activeTab, setActiveTab] = useState<Tab>("stream");
  50. const [actionFired, setActionFired] = useState(false);
  51. const currentStage = stageIndex >= 0 ? STAGES[stageIndex] : null;
  52. const reset = useCallback(() => {
  53. setPhase("typing");
  54. setTypedPrompt("");
  55. setStageIndex(-1);
  56. setStreamLines([]);
  57. setActiveTab("stream");
  58. setActionFired(false);
  59. }, []);
  60. // Typing effect
  61. useEffect(() => {
  62. if (phase !== "typing") return;
  63. let i = 0;
  64. const interval = setInterval(() => {
  65. if (i < PROMPT.length) {
  66. setTypedPrompt(PROMPT.slice(0, i + 1));
  67. i++;
  68. } else {
  69. clearInterval(interval);
  70. setTimeout(() => setPhase("streaming"), 500);
  71. }
  72. }, 40);
  73. return () => clearInterval(interval);
  74. }, [phase]);
  75. // Streaming effect
  76. useEffect(() => {
  77. if (phase !== "streaming") return;
  78. let i = 0;
  79. const interval = setInterval(() => {
  80. if (i < STAGES.length) {
  81. const currentIndex = i;
  82. const stage = STAGES[currentIndex];
  83. if (stage) {
  84. setStageIndex(currentIndex);
  85. setStreamLines((prev) => [...prev, stage.stream]);
  86. }
  87. i++;
  88. } else {
  89. clearInterval(interval);
  90. setTimeout(() => {
  91. setPhase("complete");
  92. setActiveTab("json");
  93. }, 500);
  94. }
  95. }, 600);
  96. return () => clearInterval(interval);
  97. }, [phase]);
  98. // Auto-restart
  99. useEffect(() => {
  100. if (phase !== "complete") return;
  101. const timeout = setTimeout(() => {
  102. reset();
  103. }, 8000);
  104. return () => clearTimeout(timeout);
  105. }, [phase, reset]);
  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 hasTitle = props.title;
  117. const hasDesc = props.description;
  118. const buttonLabel = children?.[0]?.props?.label;
  119. return (
  120. <div className="text-center animate-in fade-in duration-200">
  121. <div className="border border-border rounded-lg p-4 bg-background inline-block text-left">
  122. {hasTitle ? (
  123. <h3 className="font-semibold mb-1">{props.title}</h3>
  124. ) : (
  125. <div className="h-5 w-20 bg-muted rounded animate-pulse mb-1" />
  126. )}
  127. {hasDesc ? (
  128. <p className="text-xs text-muted-foreground mb-3">{props.description}</p>
  129. ) : hasTitle ? (
  130. <div className="h-3 w-32 bg-muted rounded animate-pulse mb-3" />
  131. ) : null}
  132. {buttonLabel ? (
  133. <button
  134. onClick={handleAction}
  135. className="px-3 py-1.5 bg-foreground text-background rounded text-xs font-medium hover:opacity-90 transition-opacity"
  136. >
  137. {buttonLabel}
  138. </button>
  139. ) : hasDesc ? (
  140. <div className="h-7 w-24 bg-muted rounded animate-pulse" />
  141. ) : null}
  142. </div>
  143. {actionFired && (
  144. <div className="mt-3 text-xs font-mono text-muted-foreground animate-in fade-in slide-in-from-bottom-2">
  145. onAction(&quot;start&quot;)
  146. </div>
  147. )}
  148. </div>
  149. );
  150. };
  151. const jsonCode = currentStage
  152. ? JSON.stringify(currentStage.json, null, 2)
  153. : "// waiting...";
  154. return (
  155. <div className="w-full max-w-4xl mx-auto">
  156. {/* Prompt input */}
  157. <div className="mb-6">
  158. <div className="border border-border rounded p-3 bg-card font-mono text-sm min-h-[44px] flex items-center">
  159. <span>{typedPrompt}</span>
  160. {phase === "typing" && (
  161. <span className="inline-block w-2 h-4 bg-foreground ml-0.5 animate-pulse" />
  162. )}
  163. </div>
  164. </div>
  165. <div className="grid lg:grid-cols-2 gap-4">
  166. {/* Tabbed code/stream/json panel */}
  167. <div>
  168. <div className="flex gap-4 mb-2">
  169. {(["stream", "json", "code"] as const).map((tab) => (
  170. <button
  171. key={tab}
  172. onClick={() => setActiveTab(tab)}
  173. className={`text-xs font-mono transition-colors ${
  174. activeTab === tab ? "text-foreground" : "text-muted-foreground hover:text-foreground"
  175. }`}
  176. >
  177. {tab}
  178. </button>
  179. ))}
  180. </div>
  181. <div className="border border-border rounded p-3 bg-card font-mono text-xs h-72 overflow-auto text-left">
  182. {activeTab === "stream" && (
  183. <div className="space-y-1">
  184. {streamLines.map((line, i) => (
  185. <div
  186. key={i}
  187. className="text-muted-foreground truncate animate-in fade-in slide-in-from-bottom-1 duration-200"
  188. >
  189. {line}
  190. </div>
  191. ))}
  192. {phase === "streaming" && streamLines.length < STAGES.length && (
  193. <div className="flex gap-1 mt-2">
  194. <span className="w-1 h-1 bg-muted-foreground rounded-full animate-pulse" />
  195. <span className="w-1 h-1 bg-muted-foreground rounded-full animate-pulse [animation-delay:75ms]" />
  196. <span className="w-1 h-1 bg-muted-foreground rounded-full animate-pulse [animation-delay:150ms]" />
  197. </div>
  198. )}
  199. {streamLines.length === 0 && phase !== "streaming" && (
  200. <div className="text-muted-foreground/50">waiting...</div>
  201. )}
  202. </div>
  203. )}
  204. {activeTab === "json" && (
  205. <CodeBlock code={jsonCode} lang="json" />
  206. )}
  207. {activeTab === "code" && (
  208. <CodeBlock code={CODE_EXAMPLE} lang="tsx" />
  209. )}
  210. </div>
  211. </div>
  212. {/* Rendered output */}
  213. <div>
  214. <div className="text-xs text-muted-foreground mb-2 font-mono">render</div>
  215. <div className="border border-border rounded p-3 bg-card h-72 flex items-center justify-center">
  216. {renderPreview()}
  217. </div>
  218. </div>
  219. </div>
  220. </div>
  221. );
  222. }