import { computed, defineComponent, h, onBeforeUnmount, ref, watch, type PropType, } from "vue"; import { z } from "zod"; import { ToolCallStatus } from "@copilotkit/core"; import { processPartialHtml, extractCompleteStyles, } from "../lib/processPartialHtml"; import { useSandboxFunctions } from "../providers/SandboxFunctionsContext"; export const OpenGenerativeUIActivityType = "open-generative-ui"; export const OpenGenerativeUIContentSchema = z.object({ initialHeight: z.number().optional(), generating: z.boolean().optional(), css: z.string().optional(), cssComplete: z.boolean().optional(), html: z.array(z.string()).optional(), htmlComplete: z.boolean().optional(), jsFunctions: z.string().optional(), jsFunctionsComplete: z.boolean().optional(), jsExpressions: z.array(z.string()).optional(), jsExpressionsComplete: z.boolean().optional(), }); export type OpenGenerativeUIContent = z.infer< typeof OpenGenerativeUIContentSchema >; export const GenerateSandboxedUiArgsSchema = z.object({ initialHeight: z.number().optional(), placeholderMessages: z.array(z.string()).optional(), css: z.string().optional(), html: z.string().optional(), jsFunctions: z.string().optional(), jsExpressions: z.array(z.string()).optional(), }); export type GenerateSandboxedUiArgs = z.infer< typeof GenerateSandboxedUiArgsSchema >; function shouldFlushImmediately( previous: OpenGenerativeUIContent | null, next: OpenGenerativeUIContent, ): boolean { if (next.cssComplete && (!previous || !previous.cssComplete)) return true; if (next.htmlComplete) return true; if (next.generating === false) return true; if (next.jsFunctions && (!previous || !previous.jsFunctions)) return true; if ( (next.jsExpressions?.length ?? 0) > (previous?.jsExpressions?.length ?? 0) ) return true; if (next.html?.length && (!previous || !previous.html?.length)) return true; return false; } function ensureHead(html: string): string { if (/]/i.test(html)) return html; return `${html}`; } function injectCssIntoHtml(html: string, css: string): string { const headCloseIdx = html.indexOf(""); if (headCloseIdx !== -1) { return ( html.slice(0, headCloseIdx) + `` + html.slice(headCloseIdx) ); } return `${html}`; } type SandboxInstance = { iframe: HTMLIFrameElement; promise: Promise; run: (code: string | Function) => Promise; destroy: () => void; }; type WebsandboxModule = { create: ( localApi: Record, options: { frameContainer: HTMLElement; frameContent: string; allowAdditionalAttributes: string; }, ) => SandboxInstance; }; async function loadWebsandbox(): Promise { const mod = (await import("@jetbrains/websandbox")) as any; return (mod.default?.default ?? mod.default) as WebsandboxModule; } export const OpenGenerativeUIRenderer = defineComponent({ name: "OpenGenerativeUIRenderer", props: { content: { type: Object as PropType, required: true, }, }, setup(props) { const sandboxFunctions = useSandboxFunctions(); const containerRef = ref(null); const throttledContent = ref(props.content); const latestContent = ref(props.content); const throttleTimer = ref(null); const sandboxRef = ref(null); const previewSandboxRef = ref(null); const sandboxReady = ref(false); const previewReady = ref(false); const executedExpressionIndex = ref(0); const jsFunctionsInjected = ref(false); const pendingQueue = ref([]); const localApi = computed(() => { const api: Record = {}; for (const fn of sandboxFunctions.value) { api[fn.name] = fn.handler; } return api; }); watch( () => props.content, (next) => { latestContent.value = next; if (shouldFlushImmediately(throttledContent.value, next)) { if (throttleTimer.value !== null) { window.clearTimeout(throttleTimer.value); throttleTimer.value = null; } throttledContent.value = next; return; } if (throttleTimer.value === null) { throttleTimer.value = window.setTimeout(() => { throttledContent.value = latestContent.value; throttleTimer.value = null; }, 1000); } }, { immediate: true }, ); onBeforeUnmount(() => { if (throttleTimer.value !== null) { window.clearTimeout(throttleTimer.value); } }); const partialHtml = computed(() => { if (throttledContent.value.htmlComplete) return undefined; if (!throttledContent.value.html?.length) return undefined; return throttledContent.value.html.join(""); }); const previewBody = computed(() => partialHtml.value ? processPartialHtml(partialHtml.value) : undefined, ); const previewStyles = computed(() => partialHtml.value ? extractCompleteStyles(partialHtml.value) : "", ); const fullHtml = computed(() => throttledContent.value.htmlComplete && throttledContent.value.html?.length ? throttledContent.value.html.join("") : undefined, ); const css = computed(() => throttledContent.value.cssComplete ? throttledContent.value.css : undefined, ); const hasPreview = computed( () => !!throttledContent.value.cssComplete && !!previewBody.value && previewBody.value.trim().length > 0, ); const hasVisibleSandbox = computed( () => !!fullHtml.value || hasPreview.value, ); const resolvedHeight = computed( () => `${throttledContent.value.initialHeight ?? 200}px`, ); const destroyPreview = () => { if (previewSandboxRef.value) { previewSandboxRef.value.destroy(); previewSandboxRef.value = null; } previewReady.value = false; }; const destroyFinal = () => { if (sandboxRef.value) { sandboxRef.value.destroy(); sandboxRef.value = null; } sandboxReady.value = false; pendingQueue.value = []; executedExpressionIndex.value = 0; jsFunctionsInjected.value = false; }; watch( [ hasPreview, fullHtml, css, previewBody, previewStyles, () => containerRef.value, ], async ([previewVisible, htmlComplete], _previous, onCleanup) => { if (!previewVisible || htmlComplete || !containerRef.value) return; if (previewSandboxRef.value) return; let cancelled = false; try { const Websandbox = await loadWebsandbox(); if (cancelled || !containerRef.value) return; const sandbox = Websandbox.create( {}, { frameContainer: containerRef.value, frameContent: "", allowAdditionalAttributes: "", }, ); previewSandboxRef.value = sandbox; sandbox.iframe.setAttribute( "data-testid", "open-generative-ui-preview-sandbox", ); sandbox.iframe.style.width = "100%"; sandbox.iframe.style.height = "100%"; sandbox.iframe.style.border = "none"; sandbox.iframe.style.backgroundColor = "transparent"; sandbox.promise.then(() => { if (cancelled || !previewSandboxRef.value) return; previewReady.value = true; void sandbox.run( "var s=document.createElement('style');s.textContent='html, body { overflow: hidden !important; }';document.head.appendChild(s);", ); }); } catch (error) { console.error( "[OpenGenerativeUI] Failed to load sandbox module:", error, ); } onCleanup(() => { cancelled = true; }); }, { immediate: true }, ); watch( [previewBody, previewStyles, css, previewReady], ([body, styles, cssText, ready]) => { if (!previewSandboxRef.value || !ready) return; const headParts: string[] = []; if (cssText) headParts.push(``); if (styles) headParts.push(styles); if (headParts.length) { void previewSandboxRef.value.run( `document.head.innerHTML = ${JSON.stringify(headParts.join(""))}`, ); } if (body) { void previewSandboxRef.value.run( `document.body.innerHTML = ${JSON.stringify(body)}`, ); } }, { immediate: true }, ); watch( [fullHtml, css, localApi, () => containerRef.value], async ([html, cssText, api], _previous, onCleanup) => { destroyFinal(); if (!html || !containerRef.value) return; destroyPreview(); let cancelled = false; try { const Websandbox = await loadWebsandbox(); if (cancelled || !containerRef.value) return; const htmlWithHead = ensureHead(html); const htmlWithCss = cssText ? injectCssIntoHtml(htmlWithHead, cssText) : htmlWithHead; const sandbox = Websandbox.create(api, { frameContainer: containerRef.value, frameContent: htmlWithCss, allowAdditionalAttributes: "", }); sandboxRef.value = sandbox; sandbox.iframe.setAttribute( "data-testid", "open-generative-ui-final-sandbox", ); sandbox.iframe.style.width = "100%"; sandbox.iframe.style.height = "100%"; sandbox.iframe.style.border = "none"; sandbox.iframe.style.backgroundColor = "transparent"; sandbox.promise.then(() => { if (cancelled || !sandboxRef.value) return; sandboxReady.value = true; void sandbox.run( "var s=document.createElement('style');s.textContent='html, body { overflow: hidden !important; }';document.head.appendChild(s);", ); const functionsCode = throttledContent.value.jsFunctions; if (functionsCode && !jsFunctionsInjected.value) { jsFunctionsInjected.value = true; pendingQueue.value.unshift(functionsCode); } const expressions = throttledContent.value.jsExpressions; if (expressions?.length) { const startIndex = executedExpressionIndex.value; if (startIndex < expressions.length) { pendingQueue.value.push(...expressions.slice(startIndex)); executedExpressionIndex.value = expressions.length; } } const queue = [...pendingQueue.value]; pendingQueue.value = []; for (const code of queue) { void sandbox.run(code); } }); } catch (error) { console.error( "[OpenGenerativeUI] Failed to load sandbox module:", error, ); } onCleanup(() => { cancelled = true; }); }, { immediate: true }, ); watch( () => throttledContent.value.jsFunctions, (functionsCode) => { if (!functionsCode || jsFunctionsInjected.value) return; jsFunctionsInjected.value = true; if (sandboxReady.value && sandboxRef.value) { void sandboxRef.value.run(functionsCode); } else { pendingQueue.value.push(functionsCode); } }, ); watch( () => throttledContent.value.jsExpressions, (expressions) => { if (!expressions?.length) return; const startIndex = executedExpressionIndex.value; if (startIndex >= expressions.length) return; const newExpressions = expressions.slice(startIndex); executedExpressionIndex.value = expressions.length; if (sandboxReady.value && sandboxRef.value) { for (const expression of newExpressions) { void sandboxRef.value.run(expression); } } else { pendingQueue.value.push(...newExpressions); } }, { deep: true }, ); const isGenerating = computed( () => throttledContent.value.generating !== false, ); watch( [hasPreview, fullHtml], ([previewVisible, html]) => { if (html) destroyPreview(); if (!previewVisible) destroyPreview(); }, { immediate: true }, ); onBeforeUnmount(() => { if (throttleTimer.value !== null) { window.clearTimeout(throttleTimer.value); } destroyPreview(); destroyFinal(); }); return () => h( "div", { ref: containerRef, "data-testid": "open-generative-ui-renderer", style: { position: "relative", width: "100%", height: resolvedHeight.value, borderRadius: "8px", backgroundColor: hasVisibleSandbox.value ? "transparent" : "#f5f5f5", border: hasVisibleSandbox.value ? "none" : "1px solid #e0e0e0", overflow: "hidden", display: hasVisibleSandbox.value ? "block" : "flex", alignItems: hasVisibleSandbox.value ? undefined : "center", justifyContent: hasVisibleSandbox.value ? undefined : "center", }, }, [ !hasVisibleSandbox.value ? h("div", { "data-testid": "open-generative-ui-placeholder" }, [ h( "svg", { width: "48", height: "48", viewBox: "0 0 24 24", fill: "none", style: { animation: "ck-spin 1s linear infinite" }, }, [ h("circle", { cx: "12", cy: "12", r: "10", stroke: "#e0e0e0", "stroke-width": "3", }), h("path", { d: "M12 2a10 10 0 0 1 10 10", stroke: "#999", "stroke-width": "3", "stroke-linecap": "round", }), ], ), h( "style", "@keyframes ck-spin { to { transform: rotate(360deg) } }", ), ]) : null, isGenerating.value ? h("div", { "data-testid": "open-generative-ui-progress-overlay", style: { position: "absolute", inset: 0, backgroundColor: "rgba(255,255,255,0.45)", }, }) : null, ], ); }, }); export const OpenGenerativeUIActivityRenderer = defineComponent({ name: "OpenGenerativeUIActivityRenderer", props: { activityType: { type: String, required: true }, content: { type: Object as PropType, required: true, }, message: { type: Object as PropType, required: true }, agent: { type: Object as PropType, required: false, default: undefined, }, }, setup(props) { return () => h(OpenGenerativeUIRenderer, { content: props.content }); }, }); export const OpenGenerativeUIToolRenderer = defineComponent({ name: "OpenGenerativeUIToolRenderer", props: { name: { type: String, required: true }, args: { type: Object as PropType>, required: true, }, status: { type: String as PropType, required: true }, result: { type: String as PropType, required: false }, }, setup(props) { const visibleMessageIndex = ref(0); watch( () => props.args.placeholderMessages, (messages, _, onCleanup) => { if (!messages?.length || props.status === ToolCallStatus.Complete) return; visibleMessageIndex.value = Math.max(messages.length - 1, 0); const timer = window.setInterval(() => { visibleMessageIndex.value = (visibleMessageIndex.value + 1) % Math.max(messages.length, 1); }, 5000); onCleanup(() => window.clearInterval(timer)); }, { immediate: true }, ); return () => { if (props.status === ToolCallStatus.Complete) return null; const messages = props.args.placeholderMessages; if (!messages?.length) return null; const currentMessage = messages[visibleMessageIndex.value] ?? messages[0]; return h( "div", { style: { padding: "8px 12px", color: "#999", fontSize: "14px" }, "data-testid": "open-generative-ui-tool-placeholder", }, currentMessage, ); }; }, });