import { useEffect, useMemo, useRef, useState } from "react"; import { useServerFn } from "@tanstack/react-start"; import { Copy, Download, Eye, Code2, FlaskConical, Rocket, Wrench, ExternalLink, Loader2 } from "lucide-react"; import { toast } from "sonner"; import { Button } from "@/components/ui/button"; import { withEchoRuntime } from "@/lib/echo-runtime"; import { deployEchoBuild } from "@/lib/gemini.functions"; type TestState = { status: "testing" | "passed" | "failed"; errors: string[]; controls: number }; export function SitePreview({ html, name = "echo-site", appId, buildId, deployedUrl, autoFix = false, onFix, onTestResult, }: { html: string; name?: string; appId?: string | undefined; buildId?: string | undefined; deployedUrl?: string | null | undefined; autoFix?: boolean; onFix?: (errors: string[]) => void; onTestResult?: (summary: string) => void; }) { const [tab, setTab] = useState<"preview" | "code" | "tests">("preview"); const [origin, setOrigin] = useState(""); const [test, setTest] = useState({ status: "testing", errors: [], controls: 0 }); const [url, setUrl] = useState(deployedUrl ?? null); const [deploying, setDeploying] = useState(false); const frame = useRef(null); const reported = useRef(""); const deployFn = useServerFn(deployEchoBuild); useEffect(() => setOrigin(window.location.origin), []); useEffect(() => setUrl(deployedUrl ?? null), [deployedUrl, buildId]); const runnable = useMemo(() => (origin && appId ? withEchoRuntime(html, appId, origin) : html), [html, appId, origin]); // Live testing: collect errors and the health check reported by the page inside the preview. useEffect(() => { setTest({ status: "testing", errors: [], controls: 0 }); let errors: string[] = []; let finished = false; const finish = (controls: number, textLength: number) => { if (finished) return; finished = true; if (!textLength && !errors.length) errors = ["The page rendered blank (nothing visible on screen)."]; setTest({ status: errors.length ? "failed" : "passed", errors, controls }); }; const onMessage = (e: MessageEvent) => { if (e.source !== frame.current?.contentWindow) return; const d = e.data as { echo?: number; kind?: string; text?: string; controls?: number; textLength?: number }; if (!d || d.echo !== 1) return; if (d.kind === "error" && d.text && errors.length < 20 && !errors.includes(d.text)) { errors = [...errors, d.text]; if (finished) setTest((t) => ({ ...t, status: "failed", errors })); } if (d.kind === "ready") finish(d.controls ?? 0, d.textLength ?? 0); }; window.addEventListener("message", onMessage); const timer = setTimeout(() => finish(0, errors.length ? 1 : 0), 12000); return () => { window.removeEventListener("message", onMessage); clearTimeout(timer); }; }, [runnable]); useEffect(() => { if (test.status === "testing" || reported.current === runnable) return; reported.current = runnable; onTestResult?.(test.status === "passed" ? `All live checks passed: the page loaded with no errors and ${test.controls} interactive controls.` : `Live test found ${test.errors.length} problem(s): ${test.errors.join(" | ")}${autoFix ? " Echo is fixing it automatically." : ""}`); if (test.status === "failed" && autoFix && onFix) onFix(test.errors); }, [test, runnable, autoFix, onFix, onTestResult]); const copy = async () => { await navigator.clipboard.writeText(runnable); toast.success("Code copied"); }; const download = () => { const href = URL.createObjectURL(new Blob([runnable], { type: "text/html" })); const a = document.createElement("a"); a.href = href; a.download = `${name}.html`; a.click(); URL.revokeObjectURL(href); }; const publish = async () => { if (!buildId) return; setDeploying(true); try { const r = await deployFn({ data: { buildId, apiBase: window.location.origin } }); if ("error" in r) toast.error(r.error); else { setUrl(r.url); toast.success("Published to Cloudflare. It can take up to a minute to appear."); if (r.note) toast.warning(r.note); } } catch { toast.error("Publishing failed. Try again."); } finally { setDeploying(false); } }; const testLabel = test.status === "testing" ? "Testing…" : test.status === "passed" ? "Tests passed" : `${test.errors.length} issue${test.errors.length === 1 ? "" : "s"}`; return (
{buildId && ( )}
{url && ( Live on Cloudflare: {url} )}
{origin &&