page.tsx 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291
  1. "use client";
  2. import { useState, useCallback, useRef } from "react";
  3. import { createSpecStreamCompiler } from "@json-render/core";
  4. import { Player, PlayerRef } from "@remotion/player";
  5. import { Renderer, type TimelineSpec } from "@json-render/remotion";
  6. /**
  7. * Check if spec is complete enough to render
  8. */
  9. function isSpecComplete(spec: TimelineSpec): spec is Required<TimelineSpec> {
  10. return !!(
  11. spec.composition &&
  12. spec.tracks &&
  13. Array.isArray(spec.clips) &&
  14. spec.clips.length > 0
  15. );
  16. }
  17. const EXAMPLE_PROMPTS = [
  18. "Create a 10-second product intro with title cards",
  19. "Make a social media promo video with stats",
  20. "Build a testimonial video with quotes",
  21. "Design a company intro with split screens",
  22. ];
  23. export default function Home() {
  24. const [prompt, setPrompt] = useState("");
  25. const [isGenerating, setIsGenerating] = useState(false);
  26. const [spec, setSpec] = useState<TimelineSpec | null>(null);
  27. const [error, setError] = useState<string | null>(null);
  28. const inputRef = useRef<HTMLInputElement>(null);
  29. const playerRef = useRef<PlayerRef>(null);
  30. const generate = useCallback(async () => {
  31. if (!prompt.trim() || isGenerating) return;
  32. setIsGenerating(true);
  33. setError(null);
  34. setSpec(null);
  35. try {
  36. const response = await fetch("/api/generate", {
  37. method: "POST",
  38. headers: { "Content-Type": "application/json" },
  39. body: JSON.stringify({ prompt }),
  40. });
  41. if (!response.ok) {
  42. throw new Error("Generation failed");
  43. }
  44. const reader = response.body?.getReader();
  45. if (!reader) throw new Error("No response body");
  46. const decoder = new TextDecoder();
  47. const compiler = createSpecStreamCompiler<TimelineSpec>();
  48. while (true) {
  49. const { done, value } = await reader.read();
  50. if (done) break;
  51. const chunk = decoder.decode(value, { stream: true });
  52. const { result, newPatches } = compiler.push(chunk);
  53. if (newPatches.length > 0) {
  54. setSpec(result);
  55. }
  56. }
  57. // Get final result (processes any remaining buffer)
  58. const finalSpec = compiler.getResult();
  59. setSpec(finalSpec);
  60. // Final validation and auto-play
  61. if (isSpecComplete(finalSpec)) {
  62. // Auto-play after a short delay to ensure Player is mounted
  63. setTimeout(() => {
  64. playerRef.current?.play();
  65. }, 100);
  66. } else {
  67. setError("Generated timeline is incomplete");
  68. }
  69. } catch (err) {
  70. setError(err instanceof Error ? err.message : "Generation failed");
  71. } finally {
  72. setIsGenerating(false);
  73. }
  74. }, [prompt, isGenerating]);
  75. const handleSubmit = (e: React.FormEvent) => {
  76. e.preventDefault();
  77. generate();
  78. };
  79. const handleExampleClick = (examplePrompt: string) => {
  80. setPrompt(examplePrompt);
  81. setTimeout(() => inputRef.current?.focus(), 0);
  82. };
  83. const totalTime = spec?.composition
  84. ? spec.composition.durationInFrames / spec.composition.fps
  85. : 0;
  86. return (
  87. <div className="min-h-screen">
  88. {/* Hero */}
  89. <section className="max-w-5xl mx-auto px-6 pt-24 pb-16 text-center">
  90. <div className="text-xs font-mono text-muted-foreground mb-4">
  91. @json-render/remotion
  92. </div>
  93. <h1 className="text-5xl sm:text-6xl md:text-7xl font-bold tracking-tighter mb-6">
  94. AI &rarr; json-render &rarr; Video
  95. </h1>
  96. <p className="text-lg text-muted-foreground max-w-2xl mx-auto mb-12 leading-relaxed">
  97. Define a video catalog. Users prompt. AI outputs timeline JSON
  98. constrained to your components. Remotion renders it.
  99. </p>
  100. {/* Demo */}
  101. <div className="max-w-4xl mx-auto">
  102. {/* Prompt Input */}
  103. <form onSubmit={handleSubmit} className="mb-4">
  104. <div className="border border-border rounded p-3 bg-background font-mono text-sm flex items-center gap-2">
  105. <span className="text-muted-foreground">&gt;</span>
  106. <input
  107. ref={inputRef}
  108. type="text"
  109. value={prompt}
  110. onChange={(e) => setPrompt(e.target.value)}
  111. placeholder="Describe the video you want to create..."
  112. className="flex-1 bg-transparent outline-none placeholder:text-muted-foreground/50"
  113. disabled={isGenerating}
  114. maxLength={500}
  115. />
  116. {isGenerating ? (
  117. <button
  118. type="button"
  119. onClick={() => setIsGenerating(false)}
  120. className="w-7 h-7 rounded-full bg-primary text-primary-foreground flex items-center justify-center hover:bg-primary/90 transition-colors"
  121. >
  122. <svg
  123. width="12"
  124. height="12"
  125. viewBox="0 0 24 24"
  126. fill="currentColor"
  127. >
  128. <rect x="6" y="6" width="12" height="12" />
  129. </svg>
  130. </button>
  131. ) : (
  132. <button
  133. type="submit"
  134. disabled={!prompt.trim()}
  135. className="w-7 h-7 rounded-full bg-primary text-primary-foreground flex items-center justify-center hover:bg-primary/90 transition-colors disabled:opacity-30"
  136. >
  137. <svg
  138. width="14"
  139. height="14"
  140. viewBox="0 0 24 24"
  141. fill="none"
  142. stroke="currentColor"
  143. strokeWidth="2"
  144. >
  145. <path d="M5 12h14M12 5l7 7-7 7" />
  146. </svg>
  147. </button>
  148. )}
  149. </div>
  150. </form>
  151. {/* Example prompts */}
  152. <div className="flex flex-wrap gap-2 justify-center mb-6">
  153. {EXAMPLE_PROMPTS.map((examplePrompt) => (
  154. <button
  155. key={examplePrompt}
  156. onClick={() => handleExampleClick(examplePrompt)}
  157. className="text-xs px-3 py-1.5 rounded-full border border-border text-muted-foreground hover:text-foreground hover:border-foreground/50 transition-colors"
  158. >
  159. {examplePrompt}
  160. </button>
  161. ))}
  162. </div>
  163. {error && (
  164. <div className="mb-4 p-3 bg-red-500/10 border border-red-500/20 rounded text-red-500 text-sm">
  165. {error}
  166. </div>
  167. )}
  168. <div className="grid lg:grid-cols-2 gap-4">
  169. {/* JSON Panel */}
  170. <div className="text-left">
  171. <div className="flex items-center gap-4 mb-2 h-6">
  172. <span className="text-xs font-mono text-muted-foreground">
  173. timeline.json
  174. </span>
  175. {isGenerating && (
  176. <span className="text-xs text-muted-foreground animate-pulse">
  177. generating...
  178. </span>
  179. )}
  180. </div>
  181. <div className="border border-border rounded bg-background font-mono text-xs h-[28rem] overflow-auto">
  182. {spec ? (
  183. <pre className="p-4 text-left">
  184. <code className="text-muted-foreground">
  185. {JSON.stringify(spec, null, 2)}
  186. </code>
  187. </pre>
  188. ) : isGenerating ? (
  189. <div className="p-4 text-muted-foreground/50 h-full flex items-center justify-center">
  190. <div className="flex flex-col items-center gap-2">
  191. <div className="flex gap-1">
  192. <span className="w-2 h-2 bg-muted-foreground/50 rounded-full animate-bounce" />
  193. <span className="w-2 h-2 bg-muted-foreground/50 rounded-full animate-bounce [animation-delay:0.1s]" />
  194. <span className="w-2 h-2 bg-muted-foreground/50 rounded-full animate-bounce [animation-delay:0.2s]" />
  195. </div>
  196. <span>Generating timeline...</span>
  197. </div>
  198. </div>
  199. ) : (
  200. <div className="p-4 text-muted-foreground/50 h-full flex items-center justify-center">
  201. Enter a prompt to generate a video timeline
  202. </div>
  203. )}
  204. </div>
  205. </div>
  206. {/* Video Preview Panel */}
  207. <div>
  208. <div className="flex items-center justify-between mb-2 h-6">
  209. <span className="text-xs font-mono text-muted-foreground">
  210. preview
  211. </span>
  212. {spec?.composition && (
  213. <span className="text-xs font-mono text-muted-foreground">
  214. {totalTime.toFixed(1)}s
  215. </span>
  216. )}
  217. </div>
  218. <div className="border border-border rounded bg-black h-[28rem] relative overflow-hidden">
  219. {spec && isSpecComplete(spec) ? (
  220. <Player
  221. ref={playerRef}
  222. component={Renderer}
  223. inputProps={{ spec }}
  224. durationInFrames={spec.composition.durationInFrames}
  225. fps={spec.composition.fps}
  226. compositionWidth={spec.composition.width}
  227. compositionHeight={spec.composition.height}
  228. style={{
  229. width: "100%",
  230. height: "100%",
  231. }}
  232. controls
  233. loop
  234. />
  235. ) : (
  236. <div className="h-full flex items-center justify-center text-white/30 text-sm">
  237. {isGenerating ? (
  238. <div className="flex flex-col items-center gap-2">
  239. <div className="flex gap-1">
  240. <span className="w-2 h-2 bg-white/50 rounded-full animate-bounce" />
  241. <span className="w-2 h-2 bg-white/50 rounded-full animate-bounce [animation-delay:0.1s]" />
  242. <span className="w-2 h-2 bg-white/50 rounded-full animate-bounce [animation-delay:0.2s]" />
  243. </div>
  244. <span>Generating timeline...</span>
  245. </div>
  246. ) : spec ? (
  247. <div className="flex flex-col items-center gap-2">
  248. <div className="flex gap-1">
  249. <span className="w-2 h-2 bg-white/50 rounded-full animate-bounce" />
  250. <span className="w-2 h-2 bg-white/50 rounded-full animate-bounce [animation-delay:0.1s]" />
  251. <span className="w-2 h-2 bg-white/50 rounded-full animate-bounce [animation-delay:0.2s]" />
  252. </div>
  253. <span>Building timeline...</span>
  254. </div>
  255. ) : (
  256. "Enter a prompt to generate a video"
  257. )}
  258. </div>
  259. )}
  260. </div>
  261. </div>
  262. </div>
  263. </div>
  264. </section>
  265. </div>
  266. );
  267. }