Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 9 additions & 1 deletion apps/agent/skills/advanced-visualization/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -668,7 +668,15 @@ Only these CDN origins work (CSP-enforced):
<script src="https://cdnjs.cloudflare.com/ajax/libs/Chart.js/4.4.1/chart.umd.js"></script>
```

**Three.js** (3D graphics):
**Three.js** (3D graphics) — use ES module import (import map resolves bare specifiers):
```html
<script type="module">
import * as THREE from 'three';
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js';
// ... your Three.js code here
</script>
```
Alternative UMD (global `THREE` variable):
```html
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
```
Expand Down
27 changes: 13 additions & 14 deletions apps/app/src/components/generative-ui/save-template-overlay.tsx
Original file line number Diff line number Diff line change
@@ -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";

Expand Down Expand Up @@ -35,29 +35,28 @@ export function SaveTemplateOverlay({
const [saveState, setSaveState] = useState<SaveState>("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;
Expand All @@ -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";
Expand Down
31 changes: 27 additions & 4 deletions apps/app/src/components/generative-ui/widget-renderer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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); };
Expand Down Expand Up @@ -471,6 +477,20 @@ function assembleShell(initialHtml: string = ""): string {
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<script type="importmap">
{
"imports": {
"three": "https://esm.sh/three",
"three/": "https://esm.sh/three/",
"gsap": "https://esm.sh/gsap",
"gsap/": "https://esm.sh/gsap/",
"d3": "https://esm.sh/d3",
"d3/": "https://esm.sh/d3/",
"chart.js": "https://esm.sh/chart.js",
"chart.js/": "https://esm.sh/chart.js/"
}
}
</script>
<meta http-equiv="Content-Security-Policy" content="
default-src 'self';
script-src 'unsafe-inline' 'unsafe-eval'
Expand Down Expand Up @@ -590,8 +610,8 @@ export function WidgetRenderer({ title, description, html }: WidgetRendererProps

const iframe = iframeRef.current;
if (iframe.contentWindow) {
// targetOrigin "*" is required: the sandboxed iframe (allow-scripts only,
// no allow-same-origin) has a null origin, so no specific origin can be used.
// targetOrigin "*" is required: sandboxed iframes may have a null origin
// depending on browser, so no specific origin can be used.
iframe.contentWindow.postMessage(
{ type: "update-content", html },
"*"
Expand Down Expand Up @@ -680,7 +700,10 @@ export function WidgetRenderer({ title, description, html }: WidgetRendererProps
content streamed via postMessage for progressive rendering. */}
<iframe
ref={iframeRef}
sandbox="allow-scripts"
// allow-same-origin is required for import maps to work in srcdoc iframes.
// Safe here because no auth/session data is exposed client-side.
// See: https://github.com/CopilotKit/OpenGenerativeUI/issues/3
sandbox="allow-scripts allow-same-origin"
className="w-full border-0"
onLoad={() => setLoaded(true)}
style={{
Expand Down
Loading