@mariozechner/pi-web-ui 0.5.44
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +252 -0
- package/dist/ChatPanel.d.ts +23 -0
- package/dist/ChatPanel.d.ts.map +1 -0
- package/dist/ChatPanel.js +224 -0
- package/dist/ChatPanel.js.map +1 -0
- package/dist/app.css +2 -0
- package/dist/components/AgentInterface.d.ts +35 -0
- package/dist/components/AgentInterface.d.ts.map +1 -0
- package/dist/components/AgentInterface.js +308 -0
- package/dist/components/AgentInterface.js.map +1 -0
- package/dist/components/AttachmentTile.d.ts +12 -0
- package/dist/components/AttachmentTile.d.ts.map +1 -0
- package/dist/components/AttachmentTile.js +114 -0
- package/dist/components/AttachmentTile.js.map +1 -0
- package/dist/components/ConsoleBlock.d.ts +11 -0
- package/dist/components/ConsoleBlock.d.ts.map +1 -0
- package/dist/components/ConsoleBlock.js +77 -0
- package/dist/components/ConsoleBlock.js.map +1 -0
- package/dist/components/Input.d.ts +26 -0
- package/dist/components/Input.d.ts.map +1 -0
- package/dist/components/Input.js +56 -0
- package/dist/components/Input.js.map +1 -0
- package/dist/components/MessageEditor.d.ts +38 -0
- package/dist/components/MessageEditor.d.ts.map +1 -0
- package/dist/components/MessageEditor.js +296 -0
- package/dist/components/MessageEditor.js.map +1 -0
- package/dist/components/MessageList.d.ts +13 -0
- package/dist/components/MessageList.d.ts.map +1 -0
- package/dist/components/MessageList.js +88 -0
- package/dist/components/MessageList.js.map +1 -0
- package/dist/components/Messages.d.ts +53 -0
- package/dist/components/Messages.d.ts.map +1 -0
- package/dist/components/Messages.js +323 -0
- package/dist/components/Messages.js.map +1 -0
- package/dist/components/ProviderKeyInput.d.ts +16 -0
- package/dist/components/ProviderKeyInput.d.ts.map +1 -0
- package/dist/components/ProviderKeyInput.js +183 -0
- package/dist/components/ProviderKeyInput.js.map +1 -0
- package/dist/components/SandboxedIframe.d.ts +63 -0
- package/dist/components/SandboxedIframe.d.ts.map +1 -0
- package/dist/components/SandboxedIframe.js +435 -0
- package/dist/components/SandboxedIframe.js.map +1 -0
- package/dist/components/StreamingMessageContainer.d.ts +17 -0
- package/dist/components/StreamingMessageContainer.d.ts.map +1 -0
- package/dist/components/StreamingMessageContainer.js +114 -0
- package/dist/components/StreamingMessageContainer.js.map +1 -0
- package/dist/dialogs/ApiKeyPromptDialog.d.ts +15 -0
- package/dist/dialogs/ApiKeyPromptDialog.d.ts.map +1 -0
- package/dist/dialogs/ApiKeyPromptDialog.js +80 -0
- package/dist/dialogs/ApiKeyPromptDialog.js.map +1 -0
- package/dist/dialogs/AttachmentOverlay.d.ts +32 -0
- package/dist/dialogs/AttachmentOverlay.d.ts.map +1 -0
- package/dist/dialogs/AttachmentOverlay.js +575 -0
- package/dist/dialogs/AttachmentOverlay.js.map +1 -0
- package/dist/dialogs/ModelSelector.d.ts +27 -0
- package/dist/dialogs/ModelSelector.d.ts.map +1 -0
- package/dist/dialogs/ModelSelector.js +334 -0
- package/dist/dialogs/ModelSelector.js.map +1 -0
- package/dist/dialogs/SettingsDialog.d.ts +31 -0
- package/dist/dialogs/SettingsDialog.d.ts.map +1 -0
- package/dist/dialogs/SettingsDialog.js +228 -0
- package/dist/dialogs/SettingsDialog.js.map +1 -0
- package/dist/index.d.ts +46 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +51 -0
- package/dist/index.js.map +1 -0
- package/dist/state/agent-session.d.ts +58 -0
- package/dist/state/agent-session.d.ts.map +1 -0
- package/dist/state/agent-session.js +252 -0
- package/dist/state/agent-session.js.map +1 -0
- package/dist/state/transports/AppTransport.d.ts +13 -0
- package/dist/state/transports/AppTransport.d.ts.map +1 -0
- package/dist/state/transports/AppTransport.js +316 -0
- package/dist/state/transports/AppTransport.js.map +1 -0
- package/dist/state/transports/ProviderTransport.d.ts +12 -0
- package/dist/state/transports/ProviderTransport.d.ts.map +1 -0
- package/dist/state/transports/ProviderTransport.js +44 -0
- package/dist/state/transports/ProviderTransport.js.map +1 -0
- package/dist/state/transports/index.d.ts +4 -0
- package/dist/state/transports/index.d.ts.map +1 -0
- package/dist/state/transports/index.js +4 -0
- package/dist/state/transports/index.js.map +1 -0
- package/dist/state/transports/proxy-types.d.ts +48 -0
- package/dist/state/transports/proxy-types.d.ts.map +1 -0
- package/dist/state/transports/proxy-types.js +2 -0
- package/dist/state/transports/proxy-types.js.map +1 -0
- package/dist/state/transports/types.d.ts +11 -0
- package/dist/state/transports/types.d.ts.map +1 -0
- package/dist/state/transports/types.js +2 -0
- package/dist/state/transports/types.js.map +1 -0
- package/dist/state/types.d.ts +15 -0
- package/dist/state/types.d.ts.map +1 -0
- package/dist/state/types.js +2 -0
- package/dist/state/types.js.map +1 -0
- package/dist/storage/app-storage.d.ts +26 -0
- package/dist/storage/app-storage.d.ts.map +1 -0
- package/dist/storage/app-storage.js +44 -0
- package/dist/storage/app-storage.js.map +1 -0
- package/dist/storage/backends/chrome-storage-backend.d.ts +18 -0
- package/dist/storage/backends/chrome-storage-backend.d.ts.map +1 -0
- package/dist/storage/backends/chrome-storage-backend.js +67 -0
- package/dist/storage/backends/chrome-storage-backend.js.map +1 -0
- package/dist/storage/backends/indexeddb-backend.d.ts +20 -0
- package/dist/storage/backends/indexeddb-backend.d.ts.map +1 -0
- package/dist/storage/backends/indexeddb-backend.js +89 -0
- package/dist/storage/backends/indexeddb-backend.js.map +1 -0
- package/dist/storage/backends/local-storage-backend.d.ts +18 -0
- package/dist/storage/backends/local-storage-backend.d.ts.map +1 -0
- package/dist/storage/backends/local-storage-backend.js +69 -0
- package/dist/storage/backends/local-storage-backend.js.map +1 -0
- package/dist/storage/repositories/provider-keys-repository.d.ts +34 -0
- package/dist/storage/repositories/provider-keys-repository.d.ts.map +1 -0
- package/dist/storage/repositories/provider-keys-repository.js +50 -0
- package/dist/storage/repositories/provider-keys-repository.js.map +1 -0
- package/dist/storage/repositories/settings-repository.d.ts +34 -0
- package/dist/storage/repositories/settings-repository.d.ts.map +1 -0
- package/dist/storage/repositories/settings-repository.js +46 -0
- package/dist/storage/repositories/settings-repository.js.map +1 -0
- package/dist/storage/types.d.ts +43 -0
- package/dist/storage/types.d.ts.map +1 -0
- package/dist/storage/types.js +2 -0
- package/dist/storage/types.js.map +1 -0
- package/dist/tools/artifacts/ArtifactElement.d.ts +10 -0
- package/dist/tools/artifacts/ArtifactElement.d.ts.map +1 -0
- package/dist/tools/artifacts/ArtifactElement.js +12 -0
- package/dist/tools/artifacts/ArtifactElement.js.map +1 -0
- package/dist/tools/artifacts/HtmlArtifact.d.ts +30 -0
- package/dist/tools/artifacts/HtmlArtifact.d.ts.map +1 -0
- package/dist/tools/artifacts/HtmlArtifact.js +217 -0
- package/dist/tools/artifacts/HtmlArtifact.js.map +1 -0
- package/dist/tools/artifacts/MarkdownArtifact.d.ts +20 -0
- package/dist/tools/artifacts/MarkdownArtifact.d.ts.map +1 -0
- package/dist/tools/artifacts/MarkdownArtifact.js +84 -0
- package/dist/tools/artifacts/MarkdownArtifact.js.map +1 -0
- package/dist/tools/artifacts/SvgArtifact.d.ts +19 -0
- package/dist/tools/artifacts/SvgArtifact.d.ts.map +1 -0
- package/dist/tools/artifacts/SvgArtifact.js +80 -0
- package/dist/tools/artifacts/SvgArtifact.js.map +1 -0
- package/dist/tools/artifacts/TextArtifact.d.ts +20 -0
- package/dist/tools/artifacts/TextArtifact.d.ts.map +1 -0
- package/dist/tools/artifacts/TextArtifact.js +147 -0
- package/dist/tools/artifacts/TextArtifact.js.map +1 -0
- package/dist/tools/artifacts/artifacts.d.ts +67 -0
- package/dist/tools/artifacts/artifacts.d.ts.map +1 -0
- package/dist/tools/artifacts/artifacts.js +836 -0
- package/dist/tools/artifacts/artifacts.js.map +1 -0
- package/dist/tools/artifacts/index.d.ts +7 -0
- package/dist/tools/artifacts/index.d.ts.map +1 -0
- package/dist/tools/artifacts/index.js +7 -0
- package/dist/tools/artifacts/index.js.map +1 -0
- package/dist/tools/index.d.ts +14 -0
- package/dist/tools/index.d.ts.map +1 -0
- package/dist/tools/index.js +29 -0
- package/dist/tools/index.js.map +1 -0
- package/dist/tools/javascript-repl.d.ts +43 -0
- package/dist/tools/javascript-repl.d.ts.map +1 -0
- package/dist/tools/javascript-repl.js +252 -0
- package/dist/tools/javascript-repl.js.map +1 -0
- package/dist/tools/renderer-registry.d.ts +11 -0
- package/dist/tools/renderer-registry.d.ts.map +1 -0
- package/dist/tools/renderer-registry.js +15 -0
- package/dist/tools/renderer-registry.js.map +1 -0
- package/dist/tools/renderers/BashRenderer.d.ts +12 -0
- package/dist/tools/renderers/BashRenderer.d.ts.map +1 -0
- package/dist/tools/renderers/BashRenderer.js +35 -0
- package/dist/tools/renderers/BashRenderer.js.map +1 -0
- package/dist/tools/renderers/CalculateRenderer.d.ts +12 -0
- package/dist/tools/renderers/CalculateRenderer.d.ts.map +1 -0
- package/dist/tools/renderers/CalculateRenderer.js +38 -0
- package/dist/tools/renderers/CalculateRenderer.js.map +1 -0
- package/dist/tools/renderers/DefaultRenderer.d.ts +8 -0
- package/dist/tools/renderers/DefaultRenderer.d.ts.map +1 -0
- package/dist/tools/renderers/DefaultRenderer.js +31 -0
- package/dist/tools/renderers/DefaultRenderer.js.map +1 -0
- package/dist/tools/renderers/GetCurrentTimeRenderer.d.ts +12 -0
- package/dist/tools/renderers/GetCurrentTimeRenderer.d.ts.map +1 -0
- package/dist/tools/renderers/GetCurrentTimeRenderer.js +30 -0
- package/dist/tools/renderers/GetCurrentTimeRenderer.js.map +1 -0
- package/dist/tools/types.d.ts +7 -0
- package/dist/tools/types.d.ts.map +1 -0
- package/dist/tools/types.js +2 -0
- package/dist/tools/types.js.map +1 -0
- package/dist/utils/attachment-utils.d.ts +19 -0
- package/dist/utils/attachment-utils.d.ts.map +1 -0
- package/dist/utils/attachment-utils.js +415 -0
- package/dist/utils/attachment-utils.js.map +1 -0
- package/dist/utils/auth-token.d.ts +3 -0
- package/dist/utils/auth-token.d.ts.map +1 -0
- package/dist/utils/auth-token.js +19 -0
- package/dist/utils/auth-token.js.map +1 -0
- package/dist/utils/format.d.ts +6 -0
- package/dist/utils/format.d.ts.map +1 -0
- package/dist/utils/format.js +47 -0
- package/dist/utils/format.js.map +1 -0
- package/dist/utils/i18n.d.ts +111 -0
- package/dist/utils/i18n.d.ts.map +1 -0
- package/dist/utils/i18n.js +224 -0
- package/dist/utils/i18n.js.map +1 -0
- package/dist/utils/test-sessions.d.ts +347 -0
- package/dist/utils/test-sessions.d.ts.map +1 -0
- package/dist/utils/test-sessions.js +2215 -0
- package/dist/utils/test-sessions.js.map +1 -0
- package/example/README.md +61 -0
- package/example/index.html +13 -0
- package/example/package-lock.json +1965 -0
- package/example/package.json +22 -0
- package/example/src/app.css +1 -0
- package/example/src/main.ts +57 -0
- package/example/src/test-sessions.ts +104 -0
- package/example/tsconfig.json +15 -0
- package/example/vite.config.ts +6 -0
- package/package.json +45 -0
- package/src/ChatPanel.ts +214 -0
- package/src/app.css +44 -0
- package/src/components/AgentInterface.ts +316 -0
- package/src/components/AttachmentTile.ts +112 -0
- package/src/components/ConsoleBlock.ts +67 -0
- package/src/components/Input.ts +112 -0
- package/src/components/MessageEditor.ts +272 -0
- package/src/components/MessageList.ts +82 -0
- package/src/components/Messages.ts +310 -0
- package/src/components/ProviderKeyInput.ts +170 -0
- package/src/components/SandboxedIframe.ts +525 -0
- package/src/components/StreamingMessageContainer.ts +101 -0
- package/src/dialogs/ApiKeyPromptDialog.ts +76 -0
- package/src/dialogs/AttachmentOverlay.ts +635 -0
- package/src/dialogs/ModelSelector.ts +324 -0
- package/src/dialogs/SettingsDialog.ts +223 -0
- package/src/index.ts +63 -0
- package/src/state/agent-session.ts +311 -0
- package/src/state/transports/AppTransport.ts +363 -0
- package/src/state/transports/ProviderTransport.ts +49 -0
- package/src/state/transports/index.ts +3 -0
- package/src/state/transports/proxy-types.ts +15 -0
- package/src/state/transports/types.ts +16 -0
- package/src/state/types.ts +11 -0
- package/src/storage/app-storage.ts +53 -0
- package/src/storage/backends/chrome-storage-backend.ts +82 -0
- package/src/storage/backends/indexeddb-backend.ts +107 -0
- package/src/storage/backends/local-storage-backend.ts +74 -0
- package/src/storage/repositories/provider-keys-repository.ts +55 -0
- package/src/storage/repositories/settings-repository.ts +51 -0
- package/src/storage/types.ts +48 -0
- package/src/tools/artifacts/ArtifactElement.ts +15 -0
- package/src/tools/artifacts/HtmlArtifact.ts +221 -0
- package/src/tools/artifacts/MarkdownArtifact.ts +81 -0
- package/src/tools/artifacts/SvgArtifact.ts +77 -0
- package/src/tools/artifacts/TextArtifact.ts +148 -0
- package/src/tools/artifacts/artifacts.ts +888 -0
- package/src/tools/artifacts/index.ts +6 -0
- package/src/tools/index.ts +35 -0
- package/src/tools/javascript-repl.ts +309 -0
- package/src/tools/renderer-registry.ts +18 -0
- package/src/tools/renderers/BashRenderer.ts +45 -0
- package/src/tools/renderers/CalculateRenderer.ts +49 -0
- package/src/tools/renderers/DefaultRenderer.ts +36 -0
- package/src/tools/renderers/GetCurrentTimeRenderer.ts +39 -0
- package/src/tools/types.ts +7 -0
- package/src/utils/attachment-utils.ts +472 -0
- package/src/utils/auth-token.ts +22 -0
- package/src/utils/format.ts +42 -0
- package/src/utils/i18n.ts +343 -0
- package/src/utils/test-sessions.ts +2247 -0
- package/tsconfig.build.json +20 -0
- package/tsconfig.json +7 -0
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
import { Badge, Button, html, Input, i18n } from "@mariozechner/mini-lit";
|
|
2
|
+
import { type Context, complete, getModel } from "@mariozechner/pi-ai";
|
|
3
|
+
import { LitElement } from "lit";
|
|
4
|
+
import { customElement, property, state } from "lit/decorators.js";
|
|
5
|
+
import { getAppStorage } from "../storage/app-storage.js";
|
|
6
|
+
|
|
7
|
+
// Test models for each provider
|
|
8
|
+
const TEST_MODELS: Record<string, string> = {
|
|
9
|
+
anthropic: "claude-3-5-haiku-20241022",
|
|
10
|
+
openai: "gpt-4o-mini",
|
|
11
|
+
google: "gemini-2.5-flash",
|
|
12
|
+
groq: "openai/gpt-oss-20b",
|
|
13
|
+
openrouter: "z-ai/glm-4.6",
|
|
14
|
+
cerebras: "gpt-oss-120b",
|
|
15
|
+
xai: "grok-4-fast-non-reasoning",
|
|
16
|
+
zai: "glm-4.5-air",
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
@customElement("provider-key-input")
|
|
20
|
+
export class ProviderKeyInput extends LitElement {
|
|
21
|
+
@property() provider = "";
|
|
22
|
+
@state() private keyInput = "";
|
|
23
|
+
@state() private testing = false;
|
|
24
|
+
@state() private failed = false;
|
|
25
|
+
@state() private hasKey = false;
|
|
26
|
+
|
|
27
|
+
protected createRenderRoot() {
|
|
28
|
+
return this;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
override async connectedCallback() {
|
|
32
|
+
super.connectedCallback();
|
|
33
|
+
await this.checkKeyStatus();
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
private async checkKeyStatus() {
|
|
37
|
+
try {
|
|
38
|
+
const key = await getAppStorage().providerKeys.getKey(this.provider);
|
|
39
|
+
this.hasKey = !!key;
|
|
40
|
+
} catch (error) {
|
|
41
|
+
console.error("Failed to check key status:", error);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
private async testApiKey(provider: string, apiKey: string): Promise<boolean> {
|
|
46
|
+
try {
|
|
47
|
+
const modelId = TEST_MODELS[provider];
|
|
48
|
+
if (!modelId) return false;
|
|
49
|
+
|
|
50
|
+
let model = getModel(provider as any, modelId);
|
|
51
|
+
if (!model) return false;
|
|
52
|
+
|
|
53
|
+
// Check if CORS proxy is enabled and apply it
|
|
54
|
+
const proxyEnabled = await getAppStorage().settings.get<boolean>("proxy.enabled");
|
|
55
|
+
const proxyUrl = await getAppStorage().settings.get<string>("proxy.url");
|
|
56
|
+
|
|
57
|
+
if (proxyEnabled && proxyUrl && model.baseUrl) {
|
|
58
|
+
model = {
|
|
59
|
+
...model,
|
|
60
|
+
baseUrl: `${proxyUrl}/?url=${encodeURIComponent(model.baseUrl)}`,
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const context: Context = {
|
|
65
|
+
messages: [{ role: "user", content: "Reply with: ok" }],
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
const result = await complete(model, context, {
|
|
69
|
+
apiKey,
|
|
70
|
+
maxTokens: 10,
|
|
71
|
+
} as any);
|
|
72
|
+
|
|
73
|
+
return result.stopReason === "stop";
|
|
74
|
+
} catch (error) {
|
|
75
|
+
console.error(`API key test failed for ${provider}:`, error);
|
|
76
|
+
return false;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
private async saveKey() {
|
|
81
|
+
if (!this.keyInput) return;
|
|
82
|
+
|
|
83
|
+
this.testing = true;
|
|
84
|
+
this.failed = false;
|
|
85
|
+
|
|
86
|
+
const success = await this.testApiKey(this.provider, this.keyInput);
|
|
87
|
+
|
|
88
|
+
this.testing = false;
|
|
89
|
+
|
|
90
|
+
if (success) {
|
|
91
|
+
try {
|
|
92
|
+
await getAppStorage().providerKeys.setKey(this.provider, this.keyInput);
|
|
93
|
+
this.hasKey = true;
|
|
94
|
+
this.keyInput = "";
|
|
95
|
+
this.requestUpdate();
|
|
96
|
+
} catch (error) {
|
|
97
|
+
console.error("Failed to save API key:", error);
|
|
98
|
+
this.failed = true;
|
|
99
|
+
setTimeout(() => {
|
|
100
|
+
this.failed = false;
|
|
101
|
+
this.requestUpdate();
|
|
102
|
+
}, 5000);
|
|
103
|
+
}
|
|
104
|
+
} else {
|
|
105
|
+
this.failed = true;
|
|
106
|
+
setTimeout(() => {
|
|
107
|
+
this.failed = false;
|
|
108
|
+
this.requestUpdate();
|
|
109
|
+
}, 5000);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
private async removeKey() {
|
|
114
|
+
try {
|
|
115
|
+
await getAppStorage().providerKeys.removeKey(this.provider);
|
|
116
|
+
this.hasKey = false;
|
|
117
|
+
this.keyInput = "";
|
|
118
|
+
this.requestUpdate();
|
|
119
|
+
} catch (error) {
|
|
120
|
+
console.error("Failed to remove API key:", error);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
render() {
|
|
125
|
+
return html`
|
|
126
|
+
<div class="space-y-3">
|
|
127
|
+
<div class="flex items-center gap-2">
|
|
128
|
+
<span class="text-sm font-medium capitalize text-foreground">${this.provider}</span>
|
|
129
|
+
${
|
|
130
|
+
this.testing
|
|
131
|
+
? Badge({ children: i18n("Testing..."), variant: "secondary" })
|
|
132
|
+
: this.hasKey
|
|
133
|
+
? html`<span class="text-green-600 dark:text-green-400">✓</span>`
|
|
134
|
+
: ""
|
|
135
|
+
}
|
|
136
|
+
${this.failed ? Badge({ children: i18n("✗ Invalid"), variant: "destructive" }) : ""}
|
|
137
|
+
</div>
|
|
138
|
+
<div class="flex items-center gap-2">
|
|
139
|
+
${Input({
|
|
140
|
+
type: "password",
|
|
141
|
+
placeholder: this.hasKey ? "••••••••••••" : i18n("Enter API key"),
|
|
142
|
+
value: this.keyInput,
|
|
143
|
+
onInput: (e: Event) => {
|
|
144
|
+
this.keyInput = (e.target as HTMLInputElement).value;
|
|
145
|
+
this.requestUpdate();
|
|
146
|
+
},
|
|
147
|
+
className: "flex-1",
|
|
148
|
+
})}
|
|
149
|
+
${
|
|
150
|
+
this.hasKey
|
|
151
|
+
? Button({
|
|
152
|
+
onClick: () => this.removeKey(),
|
|
153
|
+
variant: "ghost",
|
|
154
|
+
size: "sm",
|
|
155
|
+
children: i18n("Clear"),
|
|
156
|
+
className: "!text-destructive",
|
|
157
|
+
})
|
|
158
|
+
: Button({
|
|
159
|
+
onClick: () => this.saveKey(),
|
|
160
|
+
variant: "default",
|
|
161
|
+
size: "sm",
|
|
162
|
+
disabled: !this.keyInput || this.testing,
|
|
163
|
+
children: i18n("Save"),
|
|
164
|
+
})
|
|
165
|
+
}
|
|
166
|
+
</div>
|
|
167
|
+
</div>
|
|
168
|
+
`;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
@@ -0,0 +1,525 @@
|
|
|
1
|
+
import { LitElement } from "lit";
|
|
2
|
+
import { customElement, property } from "lit/decorators.js";
|
|
3
|
+
import type { Attachment } from "../utils/attachment-utils.js";
|
|
4
|
+
|
|
5
|
+
export interface SandboxFile {
|
|
6
|
+
fileName: string;
|
|
7
|
+
content: string | Uint8Array;
|
|
8
|
+
mimeType: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface SandboxResult {
|
|
12
|
+
success: boolean;
|
|
13
|
+
console: Array<{ type: string; text: string }>;
|
|
14
|
+
files?: SandboxFile[];
|
|
15
|
+
error?: { message: string; stack: string };
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Function that returns the URL to the sandbox HTML file.
|
|
20
|
+
* Used in browser extensions to load sandbox.html via chrome.runtime.getURL().
|
|
21
|
+
*/
|
|
22
|
+
export type SandboxUrlProvider = () => string;
|
|
23
|
+
|
|
24
|
+
@customElement("sandbox-iframe")
|
|
25
|
+
export class SandboxIframe extends LitElement {
|
|
26
|
+
private iframe?: HTMLIFrameElement;
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Optional: Provide a function that returns the sandbox HTML URL.
|
|
30
|
+
* If provided, the iframe will use this URL instead of srcdoc.
|
|
31
|
+
* This is required for browser extensions with strict CSP.
|
|
32
|
+
*/
|
|
33
|
+
@property({ attribute: false }) sandboxUrlProvider?: SandboxUrlProvider;
|
|
34
|
+
|
|
35
|
+
createRenderRoot() {
|
|
36
|
+
return this;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
override connectedCallback() {
|
|
40
|
+
super.connectedCallback();
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
override disconnectedCallback() {
|
|
44
|
+
super.disconnectedCallback();
|
|
45
|
+
this.iframe?.remove();
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Load HTML content into sandbox and keep it displayed (for HTML artifacts)
|
|
50
|
+
* @param sandboxId Unique ID
|
|
51
|
+
* @param htmlContent Full HTML content
|
|
52
|
+
* @param attachments Attachments available
|
|
53
|
+
*/
|
|
54
|
+
public loadContent(sandboxId: string, htmlContent: string, attachments: Attachment[]): void {
|
|
55
|
+
const completeHtml = this.prepareHtmlDocument(sandboxId, htmlContent, attachments);
|
|
56
|
+
|
|
57
|
+
if (this.sandboxUrlProvider) {
|
|
58
|
+
// Browser extension mode: use sandbox.html with postMessage
|
|
59
|
+
this.loadViaSandboxUrl(sandboxId, completeHtml, attachments);
|
|
60
|
+
} else {
|
|
61
|
+
// Web mode: use srcdoc
|
|
62
|
+
this.loadViaSrcdoc(completeHtml);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
private loadViaSandboxUrl(sandboxId: string, completeHtml: string, attachments: Attachment[]): void {
|
|
67
|
+
// Wait for sandbox-ready and send content
|
|
68
|
+
const readyHandler = (e: MessageEvent) => {
|
|
69
|
+
if (e.data.type === "sandbox-ready" && e.source === this.iframe?.contentWindow) {
|
|
70
|
+
window.removeEventListener("message", readyHandler);
|
|
71
|
+
this.iframe?.contentWindow?.postMessage(
|
|
72
|
+
{
|
|
73
|
+
type: "sandbox-load",
|
|
74
|
+
sandboxId,
|
|
75
|
+
code: completeHtml,
|
|
76
|
+
attachments,
|
|
77
|
+
},
|
|
78
|
+
"*",
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
};
|
|
82
|
+
window.addEventListener("message", readyHandler);
|
|
83
|
+
|
|
84
|
+
// Always recreate iframe to ensure fresh sandbox and sandbox-ready message
|
|
85
|
+
this.iframe?.remove();
|
|
86
|
+
this.iframe = document.createElement("iframe");
|
|
87
|
+
this.iframe.sandbox.add("allow-scripts");
|
|
88
|
+
this.iframe.sandbox.add("allow-modals");
|
|
89
|
+
this.iframe.style.width = "100%";
|
|
90
|
+
this.iframe.style.height = "100%";
|
|
91
|
+
this.iframe.style.border = "none";
|
|
92
|
+
|
|
93
|
+
this.iframe.src = this.sandboxUrlProvider!();
|
|
94
|
+
|
|
95
|
+
this.appendChild(this.iframe);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
private loadViaSrcdoc(completeHtml: string): void {
|
|
99
|
+
// Always recreate iframe to ensure fresh sandbox
|
|
100
|
+
this.iframe?.remove();
|
|
101
|
+
this.iframe = document.createElement("iframe");
|
|
102
|
+
this.iframe.sandbox.add("allow-scripts");
|
|
103
|
+
this.iframe.sandbox.add("allow-modals");
|
|
104
|
+
this.iframe.style.width = "100%";
|
|
105
|
+
this.iframe.style.height = "100%";
|
|
106
|
+
this.iframe.style.border = "none";
|
|
107
|
+
|
|
108
|
+
// Set content directly via srcdoc (no CSP restrictions in web apps)
|
|
109
|
+
this.iframe.srcdoc = completeHtml;
|
|
110
|
+
|
|
111
|
+
this.appendChild(this.iframe);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Execute code in sandbox
|
|
116
|
+
* @param sandboxId Unique ID for this execution
|
|
117
|
+
* @param code User code (plain JS for REPL, or full HTML for artifacts)
|
|
118
|
+
* @param attachments Attachments available to the code
|
|
119
|
+
* @param signal Abort signal
|
|
120
|
+
* @returns Promise resolving to execution result
|
|
121
|
+
*/
|
|
122
|
+
public async execute(
|
|
123
|
+
sandboxId: string,
|
|
124
|
+
code: string,
|
|
125
|
+
attachments: Attachment[],
|
|
126
|
+
signal?: AbortSignal,
|
|
127
|
+
): Promise<SandboxResult> {
|
|
128
|
+
if (signal?.aborted) {
|
|
129
|
+
throw new Error("Execution aborted");
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// Prepare the complete HTML document with runtime + user code
|
|
133
|
+
const completeHtml = this.prepareHtmlDocument(sandboxId, code, attachments);
|
|
134
|
+
|
|
135
|
+
// Wait for execution to complete
|
|
136
|
+
return new Promise((resolve, reject) => {
|
|
137
|
+
const logs: Array<{ type: string; text: string }> = [];
|
|
138
|
+
const files: SandboxFile[] = [];
|
|
139
|
+
let completed = false;
|
|
140
|
+
|
|
141
|
+
const messageHandler = (e: MessageEvent) => {
|
|
142
|
+
// Ignore messages not for this sandbox
|
|
143
|
+
if (e.data.sandboxId !== sandboxId) return;
|
|
144
|
+
|
|
145
|
+
if (e.data.type === "console") {
|
|
146
|
+
logs.push({
|
|
147
|
+
type: e.data.method === "error" ? "error" : "log",
|
|
148
|
+
text: e.data.text,
|
|
149
|
+
});
|
|
150
|
+
} else if (e.data.type === "file-returned") {
|
|
151
|
+
files.push({
|
|
152
|
+
fileName: e.data.fileName,
|
|
153
|
+
content: e.data.content,
|
|
154
|
+
mimeType: e.data.mimeType,
|
|
155
|
+
});
|
|
156
|
+
} else if (e.data.type === "execution-complete") {
|
|
157
|
+
completed = true;
|
|
158
|
+
cleanup();
|
|
159
|
+
resolve({
|
|
160
|
+
success: true,
|
|
161
|
+
console: logs,
|
|
162
|
+
files: files,
|
|
163
|
+
});
|
|
164
|
+
} else if (e.data.type === "execution-error") {
|
|
165
|
+
completed = true;
|
|
166
|
+
cleanup();
|
|
167
|
+
resolve({
|
|
168
|
+
success: false,
|
|
169
|
+
console: logs,
|
|
170
|
+
error: e.data.error,
|
|
171
|
+
files,
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
};
|
|
175
|
+
|
|
176
|
+
const abortHandler = () => {
|
|
177
|
+
if (!completed) {
|
|
178
|
+
cleanup();
|
|
179
|
+
reject(new Error("Execution aborted"));
|
|
180
|
+
}
|
|
181
|
+
};
|
|
182
|
+
|
|
183
|
+
let readyHandler: ((e: MessageEvent) => void) | undefined;
|
|
184
|
+
|
|
185
|
+
const cleanup = () => {
|
|
186
|
+
window.removeEventListener("message", messageHandler);
|
|
187
|
+
signal?.removeEventListener("abort", abortHandler);
|
|
188
|
+
if (readyHandler) {
|
|
189
|
+
window.removeEventListener("message", readyHandler);
|
|
190
|
+
}
|
|
191
|
+
clearTimeout(timeoutId);
|
|
192
|
+
};
|
|
193
|
+
|
|
194
|
+
// Set up listeners BEFORE creating iframe
|
|
195
|
+
window.addEventListener("message", messageHandler);
|
|
196
|
+
signal?.addEventListener("abort", abortHandler);
|
|
197
|
+
|
|
198
|
+
// Timeout after 30 seconds
|
|
199
|
+
const timeoutId = setTimeout(() => {
|
|
200
|
+
if (!completed) {
|
|
201
|
+
cleanup();
|
|
202
|
+
resolve({
|
|
203
|
+
success: false,
|
|
204
|
+
error: { message: "Execution timeout (30s)", stack: "" },
|
|
205
|
+
console: logs,
|
|
206
|
+
files,
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
}, 30000);
|
|
210
|
+
|
|
211
|
+
if (this.sandboxUrlProvider) {
|
|
212
|
+
// Browser extension mode: wait for sandbox-ready and send content
|
|
213
|
+
readyHandler = (e: MessageEvent) => {
|
|
214
|
+
if (e.data.type === "sandbox-ready" && e.source === this.iframe?.contentWindow) {
|
|
215
|
+
window.removeEventListener("message", readyHandler!);
|
|
216
|
+
// Send the complete HTML
|
|
217
|
+
this.iframe?.contentWindow?.postMessage(
|
|
218
|
+
{
|
|
219
|
+
type: "sandbox-load",
|
|
220
|
+
sandboxId,
|
|
221
|
+
code: completeHtml,
|
|
222
|
+
attachments,
|
|
223
|
+
},
|
|
224
|
+
"*",
|
|
225
|
+
);
|
|
226
|
+
}
|
|
227
|
+
};
|
|
228
|
+
window.addEventListener("message", readyHandler);
|
|
229
|
+
|
|
230
|
+
// Create iframe AFTER all listeners are set up
|
|
231
|
+
this.iframe?.remove();
|
|
232
|
+
this.iframe = document.createElement("iframe");
|
|
233
|
+
this.iframe.sandbox.add("allow-scripts");
|
|
234
|
+
this.iframe.sandbox.add("allow-modals");
|
|
235
|
+
this.iframe.style.width = "100%";
|
|
236
|
+
this.iframe.style.height = "100%";
|
|
237
|
+
this.iframe.style.border = "none";
|
|
238
|
+
|
|
239
|
+
this.iframe.src = this.sandboxUrlProvider();
|
|
240
|
+
|
|
241
|
+
this.appendChild(this.iframe);
|
|
242
|
+
} else {
|
|
243
|
+
// Web mode: use srcdoc
|
|
244
|
+
this.iframe?.remove();
|
|
245
|
+
this.iframe = document.createElement("iframe");
|
|
246
|
+
this.iframe.sandbox.add("allow-scripts");
|
|
247
|
+
this.iframe.sandbox.add("allow-modals");
|
|
248
|
+
this.iframe.style.width = "100%";
|
|
249
|
+
this.iframe.style.height = "100%";
|
|
250
|
+
this.iframe.style.border = "none";
|
|
251
|
+
|
|
252
|
+
// Set content via srcdoc BEFORE appending to DOM
|
|
253
|
+
this.iframe.srcdoc = completeHtml;
|
|
254
|
+
|
|
255
|
+
this.appendChild(this.iframe);
|
|
256
|
+
}
|
|
257
|
+
});
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/**
|
|
261
|
+
* Prepare complete HTML document with runtime + user code
|
|
262
|
+
*/
|
|
263
|
+
private prepareHtmlDocument(sandboxId: string, userCode: string, attachments: Attachment[]): string {
|
|
264
|
+
// Runtime script that will be injected
|
|
265
|
+
const runtime = this.getRuntimeScript(sandboxId, attachments);
|
|
266
|
+
|
|
267
|
+
// Check if user provided full HTML
|
|
268
|
+
const hasHtmlTag = /<html[^>]*>/i.test(userCode);
|
|
269
|
+
|
|
270
|
+
if (hasHtmlTag) {
|
|
271
|
+
// HTML Artifact - inject runtime into existing HTML
|
|
272
|
+
const headMatch = userCode.match(/<head[^>]*>/i);
|
|
273
|
+
if (headMatch) {
|
|
274
|
+
const index = headMatch.index! + headMatch[0].length;
|
|
275
|
+
return userCode.slice(0, index) + runtime + userCode.slice(index);
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
const htmlMatch = userCode.match(/<html[^>]*>/i);
|
|
279
|
+
if (htmlMatch) {
|
|
280
|
+
const index = htmlMatch.index! + htmlMatch[0].length;
|
|
281
|
+
return userCode.slice(0, index) + runtime + userCode.slice(index);
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
// Fallback: prepend runtime
|
|
285
|
+
return runtime + userCode;
|
|
286
|
+
} else {
|
|
287
|
+
// REPL - wrap code in HTML with runtime and call complete() when done
|
|
288
|
+
return `<!DOCTYPE html>
|
|
289
|
+
<html>
|
|
290
|
+
<head>
|
|
291
|
+
${runtime}
|
|
292
|
+
</head>
|
|
293
|
+
<body>
|
|
294
|
+
<script type="module">
|
|
295
|
+
(async () => {
|
|
296
|
+
try {
|
|
297
|
+
${userCode}
|
|
298
|
+
window.complete();
|
|
299
|
+
} catch (error) {
|
|
300
|
+
console.error(error?.stack || error?.message || String(error));
|
|
301
|
+
window.complete({
|
|
302
|
+
message: error?.message || String(error),
|
|
303
|
+
stack: error?.stack || new Error().stack
|
|
304
|
+
});
|
|
305
|
+
}
|
|
306
|
+
})();
|
|
307
|
+
</script>
|
|
308
|
+
</body>
|
|
309
|
+
</html>`;
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
/**
|
|
314
|
+
* Get the runtime script that captures console, provides helpers, etc.
|
|
315
|
+
*/
|
|
316
|
+
private getRuntimeScript(sandboxId: string, attachments: Attachment[]): string {
|
|
317
|
+
// Convert attachments to serializable format
|
|
318
|
+
const attachmentsData = attachments.map((a) => ({
|
|
319
|
+
id: a.id,
|
|
320
|
+
fileName: a.fileName,
|
|
321
|
+
mimeType: a.mimeType,
|
|
322
|
+
size: a.size,
|
|
323
|
+
content: a.content,
|
|
324
|
+
extractedText: a.extractedText,
|
|
325
|
+
}));
|
|
326
|
+
|
|
327
|
+
// Runtime function that will run in the sandbox (NO parameters - values injected before function)
|
|
328
|
+
const runtimeFunc = () => {
|
|
329
|
+
// Helper functions
|
|
330
|
+
(window as any).listFiles = () =>
|
|
331
|
+
(attachments || []).map((a: any) => ({
|
|
332
|
+
id: a.id,
|
|
333
|
+
fileName: a.fileName,
|
|
334
|
+
mimeType: a.mimeType,
|
|
335
|
+
size: a.size,
|
|
336
|
+
}));
|
|
337
|
+
|
|
338
|
+
(window as any).readTextFile = (attachmentId: string) => {
|
|
339
|
+
const a = (attachments || []).find((x: any) => x.id === attachmentId);
|
|
340
|
+
if (!a) throw new Error("Attachment not found: " + attachmentId);
|
|
341
|
+
if (a.extractedText) return a.extractedText;
|
|
342
|
+
try {
|
|
343
|
+
return atob(a.content);
|
|
344
|
+
} catch {
|
|
345
|
+
throw new Error("Failed to decode text content for: " + attachmentId);
|
|
346
|
+
}
|
|
347
|
+
};
|
|
348
|
+
|
|
349
|
+
(window as any).readBinaryFile = (attachmentId: string) => {
|
|
350
|
+
const a = (attachments || []).find((x: any) => x.id === attachmentId);
|
|
351
|
+
if (!a) throw new Error("Attachment not found: " + attachmentId);
|
|
352
|
+
const bin = atob(a.content);
|
|
353
|
+
const bytes = new Uint8Array(bin.length);
|
|
354
|
+
for (let i = 0; i < bin.length; i++) bytes[i] = bin.charCodeAt(i);
|
|
355
|
+
return bytes;
|
|
356
|
+
};
|
|
357
|
+
|
|
358
|
+
(window as any).returnFile = async (fileName: string, content: any, mimeType?: string) => {
|
|
359
|
+
let finalContent: any, finalMimeType: string;
|
|
360
|
+
|
|
361
|
+
if (content instanceof Blob) {
|
|
362
|
+
const arrayBuffer = await content.arrayBuffer();
|
|
363
|
+
finalContent = new Uint8Array(arrayBuffer);
|
|
364
|
+
finalMimeType = mimeType || content.type || "application/octet-stream";
|
|
365
|
+
if (!mimeType && !content.type) {
|
|
366
|
+
throw new Error(
|
|
367
|
+
"returnFile: MIME type is required for Blob content. Please provide a mimeType parameter (e.g., 'image/png').",
|
|
368
|
+
);
|
|
369
|
+
}
|
|
370
|
+
} else if (content instanceof Uint8Array) {
|
|
371
|
+
finalContent = content;
|
|
372
|
+
if (!mimeType) {
|
|
373
|
+
throw new Error(
|
|
374
|
+
"returnFile: MIME type is required for Uint8Array content. Please provide a mimeType parameter (e.g., 'image/png').",
|
|
375
|
+
);
|
|
376
|
+
}
|
|
377
|
+
finalMimeType = mimeType;
|
|
378
|
+
} else if (typeof content === "string") {
|
|
379
|
+
finalContent = content;
|
|
380
|
+
finalMimeType = mimeType || "text/plain";
|
|
381
|
+
} else {
|
|
382
|
+
finalContent = JSON.stringify(content, null, 2);
|
|
383
|
+
finalMimeType = mimeType || "application/json";
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
window.parent.postMessage(
|
|
387
|
+
{
|
|
388
|
+
type: "file-returned",
|
|
389
|
+
sandboxId,
|
|
390
|
+
fileName,
|
|
391
|
+
content: finalContent,
|
|
392
|
+
mimeType: finalMimeType,
|
|
393
|
+
},
|
|
394
|
+
"*",
|
|
395
|
+
);
|
|
396
|
+
};
|
|
397
|
+
|
|
398
|
+
// Console capture
|
|
399
|
+
const originalConsole = {
|
|
400
|
+
log: console.log,
|
|
401
|
+
error: console.error,
|
|
402
|
+
warn: console.warn,
|
|
403
|
+
info: console.info,
|
|
404
|
+
};
|
|
405
|
+
|
|
406
|
+
["log", "error", "warn", "info"].forEach((method) => {
|
|
407
|
+
(console as any)[method] = (...args: any[]) => {
|
|
408
|
+
const text = args
|
|
409
|
+
.map((arg) => {
|
|
410
|
+
try {
|
|
411
|
+
return typeof arg === "object" ? JSON.stringify(arg) : String(arg);
|
|
412
|
+
} catch {
|
|
413
|
+
return String(arg);
|
|
414
|
+
}
|
|
415
|
+
})
|
|
416
|
+
.join(" ");
|
|
417
|
+
|
|
418
|
+
window.parent.postMessage(
|
|
419
|
+
{
|
|
420
|
+
type: "console",
|
|
421
|
+
sandboxId,
|
|
422
|
+
method,
|
|
423
|
+
text,
|
|
424
|
+
},
|
|
425
|
+
"*",
|
|
426
|
+
);
|
|
427
|
+
|
|
428
|
+
(originalConsole as any)[method].apply(console, args);
|
|
429
|
+
};
|
|
430
|
+
});
|
|
431
|
+
|
|
432
|
+
// Track errors for HTML artifacts
|
|
433
|
+
let lastError: { message: string; stack: string } | null = null;
|
|
434
|
+
|
|
435
|
+
// Error handlers
|
|
436
|
+
window.addEventListener("error", (e) => {
|
|
437
|
+
const text =
|
|
438
|
+
(e.error?.stack || e.message || String(e)) + " at line " + (e.lineno || "?") + ":" + (e.colno || "?");
|
|
439
|
+
|
|
440
|
+
// Store the error
|
|
441
|
+
lastError = {
|
|
442
|
+
message: e.error?.message || e.message || String(e),
|
|
443
|
+
stack: e.error?.stack || text,
|
|
444
|
+
};
|
|
445
|
+
|
|
446
|
+
window.parent.postMessage(
|
|
447
|
+
{
|
|
448
|
+
type: "console",
|
|
449
|
+
sandboxId,
|
|
450
|
+
method: "error",
|
|
451
|
+
text,
|
|
452
|
+
},
|
|
453
|
+
"*",
|
|
454
|
+
);
|
|
455
|
+
});
|
|
456
|
+
|
|
457
|
+
window.addEventListener("unhandledrejection", (e) => {
|
|
458
|
+
const text = "Unhandled promise rejection: " + (e.reason?.message || e.reason || "Unknown error");
|
|
459
|
+
|
|
460
|
+
// Store the error
|
|
461
|
+
lastError = {
|
|
462
|
+
message: e.reason?.message || String(e.reason) || "Unhandled promise rejection",
|
|
463
|
+
stack: e.reason?.stack || text,
|
|
464
|
+
};
|
|
465
|
+
|
|
466
|
+
window.parent.postMessage(
|
|
467
|
+
{
|
|
468
|
+
type: "console",
|
|
469
|
+
sandboxId,
|
|
470
|
+
method: "error",
|
|
471
|
+
text,
|
|
472
|
+
},
|
|
473
|
+
"*",
|
|
474
|
+
);
|
|
475
|
+
});
|
|
476
|
+
|
|
477
|
+
// Expose complete() method for user code to call
|
|
478
|
+
let completionSent = false;
|
|
479
|
+
(window as any).complete = (error?: { message: string; stack: string }) => {
|
|
480
|
+
if (completionSent) return;
|
|
481
|
+
completionSent = true;
|
|
482
|
+
|
|
483
|
+
// Use provided error or last caught error
|
|
484
|
+
const finalError = error || lastError;
|
|
485
|
+
|
|
486
|
+
if (finalError) {
|
|
487
|
+
window.parent.postMessage(
|
|
488
|
+
{
|
|
489
|
+
type: "execution-error",
|
|
490
|
+
sandboxId,
|
|
491
|
+
error: finalError,
|
|
492
|
+
},
|
|
493
|
+
"*",
|
|
494
|
+
);
|
|
495
|
+
} else {
|
|
496
|
+
window.parent.postMessage(
|
|
497
|
+
{
|
|
498
|
+
type: "execution-complete",
|
|
499
|
+
sandboxId,
|
|
500
|
+
},
|
|
501
|
+
"*",
|
|
502
|
+
);
|
|
503
|
+
}
|
|
504
|
+
};
|
|
505
|
+
|
|
506
|
+
// Fallback timeout for HTML artifacts that don't call complete()
|
|
507
|
+
if (document.readyState === "complete" || document.readyState === "interactive") {
|
|
508
|
+
setTimeout(() => (window as any).complete(), 2000);
|
|
509
|
+
} else {
|
|
510
|
+
window.addEventListener("load", () => {
|
|
511
|
+
setTimeout(() => (window as any).complete(), 2000);
|
|
512
|
+
});
|
|
513
|
+
}
|
|
514
|
+
};
|
|
515
|
+
|
|
516
|
+
// Prepend the const declarations, then the function
|
|
517
|
+
return (
|
|
518
|
+
`<script>\n` +
|
|
519
|
+
`window.sandboxId = ${JSON.stringify(sandboxId)};\n` +
|
|
520
|
+
`window.attachments = ${JSON.stringify(attachmentsData)};\n` +
|
|
521
|
+
`(${runtimeFunc.toString()})();\n` +
|
|
522
|
+
`</script>`
|
|
523
|
+
);
|
|
524
|
+
}
|
|
525
|
+
}
|