"use client"; import { useState, useCallback, useRef, useEffect } from "react"; import { examples } from "@/lib/examples"; import { createSpecStreamCompiler } from "@json-render/core"; import type { Spec } from "@json-render/core"; import { cn } from "@/lib/utils"; import { ScrollArea } from "@/components/ui/scroll-area"; import { Sheet, SheetContent, SheetTitle } from "@/components/ui/sheet"; import { ResizablePanelGroup, ResizablePanel, ResizableHandle, } from "@/components/ui/resizable"; import { FileText, Download, Loader2, ArrowRight, Square } from "lucide-react"; type Mode = "scratch" | "example"; type MobileView = "json" | "preview"; interface Selection { mode: Mode; exampleName?: string; } const PDF_REFRESH_INTERVAL_MS = 2000; function CopyButton({ text }: { text: string }) { const [copied, setCopied] = useState(false); return ( ); } function isRenderableSpec(spec: Spec | null): spec is Spec { if (!spec?.root || !spec.elements) return false; const root = spec.elements[spec.root]; if (!root) return false; if (root.type !== "Document" || !root.children?.length) return false; const firstChild = spec.elements[root.children[0]!]; return firstChild?.type === "Page"; } export default function Page() { const [selection, setSelection] = useState({ mode: "example", exampleName: examples[0]!.name, }); const [prompt, setPrompt] = useState(""); const [generating, setGenerating] = useState(false); const [generatedSpec, setGeneratedSpec] = useState(null); const [pdfUrl, setPdfUrl] = useState(null); const [error, setError] = useState(null); const [mobileView, setMobileView] = useState("preview"); const [examplesSheetOpen, setExamplesSheetOpen] = useState(false); const [refreshing, setRefreshing] = useState(false); const pdfUrlRef = useRef(null); const inputRef = useRef(null); const mobileInputRef = useRef(null); const abortRef = useRef(null); const codeScrollRef = useRef(null); const mobileCodeScrollRef = useRef(null); useEffect(() => { if (!generating) return; codeScrollRef.current?.scrollTo({ top: codeScrollRef.current.scrollHeight, }); mobileCodeScrollRef.current?.scrollTo({ top: mobileCodeScrollRef.current.scrollHeight, }); }, [generating, generatedSpec]); const currentExample = selection.mode === "example" ? examples.find((e) => e.name === selection.exampleName) : null; const activeSpec = generatedSpec ?? currentExample?.spec ?? null; const examplePdfUrl = selection.mode === "example" && !generatedSpec ? `/api/pdf?name=${selection.exampleName}` : null; const displayPdfUrl = pdfUrl ?? examplePdfUrl; useEffect(() => { inputRef.current?.focus(); }, [selection.mode, selection.exampleName]); const fetchPdfBlob = useCallback(async (spec: Spec, signal?: AbortSignal) => { const res = await fetch("/api/pdf", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ spec }), signal, }); if (!res.ok) throw new Error("Failed to generate PDF"); const blob = await res.blob(); const url = URL.createObjectURL(blob); const prev = pdfUrlRef.current; pdfUrlRef.current = url; setPdfUrl(url); if (prev) URL.revokeObjectURL(prev); }, []); // Progressive PDF refresh during generation const lastRefreshSpec = useRef(""); const generatedSpecRef = useRef(null); generatedSpecRef.current = generatedSpec; useEffect(() => { if (!generating) return; const interval = setInterval(() => { const spec = generatedSpecRef.current; if (!spec) return; const specKey = JSON.stringify(spec); if (specKey === lastRefreshSpec.current) return; if (!isRenderableSpec(spec)) return; lastRefreshSpec.current = specKey; setRefreshing(true); fetchPdfBlob(spec) .catch(() => {}) .finally(() => setRefreshing(false)); }, PDF_REFRESH_INTERVAL_MS); return () => clearInterval(interval); }, [generating, fetchPdfBlob]); const handleGenerate = useCallback(async () => { if (!prompt.trim()) return; abortRef.current?.abort(); const controller = new AbortController(); abortRef.current = controller; setGenerating(true); setError(null); lastRefreshSpec.current = ""; try { const startingSpec = selection.mode === "example" && currentExample ? currentExample.spec : null; const res = await fetch("/api/generate", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ prompt: prompt.trim(), startingSpec }), signal: controller.signal, }); if (!res.ok) throw new Error("Generation failed"); const reader = res.body?.getReader(); if (!reader) throw new Error("No response body"); const decoder = new TextDecoder(); const compiler = createSpecStreamCompiler( startingSpec ? { ...startingSpec } : {}, ); while (true) { const { done, value } = await reader.read(); if (done) break; const chunk = decoder.decode(value, { stream: true }); const { result, newPatches } = compiler.push(chunk); if (newPatches.length > 0) setGeneratedSpec(result); } const finalSpec = compiler.getResult(); setGeneratedSpec(finalSpec); setGenerating(false); await fetchPdfBlob(finalSpec); } catch (e) { if (controller.signal.aborted) return; setError(e instanceof Error ? e.message : "Something went wrong"); setGenerating(false); } }, [prompt, selection, currentExample, fetchPdfBlob]); const handleStop = useCallback(() => { abortRef.current?.abort(); setGenerating(false); if (isRenderableSpec(generatedSpec)) { fetchPdfBlob(generatedSpec).catch(() => {}); } }, [generatedSpec, fetchPdfBlob]); const select = (next: Selection) => { abortRef.current?.abort(); setSelection(next); setGeneratedSpec(null); setPdfUrl(null); setError(null); setPrompt(""); setGenerating(false); setExamplesSheetOpen(false); }; const handleDownload = async () => { if (!activeSpec) return; if (generatedSpec) { const res = await fetch("/api/pdf", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ spec: generatedSpec, download: true }), }); const blob = await res.blob(); const url = URL.createObjectURL(blob); const a = document.createElement("a"); a.href = url; a.download = "document.pdf"; a.click(); URL.revokeObjectURL(url); } else if (selection.mode === "example") { window.open( `/api/pdf?name=${selection.exampleName}&download=1`, "_blank", ); } }; const handleKeyDown = useCallback( (e: React.KeyboardEvent) => { if (e.key === "Enter" && !e.shiftKey) { e.preventDefault(); handleGenerate(); } }, [handleGenerate], ); const jsonCode = activeSpec ? JSON.stringify(activeSpec, null, 2) : "// select an example or generate a PDF"; // --------------------------------------------------------------------------- // Pane: Chat / Examples // --------------------------------------------------------------------------- const chatPane = (
json-render / react-pdf

start

examples

{examples.map((ex) => ( ))}
{ const target = e.target as HTMLElement; if (!target.closest("button") && target.tagName !== "TEXTAREA") { e.preventDefault(); inputRef.current?.focus(); } }} > {error && (
{error}
)}