diff --git a/apps/agent/skills/advanced-visualization/SKILL.md b/apps/agent/skills/advanced-visualization/SKILL.md index f40735e..3ee90df 100644 --- a/apps/agent/skills/advanced-visualization/SKILL.md +++ b/apps/agent/skills/advanced-visualization/SKILL.md @@ -668,7 +668,15 @@ Only these CDN origins work (CSP-enforced): ``` -**Three.js** (3D graphics): +**Three.js** (3D graphics) — use ES module import (import map resolves bare specifiers): +```html + +``` +Alternative UMD (global `THREE` variable): ```html ``` diff --git a/apps/app/src/components/generative-ui/save-template-overlay.tsx b/apps/app/src/components/generative-ui/save-template-overlay.tsx index 2606ec5..d1fbb15 100644 --- a/apps/app/src/components/generative-ui/save-template-overlay.tsx +++ b/apps/app/src/components/generative-ui/save-template-overlay.tsx @@ -1,6 +1,6 @@ "use client"; -import { useState, useCallback, useEffect, useMemo, type ReactNode } from "react"; +import { useState, useCallback, useMemo, useRef, type ReactNode } from "react"; import { useAgent } from "@copilotkit/react-core/v2"; import { SEED_TEMPLATES } from "@/components/template-library/seed-templates"; @@ -35,29 +35,28 @@ export function SaveTemplateOverlay({ const [saveState, setSaveState] = useState("idle"); const [templateName, setTemplateName] = useState(""); - // Capture pending_template once — it may be cleared by the agent later. - // Syncs external agent state into local state (legitimate effect-based setState). + // Capture pending_template at mount time — it may be cleared by the agent later. + // Uses ref (not state) to avoid an async re-render that would shift sibling positions + // and cause React to remount the iframe, losing rendered 3D/canvas content. const pending = agent.state?.pending_template as { id: string; name: string } | null | undefined; - const [capturedSource, setCapturedSource] = useState<{ id: string; name: string } | null>(null); - useEffect(() => { - if (pending?.id) { - // eslint-disable-next-line react-hooks/set-state-in-effect -- one-time capture of external agent state - setCapturedSource((prev) => prev ?? pending); - } - }, [pending]); + const sourceRef = useRef<{ id: string; name: string } | null>(null); + // eslint-disable-next-line react-hooks/refs -- one-time ref init during render (React-endorsed pattern) + if (pending?.id && !sourceRef.current) { + sourceRef.current = pending; // eslint-disable-line react-hooks/refs + } // Check if this content matches an existing template: // 1. Exact HTML match (seed templates rendered as-is) // 2. Source template captured from pending_template (applied templates with modified data) const matchedTemplate = useMemo(() => { // First check source template from apply flow - if (capturedSource) { + if (sourceRef.current) { // eslint-disable-line react-hooks/refs const allTemplates = [ ...SEED_TEMPLATES, ...((agent.state?.templates as { id: string; name: string }[]) || []), ]; - const found = allTemplates.find((t) => t.id === capturedSource.id); - if (found) return found; + const source = allTemplates.find((t) => t.id === sourceRef.current!.id); // eslint-disable-line react-hooks/refs + if (source) return source; } // Then check exact HTML match if (!html) return null; @@ -68,7 +67,7 @@ export function SaveTemplateOverlay({ ...((agent.state?.templates as { id: string; name: string; html: string }[]) || []), ]; return allTemplates.find((t) => t.html && normalise(t.html) === norm) ?? null; - }, [html, agent.state?.templates, capturedSource]); + }, [html, agent.state?.templates]); const handleSave = useCallback(() => { const name = templateName.trim() || title || "Untitled Template"; diff --git a/apps/app/src/components/generative-ui/widget-renderer.tsx b/apps/app/src/components/generative-ui/widget-renderer.tsx index 6a64a1a..c8bd429 100644 --- a/apps/app/src/components/generative-ui/widget-renderer.tsx +++ b/apps/app/src/components/generative-ui/widget-renderer.tsx @@ -428,7 +428,13 @@ window.addEventListener('message', function(e) { content.setAttribute('data-exec-' + hash, '1'); try { var newScript = document.createElement('script'); - if (scriptInfo.type) newScript.type = scriptInfo.type; + // Auto-detect ES module syntax: if the script contains import/export + // statements but lacks type="module", promote it so the import map applies. + var effectiveType = scriptInfo.type; + if (!effectiveType && scriptInfo.text && /\\b(import\\s|export\\s|import\\()/.test(scriptInfo.text)) { + effectiveType = 'module'; + } + if (effectiveType) newScript.type = effectiveType; if (scriptInfo.src) { newScript.src = scriptInfo.src; newScript.onload = function() { runScripts(scripts, idx + 1); }; @@ -471,6 +477,20 @@ function assembleShell(initialHtml: string = ""): string { + setLoaded(true)} style={{