import { THEME_CSS, SVG_CLASSES_CSS, FORM_STYLES_WITH_STAGGER_CSS as FORM_STYLES_CSS, IMPORTMAP_SCRIPT_TAG, } from "@repo/design-system"; const CHART_COLORS = [ "#3b82f6", "#8b5cf6", "#ec4899", "#f59e0b", "#10b981", "#06b6d4", "#f97316", ]; export interface StandaloneActivityContent { css?: string; html?: string[]; jsFunctions?: string; jsExpressions?: string[]; } const WEBSANDBOX_STUB = `window.Websandbox = { connection: { remote: { sendPrompt: async () => {}, openLink: async ({ url }) => { if (/^https:/.test(url)) window.open(url, "_blank", "noopener,noreferrer"); } } } };`; function escapeScriptClose(js: string): string { return js.replace(/<\/script/gi, "<\\/script"); } function escapeStyleClose(css: string): string { return css.replace(/<\/style/gi, "<\\/style"); } /** * Wrap an open-generative-ui activity payload in a standalone document that * works when opened in a browser: importmap first, then the same design-system * css composition the live renderer injects, then the generated css, a * Websandbox stub so exported bridge calls degrade gracefully, the joined html * chunks, and finally the generated js in ONE classic (non-module) script: * jsFunctions at top level — so function declarations become window globals, * matching the live websandbox rail where inline onclick="fn()" handlers * resolve them — followed by the jsExpressions inside an async IIFE so they * can use `await` (dynamic import() still resolves via the importmap in * classic scripts). */ export function assembleStandaloneHtmlFromActivity( content: StandaloneActivityContent, title = "generated-widget" ): string { const body = content.html?.join("") ?? ""; const expressions = content.jsExpressions ?? []; const scriptParts = [ ...(content.jsFunctions ? [escapeScriptClose(content.jsFunctions)] : []), ...(expressions.length > 0 ? [ `(async () => { ${expressions.map(escapeScriptClose).join("\n")} })();`, ] : []), ]; const generatedScript = scriptParts.length > 0 ? `` : ""; return ` ${escapeHtml(title)} ${IMPORTMAP_SCRIPT_TAG} ${content.css ? `\n ` : ""} ${body} ${generatedScript} `; } /** * Generate a standalone HTML file that renders a chart using Chart.js from CDN. */ export function chartToStandaloneHtml( type: "bar" | "pie", data: { title: string; description: string; data: Array<{ label: string; value: number }> } ): string { const labels = JSON.stringify(data.data.map((d) => d.label)); const values = JSON.stringify(data.data.map((d) => d.value)); const colors = JSON.stringify( data.data.map((_, i) => CHART_COLORS[i % CHART_COLORS.length]) ); const chartConfig = type === "bar" ? `{ type: 'bar', data: { labels: ${labels}, datasets: [{ data: ${values}, backgroundColor: ${colors}, borderRadius: 4, }] }, options: { responsive: true, plugins: { legend: { display: false }, tooltip: { backgroundColor: '#1f2937', titleColor: '#fff', bodyColor: '#fff', cornerRadius: 8, padding: 10 } }, scales: { x: { grid: { display: false } }, y: { grid: { color: 'rgba(0,0,0,0.06)' } } } } }` : `{ type: 'pie', data: { labels: ${labels}, datasets: [{ data: ${values}, backgroundColor: ${colors}, }] }, options: { responsive: true, plugins: { legend: { position: 'bottom', labels: { padding: 16, usePointStyle: true } }, tooltip: { backgroundColor: '#1f2937', titleColor: '#fff', bodyColor: '#fff', cornerRadius: 8, padding: 10 } } } }`; return ` ${escapeHtml(data.title)}

${escapeHtml(data.title)}

${escapeHtml(data.description)}

`; } export function slugify(text: string): string { return text .toLowerCase() .replace(/[^a-z0-9]+/g, "-") .replace(/^-+|-+$/g, ""); } export function triggerDownload(htmlString: string, filename: string): void { const blob = new Blob([htmlString], { type: "text/html" }); const url = URL.createObjectURL(blob); const a = document.createElement("a"); a.href = url; a.download = filename; document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(url); } function escapeHtml(text: string): string { return text .replace(/&/g, "&") .replace(//g, ">") .replace(/"/g, """); }