"""Live-preview rendering for smolbuilder. Turns the agent's workspace (a `path -> content` dict of a small static web app) into a single self-contained HTML document, then into a sandboxed iframe that Gradio can drop straight into a `gr.HTML`. This is the "Replit/Lovable" preview: what the tiny model just built, running live in the browser. Deliberately dependency-free (stdlib only) so it can be unit-tested without Gradio or the Rust engine, and so the rendering logic stays trivially auditable. Design choices: - We inline locally-referenced `` and `""", re.I | re.S) _HREF_RE = re.compile(r"""\bhref\s*=\s*['"]([^'"]+)['"]""", re.I) def find_entry(files: dict[str, str]) -> str | None: """Pick the HTML entrypoint to preview, or None if there's nothing webby.""" lower = {p.lower(): p for p in files} for cand in _ENTRY_CANDIDATES: if cand in lower: return lower[cand] # Fall back to any .html file (shallowest path wins for determinism). htmls = sorted((p for p in files if p.lower().endswith(".html")), key=lambda p: (p.count("/"), p)) return htmls[0] if htmls else None def _is_local(url: str) -> bool: """True for a same-app relative reference we can inline (not a CDN/data URI).""" u = url.strip() if not u: return False return not re.match(r"^(?:[a-z]+:)?//|^https?:|^data:|^mailto:|^#", u, re.I) def _lookup(files: dict[str, str], ref: str) -> str | None: """Resolve a relative href/src against the workspace file map.""" ref = ref.split("?", 1)[0].split("#", 1)[0].lstrip("./").lstrip("/") if ref in files: return files[ref] # Case-insensitive / basename fallback so '/style.css' finds 'style.css'. base = ref.rsplit("/", 1)[-1].lower() for path, content in files.items(): if path.lower() == ref.lower() or path.rsplit("/", 1)[-1].lower() == base: return content return None def inline_app(files: dict[str, str]) -> str: """Return one self-contained HTML document for the app in `files`. If there's no HTML entrypoint, render a friendly placeholder (e.g. the model has only written notes or a not-yet-web file). """ entry = find_entry(files) if entry is None: return _placeholder(files) doc = files[entry] def _inline_css(match: re.Match) -> str: tag = match.group(0) href_m = _HREF_RE.search(tag) if not href_m or not _is_local(href_m.group(1)): return tag css = _lookup(files, href_m.group(1)) if css is None: return tag return f"" def _inline_js(match: re.Match) -> str: src = match.group(1) if not _is_local(src): return match.group(0) js = _lookup(files, src) if js is None: return match.group(0) # Guard against the inlined body prematurely closing the script element. safe = js.replace("", "<\\/script>") return f"" doc = _LINK_RE.sub(_inline_css, doc) doc = _SCRIPT_SRC_RE.sub(_inline_js, doc) return doc def _escape_srcdoc(doc: str) -> str: """Escape an HTML document for a double-quoted `srcdoc="..."` attribute. Only `&` and `"` are significant inside a double-quoted attribute value, and `&` must go first (so the `&` we introduce for `"` isn't re-escaped). `<`, `>` and even a literal `` are FINE here — the parser is in attribute-value state, not script-data state — so we must NOT touch them (html.escape would corrupt the rendered document). """ return doc.replace("&", "&").replace('"', """) def preview_iframe(files: dict[str, str], *, height: int = 540) -> str: """Render the app as a sandboxed `srcdoc` iframe ready for `gr.HTML`.""" srcdoc = _escape_srcdoc(inline_app(files)) return ( f'' ) def _placeholder(files: dict[str, str]) -> str: listing = "".join( f"
  • {html.escape(p)}
  • " for p in sorted(files) ) or "
  • workspace is empty
  • " return ( "" "" "

    No preview yet

    " "

    smolbuilder previews the app's index.html. " "Describe a web app on the left and it'll appear here, live.

    " f"

    Files in the workspace:

    " "" )