/** * DOM-free building blocks for showing agent previews outside a browser page * (React Native web views): the same documents and loading surface the web * chat uses, as strings and pure functions. */ import { PREVIEW_LOADING_COLORS } from "./preview-loading-paint"; import { type OpenGeniText } from "./messages/runtime"; export { inlineHtmlDocument } from "./components/artifacts/inline-html-document"; export { loadSiteSnapshot, type SiteDisplayContent, type SiteSnapshotReadClient, } from "./site-snapshot"; export { paintPreviewLoading, PREVIEW_LOADING_COLORS } from "./preview-loading-paint"; /** Fence languages the assistant uses for interactive previews. */ export type InteractivePreviewKind = "html" | "site"; export function interactivePreviewKind(language: string): InteractivePreviewKind | null { return language === "opengeni-html" ? "html" : language === "opengeni-site" ? "site" : null; } /** A Site fence body: `{"siteId": "…", "versionId"?: "…"}`; null when malformed. */ export function parseSiteFence(content: string): { siteId: string; versionId?: string } | null { try { const value = JSON.parse(content) as { siteId?: unknown; versionId?: unknown }; const uuid = /^[0-9a-f-]{36}$/i; if (!value || typeof value.siteId !== "string" || !uuid.test(value.siteId)) return null; if (value.versionId === undefined) return { siteId: value.siteId }; return typeof value.versionId === "string" && uuid.test(value.versionId) ? { siteId: value.siteId, versionId: value.versionId } : null; } catch { return null; } } /** * Script source of `paintPreviewLoading` (kept in lockstep by a test), for * documents that run outside this bundle. Release React Native builds compile * functions to bytecode, so `Function.prototype.toString` cannot be used. */ export const PREVIEW_LOADING_PAINT_SCRIPT = `function paintPreviewLoading(context,width,height,time,ink,glowColor){ context.clearRect(0,0,width,height); const cx=width*(0.5+0.13*Math.sin(time*0.21)); const cy=height*(0.5+0.12*Math.cos(time*0.27)); const glow=context.createRadialGradient(cx,cy,0,cx,cy,Math.max(width,height)*0.65); glow.addColorStop(0,glowColor);glow.addColorStop(1,"transparent"); context.globalAlpha=0.09;context.fillStyle=glow;context.fillRect(0,0,width,height); const rows=Math.ceil(height/18)+7;const cols=Math.ceil(width/18)+7; const points=Array.from({length:rows},(_row,r)=>Array.from({length:cols},(_col,c)=>{ const x=(c-3)*18;const y=(r-3)*18;const dist=Math.hypot((x-cx)*0.8,y-cy); const wave=Math.sin(dist*0.032-time*0.95); return {x:x+((x-cx)/Math.max(dist,1))*wave*4,y:y+((y-cy)/Math.max(dist,1))*wave*4,l:Math.pow((wave+1)/2,5)};})); context.strokeStyle=ink;context.fillStyle=ink;context.lineWidth=0.65; points.forEach((row,r)=>row.forEach((p,c)=>{ context.globalAlpha=0.035+p.l*0.055;context.beginPath(); if(c+1 value.replace( /[&<>"']/g, (char) => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" })[char] ?? char, ); /** * A standalone page for the web's "Preparing preview…" surface: the animated * ripple filling the page with a steady label, for a web view while the * assistant is still writing the preview. Never contains the unfinished fence. */ export function previewLoadingDocument( t: OpenGeniText, options: { scheme: "light" | "dark"; label?: string; labelColor?: string; reducedMotion?: boolean; }, ): string { const colors = PREVIEW_LOADING_COLORS[options.scheme]; const label = escapeHtml(options.label ?? t("nativePreviews.preparingPreview")); const labelColor = options.labelColor ?? (options.scheme === "dark" ? "#a1a1a1" : "#6b6b6b"); return `${label}`; }