import { useEffect, useRef, useState } from "react"; // Which languages get a live sandboxed preview (RenderBlock) instead of a plain // syntax block (CodeBlock), and how each becomes a document body, lives in // ./preview/languages.js. A language like `js` is deliberately absent — // auto-executing bare script isn't this feature's job (see RenderBlock's doc // comment for the sandboxing model). import { PREVIEW_LANGS, RENDERABLE_LANGS } from "./preview/languages.js"; // Parse content into an array of {type, value, lang, streaming} blocks. // Handles: // - ```lang\ncode``` (well-formed) // - ```code``` (no language, no newline — model formatting bug) // - ```lang\ncode (unclosed — streaming in progress) function parseBlocks(content) { const blocks = []; let i = 0; while (i < content.length) { const fenceStart = content.indexOf("```", i); if (fenceStart === -1) { if (i < content.length) blocks.push({ type: "text", value: content.slice(i) }); break; } if (fenceStart > i) blocks.push({ type: "text", value: content.slice(i, fenceStart) }); let j = fenceStart + 3; let lang = ""; // Language specifier is valid only when word-chars are followed by a newline. // If there's no newline (e.g. ```pythonprint(...)) treat everything as code. const langMatch = content.slice(j).match(/^(\w+)(\r?\n)/); if (langMatch) { lang = langMatch[1]; j += langMatch[0].length; } else if (/^\r?\n/.test(content.slice(j))) { j += content[j] === "\r" ? 2 : 1; } // else: no newline — leave j as-is, lang stays "" const closeIdx = content.indexOf("```", j); if (closeIdx === -1) { blocks.push({ type: "code", lang, value: content.slice(j), streaming: true }); i = content.length; } else { blocks.push({ type: "code", lang, value: content.slice(j, closeIdx).replace(/\n$/, ""), streaming: false }); i = closeIdx + 3; } } return blocks; } export function Markdown({ content }) { if (!content) return null; const blocks = parseBlocks(content); return (
{blocks.map((block, i) => { if (block.type !== "code") return ; const lang = (block.lang || "").toLowerCase(); return RENDERABLE_LANGS.has(lang) ? : ; })}
); } function CodeBlock({ lang, value, streaming }) { const [copied, setCopied] = useState(false); const copy = () => { navigator.clipboard.writeText(value.trimEnd()).then(() => { setCopied(true); setTimeout(() => setCopied(false), 1500); }); }; return (
{lang || "code"} {streaming && } {!streaming && ( )}
        {value.trimEnd()}
      
); } // Content-Security-Policy for the rendered preview. Together with the iframe's // `sandbox` attribute below, this is the entire trust boundary for model- // authored HTML/SVG, so it stays conservative rather than convenient: // - script-src/style-src 'unsafe-inline' inline ` below is safe unescaped because this module is emitted as an // external .js asset - it is never inlined into index.html, where the HTML // parser would end the surrounding script tag early. // // postMessage is the one channel an opaque-origin sandboxed frame still has to // the parent, and this is the entire protocol over it: one message shape, // outbound only, carrying a content height and an error string. Nothing flows // the other way. The parent treats both fields as untrusted data - the height // is clamped and the message is rendered as text, never as markup - because // they were produced by the same code the sandbox exists to contain. // // Without this the frame is silent: a preview whose script throws just renders // blank, which is why the server-side validator in synapse/tools.py has to // guess at runtime failures it can't observe. const _PREVIEW_BOOTSTRAP = ``; // Substituted with the real line offset once the document is assembled and its // shell can be measured. Sits on one line so replacing it can't shift any. const _OFFSET_TOKEN = "__PREVIEW_LINE_OFFSET__"; /** * Build the sandboxed document for a fence. Returns {doc, error}: a language * whose source doesn't parse (JSX, today) has no document to show, and the * caller renders the message instead of a frame. * * The shell - charset, CSP, bootstrap - is identical for every language; only * the body differs, so only that part goes through the registry. Nothing about * the sandboxing is per-language and shouldn't be: SVG can carry ` + _PREVIEW_BOOTSTRAP + ""; // Lines of shell above the user's own code: the document head, plus whatever // the language puts in the body ahead of it (the Preact build, for JSX). const offset = (head.match(/\n/g) || []).length + body.userOffset; return { doc: (head + body.html + "").replace(_OFFSET_TOKEN, String(offset)), error: "", }; } // Auto-height bounds. The frame is sized from content, and content sized in // viewport/percentage units is therefore sized from the frame - a body with its // own margin makes that loop grow by the margin on every pass. Measuring the // body box rather than documentElement is what actually settles that loop; // _MAX_PREVIEW_H then caps anything still climbing within a few iterations. // // _MAX_H_STEPS is only a last resort against a document that oscillates // forever, so it is generous: an interactive component legitimately changes // height on every click, and a tight budget would freeze the frame mid-session // at whatever size it happened to reach. const _MIN_PREVIEW_H = 160; const _MAX_PREVIEW_H = 720; const _MAX_H_STEPS = 60; // A frame that never posts again — a synchronous `while(true)` in the user's // own script, or a runaway re-render loop the bootstrap's own coalescing // can't outpace — has nothing else to signal it. Silence past this long since // mount (or since the last message) is treated as hung and the frame is torn // down; the bootstrap's 1s heartbeat means a merely-idle-but-alive frame never // gets close to this. const _WATCHDOG_MS = 6000; // Live preview for a renderable fenced block: a Preview/Code toggle rendered // via a sandboxed iframe whose document is an encoded data: URL. // // Trust boundary: `sandbox="allow-scripts"` — deliberately without // allow-same-origin, allow-forms, allow-popups, or allow-top-navigation. No // allow-same-origin forces the iframe onto an opaque origin, which is what // actually matters here: even the inline scripts the CSP allows to run can't // read this app's cookies/localStorage, can't call its API (no credentialed // or same-origin fetch is possible), and can't reach `window.parent`. The CSP // above blocks resource and script-initiated network access. The embedding // document's `frame-src data:` policy in index.html closes a separate CSP gap: // a child is otherwise allowed to navigate its own browsing context to a URL. // The initial data: document is allowed and inherits the parent policy, while // an http(s) navigation is rejected before its request is sent. Nothing here // substitutes for a general code-execution sandbox (Docker, WASM, etc.); // model-authored code runs only inside the browser's sandboxed frame. function RenderBlock({ lang, value, streaming }) { const [tab, setTab] = useState("preview"); const [expanded, setExpanded] = useState(false); const [copied, setCopied] = useState(false); const copy = () => { navigator.clipboard.writeText(value.trimEnd()).then(() => { setCopied(true); setTimeout(() => setCopied(false), 1500); }); }; // Don't preview a block whose fence hasn't closed yet - it's incomplete // markup by definition, and re-pointing an iframe at a half-formed // document on every streamed token is both wasteful and flickery. Code view // already has its own streaming indicator (the same dot CodeBlock uses). const showPreview = tab === "preview" && !streaming; return (
setTab("preview")}> Preview setTab("code")}> Code {lang} {streaming && }
{showPreview && ( )} {!streaming && ( )}
{showPreview ? ( // Keyed by the markup: new markup is a new document, so remounting is // what resets the reported error and measured height. No reset effect. ) : (
          {value.trimEnd()}
        
)}
); } // The sandboxed frame plus the two things it reports back: its content height // and its first uncaught error. Split out of RenderBlock so the caller can key // it by markup - a fresh document then gets fresh state by remounting. function PreviewFrame({ lang, value, expanded }) { const [error, setError] = useState(""); const [doc, setDoc] = useState(""); const [buildError, setBuildError] = useState(""); const [height, setHeight] = useState(240); const [hung, setHung] = useState(false); const frameRef = useRef(null); const heightRef = useRef(240); // mirrors `height` so the listener needn't re-subscribe const stepsRef = useRef(0); const lastMsgRef = useRef(0); // set for real by the watchdog effect below // Receive the bootstrap's reports. The frame is on an opaque origin, so // e.origin is the string "null" and proves nothing - identify the sender by // its window instead, which content inside the sandbox cannot forge. useEffect(() => { const onMessage = (e) => { if (!frameRef.current || e.source !== frameRef.current.contentWindow) return; const data = e.data; if (!data || data.__nexusPreview !== 1) return; lastMsgRef.current = Date.now(); if (typeof data.err === "string" && data.err) setError(data.err); if (typeof data.h === "number" && Number.isFinite(data.h) && stepsRef.current < _MAX_H_STEPS) { const next = Math.min(_MAX_PREVIEW_H, Math.max(_MIN_PREVIEW_H, Math.round(data.h))); if (Math.abs(next - heightRef.current) >= 8) { heightRef.current = next; stepsRef.current += 1; setHeight(next); } } }; window.addEventListener("message", onMessage); return () => window.removeEventListener("message", onMessage); }, []); // Watchdog: a frame that goes silent past _WATCHDOG_MS — most likely a // synchronous infinite loop in the model's own script, which blocks even // the bootstrap's heartbeat from ever running — gets torn down rather than // left spinning. Checked on an interval rather than a single timeout so a // message arriving late (slow compile, heavy first paint) keeps resetting // the clock instead of tripping early. useEffect(() => { lastMsgRef.current = Date.now(); const id = setInterval(() => { if (Date.now() - lastMsgRef.current > _WATCHDOG_MS) { setHung(true); clearInterval(id); } }, 1000); return () => clearInterval(id); }, [lang, value]); useEffect(() => { let current = true; setDoc(""); setBuildError(""); buildSrcDoc(lang, value).then((result) => { if (!current) return; setDoc(result.doc || ""); setBuildError(result.error || ""); }); return () => { current = false; }; }, [lang, value]); // A build failure (JSX that doesn't parse) has no document to show at all, so // the message stands in for the frame rather than sitting under it. A hung // frame tears down the same way: dropping frameUrl unmounts the iframe, // which is what actually stops a runaway script from holding the tab. const frameUrl = doc && !hung ? `data:text/html;charset=utf-8,${encodeURIComponent(doc)}` : ""; const shown = hung ? "Preview stopped responding (likely an infinite loop) and was stopped." : buildError || error; return ( <> {frameUrl && (