mirror of
https://github.com/getcompanion-ai/co-mono.git
synced 2026-04-21 15:01:26 +00:00
Fix sandboxed iframe.
This commit is contained in:
parent
faefc63309
commit
af426d2682
5 changed files with 312 additions and 193 deletions
|
|
@ -1,70 +0,0 @@
|
||||||
import { LitElement } from "lit";
|
|
||||||
import { customElement, property } from "lit/decorators.js";
|
|
||||||
|
|
||||||
// @ts-ignore - browser global exists in Firefox
|
|
||||||
declare const browser: any;
|
|
||||||
|
|
||||||
@customElement("sandbox-iframe")
|
|
||||||
export class SandboxIframe extends LitElement {
|
|
||||||
@property() content = "";
|
|
||||||
private iframe?: HTMLIFrameElement;
|
|
||||||
|
|
||||||
createRenderRoot() {
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
override connectedCallback() {
|
|
||||||
super.connectedCallback();
|
|
||||||
window.addEventListener("message", this.handleMessage);
|
|
||||||
this.createIframe();
|
|
||||||
}
|
|
||||||
|
|
||||||
override disconnectedCallback() {
|
|
||||||
super.disconnectedCallback();
|
|
||||||
window.removeEventListener("message", this.handleMessage);
|
|
||||||
this.iframe?.remove();
|
|
||||||
}
|
|
||||||
|
|
||||||
private handleMessage = (e: MessageEvent) => {
|
|
||||||
if (e.data.type === "sandbox-ready" && e.source === this.iframe?.contentWindow) {
|
|
||||||
// Sandbox is ready, send content
|
|
||||||
this.iframe?.contentWindow?.postMessage(
|
|
||||||
{
|
|
||||||
type: "loadContent",
|
|
||||||
content: this.content,
|
|
||||||
artifactId: "test",
|
|
||||||
attachments: [],
|
|
||||||
},
|
|
||||||
"*",
|
|
||||||
);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
private createIframe() {
|
|
||||||
this.iframe = document.createElement("iframe");
|
|
||||||
this.iframe.sandbox.add("allow-scripts");
|
|
||||||
this.iframe.sandbox.add("allow-modals");
|
|
||||||
this.iframe.style.width = "100%";
|
|
||||||
this.iframe.style.height = "100%";
|
|
||||||
this.iframe.style.border = "none";
|
|
||||||
|
|
||||||
const isFirefox = typeof browser !== "undefined" && browser.runtime !== undefined;
|
|
||||||
if (isFirefox) {
|
|
||||||
this.iframe.src = browser.runtime.getURL("sandbox.html");
|
|
||||||
} else {
|
|
||||||
this.iframe.src = chrome.runtime.getURL("sandbox.html");
|
|
||||||
}
|
|
||||||
|
|
||||||
this.appendChild(this.iframe);
|
|
||||||
}
|
|
||||||
|
|
||||||
public updateContent(newContent: string) {
|
|
||||||
this.content = newContent;
|
|
||||||
// Recreate iframe for clean state
|
|
||||||
if (this.iframe) {
|
|
||||||
this.iframe.remove();
|
|
||||||
this.iframe = undefined;
|
|
||||||
}
|
|
||||||
this.createIframe();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
271
packages/browser-extension/src/components/SandboxedIframe.ts
Normal file
271
packages/browser-extension/src/components/SandboxedIframe.ts
Normal file
|
|
@ -0,0 +1,271 @@
|
||||||
|
import { LitElement } from "lit";
|
||||||
|
import { customElement, property } from "lit/decorators.js";
|
||||||
|
import type { Attachment } from "../utils/attachment-utils.js";
|
||||||
|
|
||||||
|
// @ts-ignore - browser global exists in Firefox
|
||||||
|
declare const browser: any;
|
||||||
|
|
||||||
|
@customElement("sandbox-iframe")
|
||||||
|
export class SandboxIframe extends LitElement {
|
||||||
|
@property() content = "";
|
||||||
|
@property() artifactId = "";
|
||||||
|
@property({ attribute: false }) attachments: Attachment[] = [];
|
||||||
|
|
||||||
|
private iframe?: HTMLIFrameElement;
|
||||||
|
private logs: Array<{ type: "log" | "error"; text: string }> = [];
|
||||||
|
|
||||||
|
createRenderRoot() {
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
override connectedCallback() {
|
||||||
|
super.connectedCallback();
|
||||||
|
window.addEventListener("message", this.handleMessage);
|
||||||
|
this.createIframe();
|
||||||
|
}
|
||||||
|
|
||||||
|
override disconnectedCallback() {
|
||||||
|
super.disconnectedCallback();
|
||||||
|
window.removeEventListener("message", this.handleMessage);
|
||||||
|
this.iframe?.remove();
|
||||||
|
}
|
||||||
|
|
||||||
|
private handleMessage = (e: MessageEvent) => {
|
||||||
|
// Handle sandbox-ready message
|
||||||
|
if (e.data.type === "sandbox-ready" && e.source === this.iframe?.contentWindow) {
|
||||||
|
// Sandbox is ready, inject our runtime and send content
|
||||||
|
const enhancedContent = this.injectRuntimeScripts(this.content);
|
||||||
|
this.iframe?.contentWindow?.postMessage(
|
||||||
|
{
|
||||||
|
type: "loadContent",
|
||||||
|
content: enhancedContent,
|
||||||
|
artifactId: this.artifactId,
|
||||||
|
attachments: this.attachments,
|
||||||
|
},
|
||||||
|
"*",
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only handle messages for this artifact
|
||||||
|
if (e.data.artifactId !== this.artifactId) return;
|
||||||
|
|
||||||
|
// Handle console messages
|
||||||
|
if (e.data.type === "console") {
|
||||||
|
const log = {
|
||||||
|
type: e.data.method === "error" ? ("error" as const) : ("log" as const),
|
||||||
|
text: e.data.text,
|
||||||
|
};
|
||||||
|
this.logs.push(log);
|
||||||
|
this.dispatchEvent(
|
||||||
|
new CustomEvent("console", {
|
||||||
|
detail: log,
|
||||||
|
bubbles: true,
|
||||||
|
composed: true,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
} else if (e.data.type === "execution-complete") {
|
||||||
|
// Store final logs
|
||||||
|
this.logs = e.data.logs || [];
|
||||||
|
this.dispatchEvent(
|
||||||
|
new CustomEvent("execution-complete", {
|
||||||
|
detail: { logs: this.logs },
|
||||||
|
bubbles: true,
|
||||||
|
composed: true,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Force reflow when iframe content is ready
|
||||||
|
if (this.iframe) {
|
||||||
|
this.iframe.style.display = "none";
|
||||||
|
this.iframe.offsetHeight; // Force reflow
|
||||||
|
this.iframe.style.display = "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
private injectRuntimeScripts(htmlContent: string): string {
|
||||||
|
// Define the runtime function that will be injected
|
||||||
|
const runtimeFunction = (artifactId: string, attachments: any[]) => {
|
||||||
|
// @ts-ignore - window extensions
|
||||||
|
window.__artifactLogs = [];
|
||||||
|
const originalConsole = {
|
||||||
|
log: console.log,
|
||||||
|
error: console.error,
|
||||||
|
warn: console.warn,
|
||||||
|
info: console.info,
|
||||||
|
};
|
||||||
|
|
||||||
|
["log", "error", "warn", "info"].forEach((method) => {
|
||||||
|
// @ts-ignore
|
||||||
|
console[method] = (...args: any[]) => {
|
||||||
|
const text = args
|
||||||
|
.map((arg: any) => {
|
||||||
|
try {
|
||||||
|
return typeof arg === "object" ? JSON.stringify(arg) : String(arg);
|
||||||
|
} catch {
|
||||||
|
return String(arg);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.join(" ");
|
||||||
|
// @ts-ignore
|
||||||
|
window.__artifactLogs.push({ type: method === "error" ? "error" : "log", text });
|
||||||
|
window.parent.postMessage(
|
||||||
|
{
|
||||||
|
type: "console",
|
||||||
|
method,
|
||||||
|
text,
|
||||||
|
artifactId,
|
||||||
|
},
|
||||||
|
"*",
|
||||||
|
);
|
||||||
|
// @ts-ignore
|
||||||
|
originalConsole[method].apply(console, args);
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
window.addEventListener("error", (e: ErrorEvent) => {
|
||||||
|
const text = e.message + " at line " + e.lineno + ":" + e.colno;
|
||||||
|
// @ts-ignore
|
||||||
|
window.__artifactLogs.push({ type: "error", text });
|
||||||
|
window.parent.postMessage(
|
||||||
|
{
|
||||||
|
type: "console",
|
||||||
|
method: "error",
|
||||||
|
text,
|
||||||
|
artifactId,
|
||||||
|
},
|
||||||
|
"*",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
window.addEventListener("unhandledrejection", (e: PromiseRejectionEvent) => {
|
||||||
|
const text = "Unhandled promise rejection: " + (e.reason?.message || e.reason || "Unknown error");
|
||||||
|
// @ts-ignore
|
||||||
|
window.__artifactLogs.push({ type: "error", text });
|
||||||
|
window.parent.postMessage(
|
||||||
|
{
|
||||||
|
type: "console",
|
||||||
|
method: "error",
|
||||||
|
text,
|
||||||
|
artifactId,
|
||||||
|
},
|
||||||
|
"*",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Attachment helpers
|
||||||
|
// @ts-ignore
|
||||||
|
window.attachments = attachments;
|
||||||
|
// @ts-ignore
|
||||||
|
window.listFiles = () => {
|
||||||
|
// @ts-ignore
|
||||||
|
return (window.attachments || []).map((a: any) => ({
|
||||||
|
id: a.id,
|
||||||
|
fileName: a.fileName,
|
||||||
|
mimeType: a.mimeType,
|
||||||
|
size: a.size,
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
// @ts-ignore
|
||||||
|
window.readTextFile = (attachmentId: string) => {
|
||||||
|
// @ts-ignore
|
||||||
|
const a = (window.attachments || []).find((x: any) => x.id === attachmentId);
|
||||||
|
if (!a) throw new Error("Attachment not found: " + attachmentId);
|
||||||
|
if (a.extractedText) return a.extractedText;
|
||||||
|
try {
|
||||||
|
return atob(a.content);
|
||||||
|
} catch {
|
||||||
|
throw new Error("Failed to decode text content for: " + attachmentId);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
// @ts-ignore
|
||||||
|
window.readBinaryFile = (attachmentId: string) => {
|
||||||
|
// @ts-ignore
|
||||||
|
const a = (window.attachments || []).find((x: any) => x.id === attachmentId);
|
||||||
|
if (!a) throw new Error("Attachment not found: " + attachmentId);
|
||||||
|
const bin = atob(a.content);
|
||||||
|
const bytes = new Uint8Array(bin.length);
|
||||||
|
for (let i = 0; i < bin.length; i++) bytes[i] = bin.charCodeAt(i);
|
||||||
|
return bytes;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Send completion after 2 seconds
|
||||||
|
const sendCompletion = () => {
|
||||||
|
window.parent.postMessage(
|
||||||
|
{
|
||||||
|
type: "execution-complete",
|
||||||
|
// @ts-ignore
|
||||||
|
logs: window.__artifactLogs || [],
|
||||||
|
artifactId,
|
||||||
|
},
|
||||||
|
"*",
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
if (document.readyState === "complete" || document.readyState === "interactive") {
|
||||||
|
setTimeout(sendCompletion, 2000);
|
||||||
|
} else {
|
||||||
|
window.addEventListener("load", () => {
|
||||||
|
setTimeout(sendCompletion, 2000);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Convert function to string and wrap in IIFE with parameters
|
||||||
|
const runtimeScript = `
|
||||||
|
<script>
|
||||||
|
(${runtimeFunction.toString()})(${JSON.stringify(this.artifactId)}, ${JSON.stringify(this.attachments)});
|
||||||
|
</script>
|
||||||
|
`;
|
||||||
|
|
||||||
|
// Inject at start of <head> or start of document
|
||||||
|
const headMatch = htmlContent.match(/<head[^>]*>/i);
|
||||||
|
if (headMatch) {
|
||||||
|
const index = headMatch.index! + headMatch[0].length;
|
||||||
|
return htmlContent.slice(0, index) + runtimeScript + htmlContent.slice(index);
|
||||||
|
}
|
||||||
|
|
||||||
|
const htmlMatch = htmlContent.match(/<html[^>]*>/i);
|
||||||
|
if (htmlMatch) {
|
||||||
|
const index = htmlMatch.index! + htmlMatch[0].length;
|
||||||
|
return htmlContent.slice(0, index) + runtimeScript + htmlContent.slice(index);
|
||||||
|
}
|
||||||
|
|
||||||
|
return runtimeScript + htmlContent;
|
||||||
|
}
|
||||||
|
|
||||||
|
private createIframe() {
|
||||||
|
this.iframe = document.createElement("iframe");
|
||||||
|
this.iframe.sandbox.add("allow-scripts");
|
||||||
|
this.iframe.sandbox.add("allow-modals");
|
||||||
|
this.iframe.style.width = "100%";
|
||||||
|
this.iframe.style.height = "100%";
|
||||||
|
this.iframe.style.border = "none";
|
||||||
|
|
||||||
|
const isFirefox = typeof browser !== "undefined" && browser.runtime !== undefined;
|
||||||
|
if (isFirefox) {
|
||||||
|
this.iframe.src = browser.runtime.getURL("sandbox.html");
|
||||||
|
} else {
|
||||||
|
this.iframe.src = chrome.runtime.getURL("sandbox.html");
|
||||||
|
}
|
||||||
|
|
||||||
|
this.appendChild(this.iframe);
|
||||||
|
}
|
||||||
|
|
||||||
|
public updateContent(newContent: string) {
|
||||||
|
this.content = newContent;
|
||||||
|
// Clear logs for new content
|
||||||
|
this.logs = [];
|
||||||
|
// Recreate iframe for clean state
|
||||||
|
if (this.iframe) {
|
||||||
|
this.iframe.remove();
|
||||||
|
this.iframe = undefined;
|
||||||
|
}
|
||||||
|
this.createIframe();
|
||||||
|
}
|
||||||
|
|
||||||
|
public getLogs(): Array<{ type: "log" | "error"; text: string }> {
|
||||||
|
return this.logs;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -68,7 +68,7 @@ const originalConsole = {
|
||||||
|
|
||||||
// Error handlers
|
// Error handlers
|
||||||
window.addEventListener("error", (e) => {
|
window.addEventListener("error", (e) => {
|
||||||
const text = e.message + " at line " + e.lineno + ":" + e.colno;
|
const text = (e.error?.stack || e.message || String(e)) + " at line " + (e.lineno || "?") + ":" + (e.colno || "?");
|
||||||
window.__artifactLogs.push({ type: "error", text });
|
window.__artifactLogs.push({ type: "error", text });
|
||||||
window.parent.postMessage(
|
window.parent.postMessage(
|
||||||
{
|
{
|
||||||
|
|
@ -79,6 +79,7 @@ window.addEventListener("error", (e) => {
|
||||||
},
|
},
|
||||||
"*",
|
"*",
|
||||||
);
|
);
|
||||||
|
return false;
|
||||||
});
|
});
|
||||||
|
|
||||||
window.addEventListener("unhandledrejection", (e) => {
|
window.addEventListener("unhandledrejection", (e) => {
|
||||||
|
|
@ -154,7 +155,7 @@ window.addEventListener("message", (event) => {
|
||||||
"});\n\n" +
|
"});\n\n" +
|
||||||
"// Error handlers\n" +
|
"// Error handlers\n" +
|
||||||
"window.addEventListener('error', (e) => {\n" +
|
"window.addEventListener('error', (e) => {\n" +
|
||||||
" const text = e.message + ' at line ' + e.lineno + ':' + e.colno;\n" +
|
" const text = (e.error?.stack || e.message || String(e)) + ' at line ' + (e.lineno || '?') + ':' + (e.colno || '?');\n" +
|
||||||
" window.__artifactLogs.push({ type: 'error', text });\n" +
|
" window.__artifactLogs.push({ type: 'error', text });\n" +
|
||||||
" window.parent.postMessage({\n" +
|
" window.parent.postMessage({\n" +
|
||||||
" type: 'console',\n" +
|
" type: 'console',\n" +
|
||||||
|
|
@ -162,6 +163,7 @@ window.addEventListener("message", (event) => {
|
||||||
" text,\n" +
|
" text,\n" +
|
||||||
" artifactId: window.__currentArtifactId\n" +
|
" artifactId: window.__currentArtifactId\n" +
|
||||||
" }, '*');\n" +
|
" }, '*');\n" +
|
||||||
|
" return false;\n" +
|
||||||
"});\n\n" +
|
"});\n\n" +
|
||||||
"window.addEventListener('unhandledrejection', (e) => {\n" +
|
"window.addEventListener('unhandledrejection', (e) => {\n" +
|
||||||
" const text = 'Unhandled promise rejection: ' + (e.reason?.message || e.reason || 'Unknown error');\n" +
|
" const text = 'Unhandled promise rejection: ' + (e.reason?.message || e.reason || 'Unknown error');\n" +
|
||||||
|
|
@ -173,8 +175,11 @@ window.addEventListener("message", (event) => {
|
||||||
" artifactId: window.__currentArtifactId\n" +
|
" artifactId: window.__currentArtifactId\n" +
|
||||||
" }, '*');\n" +
|
" }, '*');\n" +
|
||||||
"});\n\n" +
|
"});\n\n" +
|
||||||
"// Send completion when ready\n" +
|
"// Send completion after 2 seconds to collect all logs and errors\n" +
|
||||||
|
"let completionSent = false;\n" +
|
||||||
"const sendCompletion = function() {\n" +
|
"const sendCompletion = function() {\n" +
|
||||||
|
" if (completionSent) return;\n" +
|
||||||
|
" completionSent = true;\n" +
|
||||||
" window.parent.postMessage({\n" +
|
" window.parent.postMessage({\n" +
|
||||||
" type: 'execution-complete',\n" +
|
" type: 'execution-complete',\n" +
|
||||||
" logs: window.__artifactLogs || [],\n" +
|
" logs: window.__artifactLogs || [],\n" +
|
||||||
|
|
@ -182,10 +187,10 @@ window.addEventListener("message", (event) => {
|
||||||
" }, '*');\n" +
|
" }, '*');\n" +
|
||||||
"};\n\n" +
|
"};\n\n" +
|
||||||
"if (document.readyState === 'complete' || document.readyState === 'interactive') {\n" +
|
"if (document.readyState === 'complete' || document.readyState === 'interactive') {\n" +
|
||||||
" setTimeout(sendCompletion, 0);\n" +
|
" setTimeout(sendCompletion, 2000);\n" +
|
||||||
"} else {\n" +
|
"} else {\n" +
|
||||||
" window.addEventListener('DOMContentLoaded', function() {\n" +
|
" window.addEventListener('load', function() {\n" +
|
||||||
" setTimeout(sendCompletion, 0);\n" +
|
" setTimeout(sendCompletion, 2000);\n" +
|
||||||
" });\n" +
|
" });\n" +
|
||||||
"}\n" +
|
"}\n" +
|
||||||
"</" +
|
"</" +
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@ import { FileCode2, Settings } from "lucide";
|
||||||
import "@mariozechner/mini-lit/dist/ThemeToggle.js";
|
import "@mariozechner/mini-lit/dist/ThemeToggle.js";
|
||||||
import "./ChatPanel.js";
|
import "./ChatPanel.js";
|
||||||
import "./live-reload.js";
|
import "./live-reload.js";
|
||||||
import "./components/SandboxIframe.js";
|
import "./components/SandboxedIframe.js";
|
||||||
import type { ChatPanel } from "./ChatPanel.js";
|
import type { ChatPanel } from "./ChatPanel.js";
|
||||||
import { ApiKeysDialog } from "./dialogs/ApiKeysDialog.js";
|
import { ApiKeysDialog } from "./dialogs/ApiKeysDialog.js";
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -4,8 +4,10 @@ import { html } from "lit";
|
||||||
import { customElement, property, state } from "lit/decorators.js";
|
import { customElement, property, state } from "lit/decorators.js";
|
||||||
import { createRef, type Ref, ref } from "lit/directives/ref.js";
|
import { createRef, type Ref, ref } from "lit/directives/ref.js";
|
||||||
import { unsafeHTML } from "lit/directives/unsafe-html.js";
|
import { unsafeHTML } from "lit/directives/unsafe-html.js";
|
||||||
|
import type { SandboxIframe } from "../../components/SandboxedIframe.js";
|
||||||
import type { Attachment } from "../../utils/attachment-utils.js";
|
import type { Attachment } from "../../utils/attachment-utils.js";
|
||||||
import { i18n } from "../../utils/i18n.js";
|
import { i18n } from "../../utils/i18n.js";
|
||||||
|
import "../../components/SandboxedIframe.js";
|
||||||
import { ArtifactElement } from "./ArtifactElement.js";
|
import { ArtifactElement } from "./ArtifactElement.js";
|
||||||
|
|
||||||
@customElement("html-artifact")
|
@customElement("html-artifact")
|
||||||
|
|
@ -15,11 +17,10 @@ export class HtmlArtifact extends ArtifactElement {
|
||||||
@property({ attribute: false }) attachments: Attachment[] = [];
|
@property({ attribute: false }) attachments: Attachment[] = [];
|
||||||
|
|
||||||
private _content = "";
|
private _content = "";
|
||||||
private iframe?: HTMLIFrameElement;
|
|
||||||
private logs: Array<{ type: "log" | "error"; text: string }> = [];
|
private logs: Array<{ type: "log" | "error"; text: string }> = [];
|
||||||
|
|
||||||
// Refs for DOM elements
|
// Refs for DOM elements
|
||||||
private iframeContainerRef: Ref<HTMLDivElement> = createRef();
|
private sandboxIframeRef: Ref<SandboxIframe> = createRef();
|
||||||
private consoleLogsRef: Ref<HTMLDivElement> = createRef();
|
private consoleLogsRef: Ref<HTMLDivElement> = createRef();
|
||||||
private consoleButtonRef: Ref<HTMLButtonElement> = createRef();
|
private consoleButtonRef: Ref<HTMLButtonElement> = createRef();
|
||||||
|
|
||||||
|
|
@ -55,16 +56,16 @@ export class HtmlArtifact extends ArtifactElement {
|
||||||
const oldValue = this._content;
|
const oldValue = this._content;
|
||||||
this._content = value;
|
this._content = value;
|
||||||
if (oldValue !== value) {
|
if (oldValue !== value) {
|
||||||
// Delay to ensure component is rendered
|
|
||||||
requestAnimationFrame(async () => {
|
|
||||||
this.requestUpdate();
|
this.requestUpdate();
|
||||||
await this.updateComplete;
|
// Update sandbox iframe if it exists
|
||||||
this.updateIframe();
|
if (this.sandboxIframeRef.value) {
|
||||||
// Ensure iframe gets attached
|
this.logs = [];
|
||||||
requestAnimationFrame(() => {
|
if (this.consoleLogsRef.value) {
|
||||||
this.attachIframeToContainer();
|
this.consoleLogsRef.value.innerHTML = "";
|
||||||
});
|
}
|
||||||
});
|
this.updateConsoleButton();
|
||||||
|
this.sandboxIframeRef.value.updateContent(value);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -72,60 +73,14 @@ export class HtmlArtifact extends ArtifactElement {
|
||||||
return this._content;
|
return this._content;
|
||||||
}
|
}
|
||||||
|
|
||||||
override connectedCallback() {
|
private handleConsoleEvent = (e: CustomEvent) => {
|
||||||
super.connectedCallback();
|
this.addLog(e.detail);
|
||||||
window.addEventListener("message", this.handleMessage);
|
};
|
||||||
window.addEventListener("message", this.sandboxReadyHandler);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override firstUpdated() {
|
private handleExecutionComplete = (e: CustomEvent) => {
|
||||||
// Create iframe if we have content after first render
|
|
||||||
if (this._content) {
|
|
||||||
this.updateIframe();
|
|
||||||
// Ensure iframe is attached after render completes
|
|
||||||
requestAnimationFrame(() => {
|
|
||||||
this.attachIframeToContainer();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override updated() {
|
|
||||||
// Always try to attach iframe if it exists but isn't in DOM
|
|
||||||
if (this.iframe && !this.iframe.parentElement) {
|
|
||||||
this.attachIframeToContainer();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override disconnectedCallback() {
|
|
||||||
super.disconnectedCallback();
|
|
||||||
window.removeEventListener("message", this.handleMessage);
|
|
||||||
window.removeEventListener("message", this.sandboxReadyHandler);
|
|
||||||
this.iframe?.remove();
|
|
||||||
this.iframe = undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
private handleMessage = (e: MessageEvent) => {
|
|
||||||
// Only handle messages for this artifact
|
|
||||||
if (e.data.artifactId !== this.filename) return;
|
|
||||||
|
|
||||||
if (e.data.type === "console") {
|
|
||||||
this.addLog({
|
|
||||||
type: e.data.method === "error" ? "error" : "log",
|
|
||||||
text: e.data.text,
|
|
||||||
});
|
|
||||||
} else if (e.data.type === "execution-complete") {
|
|
||||||
// Store final logs
|
// Store final logs
|
||||||
this.logs = e.data.logs || [];
|
this.logs = e.detail.logs || [];
|
||||||
this.updateConsoleButton();
|
this.updateConsoleButton();
|
||||||
|
|
||||||
// Force reflow when iframe content is ready
|
|
||||||
// This fixes the 0x0 size issue on initial load
|
|
||||||
if (this.iframe) {
|
|
||||||
this.iframe.style.display = "none";
|
|
||||||
this.iframe.offsetHeight; // Force reflow
|
|
||||||
this.iframe.style.display = "";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
private addLog(log: { type: "log" | "error"; text: string }) {
|
private addLog(log: { type: "log" | "error"; text: string }) {
|
||||||
|
|
@ -155,56 +110,6 @@ export class HtmlArtifact extends ArtifactElement {
|
||||||
button.innerHTML = `<span>${text}</span><span>${this.consoleOpen ? "▼" : "▶"}</span>`;
|
button.innerHTML = `<span>${text}</span><span>${this.consoleOpen ? "▼" : "▶"}</span>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
private updateIframe() {
|
|
||||||
// Clear logs for new content
|
|
||||||
this.logs = [];
|
|
||||||
if (this.consoleLogsRef.value) {
|
|
||||||
this.consoleLogsRef.value.innerHTML = "";
|
|
||||||
}
|
|
||||||
this.updateConsoleButton();
|
|
||||||
|
|
||||||
// Remove and recreate iframe for clean state
|
|
||||||
if (this.iframe) {
|
|
||||||
this.iframe.remove();
|
|
||||||
this.iframe = undefined;
|
|
||||||
}
|
|
||||||
this.createIframe();
|
|
||||||
}
|
|
||||||
|
|
||||||
private sandboxReadyHandler = (e: MessageEvent) => {
|
|
||||||
if (e.data.type === "sandbox-ready" && e.source === this.iframe?.contentWindow) {
|
|
||||||
// Sandbox is ready, send content
|
|
||||||
this.iframe?.contentWindow?.postMessage(
|
|
||||||
{
|
|
||||||
type: "loadContent",
|
|
||||||
content: this._content,
|
|
||||||
artifactId: this.filename,
|
|
||||||
attachments: this.attachments,
|
|
||||||
},
|
|
||||||
"*",
|
|
||||||
);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
private createIframe() {
|
|
||||||
this.iframe = document.createElement("iframe");
|
|
||||||
this.iframe.sandbox.add("allow-scripts");
|
|
||||||
this.iframe.sandbox.add("allow-modals"); // Allow alert, confirm, prompt
|
|
||||||
this.iframe.className = "w-full h-full border-0";
|
|
||||||
this.iframe.title = this.displayTitle || this.filename;
|
|
||||||
this.iframe.src = chrome.runtime.getURL("sandbox.html");
|
|
||||||
this.attachIframeToContainer();
|
|
||||||
}
|
|
||||||
|
|
||||||
private attachIframeToContainer() {
|
|
||||||
if (!this.iframe || !this.iframeContainerRef.value) return;
|
|
||||||
|
|
||||||
// Only append if not already in the container
|
|
||||||
if (this.iframe.parentElement !== this.iframeContainerRef.value) {
|
|
||||||
this.iframeContainerRef.value.appendChild(this.iframe);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private toggleConsole() {
|
private toggleConsole() {
|
||||||
this.consoleOpen = !this.consoleOpen;
|
this.consoleOpen = !this.consoleOpen;
|
||||||
this.requestUpdate();
|
this.requestUpdate();
|
||||||
|
|
@ -237,7 +142,15 @@ export class HtmlArtifact extends ArtifactElement {
|
||||||
<div class="flex-1 overflow-hidden relative">
|
<div class="flex-1 overflow-hidden relative">
|
||||||
<!-- Preview container - always in DOM, just hidden when not active -->
|
<!-- Preview container - always in DOM, just hidden when not active -->
|
||||||
<div class="absolute inset-0 flex flex-col" style="display: ${this.viewMode === "preview" ? "flex" : "none"}">
|
<div class="absolute inset-0 flex flex-col" style="display: ${this.viewMode === "preview" ? "flex" : "none"}">
|
||||||
<div class="flex-1 relative" ${ref(this.iframeContainerRef)}></div>
|
<sandbox-iframe
|
||||||
|
class="flex-1"
|
||||||
|
.content=${this._content}
|
||||||
|
.artifactId=${this.filename}
|
||||||
|
.attachments=${this.attachments}
|
||||||
|
@console=${this.handleConsoleEvent}
|
||||||
|
@execution-complete=${this.handleExecutionComplete}
|
||||||
|
${ref(this.sandboxIframeRef)}
|
||||||
|
></sandbox-iframe>
|
||||||
${
|
${
|
||||||
this.logs.length > 0
|
this.logs.length > 0
|
||||||
? html`
|
? html`
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue