@oh-my-pi/pi-web-ui 1.337.0
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/CHANGELOG.md +96 -0
- package/README.md +609 -0
- package/example/README.md +61 -0
- package/example/index.html +13 -0
- package/example/package.json +24 -0
- package/example/src/app.css +1 -0
- package/example/src/custom-messages.ts +99 -0
- package/example/src/main.ts +420 -0
- package/example/tsconfig.json +23 -0
- package/example/vite.config.ts +6 -0
- package/package.json +57 -0
- package/scripts/count-prompt-tokens.ts +88 -0
- package/src/ChatPanel.ts +218 -0
- package/src/app.css +68 -0
- package/src/components/AgentInterface.ts +390 -0
- package/src/components/AttachmentTile.ts +107 -0
- package/src/components/ConsoleBlock.ts +74 -0
- package/src/components/CustomProviderCard.ts +96 -0
- package/src/components/ExpandableSection.ts +46 -0
- package/src/components/Input.ts +113 -0
- package/src/components/MessageEditor.ts +404 -0
- package/src/components/MessageList.ts +97 -0
- package/src/components/Messages.ts +384 -0
- package/src/components/ProviderKeyInput.ts +152 -0
- package/src/components/SandboxedIframe.ts +626 -0
- package/src/components/StreamingMessageContainer.ts +107 -0
- package/src/components/ThinkingBlock.ts +45 -0
- package/src/components/message-renderer-registry.ts +28 -0
- package/src/components/sandbox/ArtifactsRuntimeProvider.ts +219 -0
- package/src/components/sandbox/AttachmentsRuntimeProvider.ts +66 -0
- package/src/components/sandbox/ConsoleRuntimeProvider.ts +186 -0
- package/src/components/sandbox/FileDownloadRuntimeProvider.ts +110 -0
- package/src/components/sandbox/RuntimeMessageBridge.ts +82 -0
- package/src/components/sandbox/RuntimeMessageRouter.ts +216 -0
- package/src/components/sandbox/SandboxRuntimeProvider.ts +52 -0
- package/src/dialogs/ApiKeyPromptDialog.ts +75 -0
- package/src/dialogs/AttachmentOverlay.ts +640 -0
- package/src/dialogs/CustomProviderDialog.ts +274 -0
- package/src/dialogs/ModelSelector.ts +314 -0
- package/src/dialogs/PersistentStorageDialog.ts +146 -0
- package/src/dialogs/ProvidersModelsTab.ts +212 -0
- package/src/dialogs/SessionListDialog.ts +157 -0
- package/src/dialogs/SettingsDialog.ts +216 -0
- package/src/index.ts +115 -0
- package/src/prompts/prompts.ts +282 -0
- package/src/storage/app-storage.ts +60 -0
- package/src/storage/backends/indexeddb-storage-backend.ts +193 -0
- package/src/storage/store.ts +33 -0
- package/src/storage/stores/custom-providers-store.ts +62 -0
- package/src/storage/stores/provider-keys-store.ts +33 -0
- package/src/storage/stores/sessions-store.ts +136 -0
- package/src/storage/stores/settings-store.ts +34 -0
- package/src/storage/types.ts +206 -0
- package/src/tools/artifacts/ArtifactElement.ts +14 -0
- package/src/tools/artifacts/ArtifactPill.ts +26 -0
- package/src/tools/artifacts/Console.ts +102 -0
- package/src/tools/artifacts/DocxArtifact.ts +213 -0
- package/src/tools/artifacts/ExcelArtifact.ts +231 -0
- package/src/tools/artifacts/GenericArtifact.ts +118 -0
- package/src/tools/artifacts/HtmlArtifact.ts +203 -0
- package/src/tools/artifacts/ImageArtifact.ts +116 -0
- package/src/tools/artifacts/MarkdownArtifact.ts +83 -0
- package/src/tools/artifacts/PdfArtifact.ts +201 -0
- package/src/tools/artifacts/SvgArtifact.ts +82 -0
- package/src/tools/artifacts/TextArtifact.ts +148 -0
- package/src/tools/artifacts/artifacts-tool-renderer.ts +371 -0
- package/src/tools/artifacts/artifacts.ts +713 -0
- package/src/tools/artifacts/index.ts +7 -0
- package/src/tools/extract-document.ts +271 -0
- package/src/tools/index.ts +46 -0
- package/src/tools/javascript-repl.ts +316 -0
- package/src/tools/renderer-registry.ts +127 -0
- package/src/tools/renderers/BashRenderer.ts +52 -0
- package/src/tools/renderers/CalculateRenderer.ts +58 -0
- package/src/tools/renderers/DefaultRenderer.ts +95 -0
- package/src/tools/renderers/GetCurrentTimeRenderer.ts +92 -0
- package/src/tools/types.ts +15 -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 +653 -0
- package/src/utils/model-discovery.ts +277 -0
- package/src/utils/proxy-utils.ts +134 -0
- package/src/utils/test-sessions.ts +2357 -0
- package/tsconfig.build.json +20 -0
- package/tsconfig.json +7 -0
|
@@ -0,0 +1,626 @@
|
|
|
1
|
+
import { LitElement } from "lit";
|
|
2
|
+
import { customElement, property } from "lit/decorators.js";
|
|
3
|
+
import { ConsoleRuntimeProvider } from "./sandbox/ConsoleRuntimeProvider.js";
|
|
4
|
+
import { RuntimeMessageBridge } from "./sandbox/RuntimeMessageBridge.js";
|
|
5
|
+
import { type MessageConsumer, RUNTIME_MESSAGE_ROUTER } from "./sandbox/RuntimeMessageRouter.js";
|
|
6
|
+
import type { SandboxRuntimeProvider } from "./sandbox/SandboxRuntimeProvider.js";
|
|
7
|
+
|
|
8
|
+
export interface SandboxFile {
|
|
9
|
+
fileName: string;
|
|
10
|
+
content: string | Uint8Array;
|
|
11
|
+
mimeType: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface SandboxResult {
|
|
15
|
+
success: boolean;
|
|
16
|
+
console: Array<{ type: string; text: string }>;
|
|
17
|
+
files?: SandboxFile[];
|
|
18
|
+
error?: { message: string; stack: string };
|
|
19
|
+
returnValue?: any;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Function that returns the URL to the sandbox HTML file.
|
|
24
|
+
* Used in browser extensions to load sandbox.html via chrome.runtime.getURL().
|
|
25
|
+
*/
|
|
26
|
+
export type SandboxUrlProvider = () => string;
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Configuration for prepareHtmlDocument
|
|
30
|
+
*/
|
|
31
|
+
export interface PrepareHtmlOptions {
|
|
32
|
+
/** True if this is an HTML artifact (inject into existing HTML), false if REPL (wrap in HTML) */
|
|
33
|
+
isHtmlArtifact: boolean;
|
|
34
|
+
/** True if this is a standalone download (no runtime bridge, no navigation interceptor) */
|
|
35
|
+
isStandalone?: boolean;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Escape HTML special sequences in code to prevent premature tag closure
|
|
40
|
+
* @param code Code that will be injected into <script> tags
|
|
41
|
+
* @returns Escaped code safe for injection
|
|
42
|
+
*/
|
|
43
|
+
function escapeScriptContent(code: string): string {
|
|
44
|
+
return code.replace(/<\/script/gi, "<\\/script");
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
@customElement("sandbox-iframe")
|
|
48
|
+
export class SandboxIframe extends LitElement {
|
|
49
|
+
private iframe?: HTMLIFrameElement;
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Optional: Provide a function that returns the sandbox HTML URL.
|
|
53
|
+
* If provided, the iframe will use this URL instead of srcdoc.
|
|
54
|
+
* This is required for browser extensions with strict CSP.
|
|
55
|
+
*/
|
|
56
|
+
@property({ attribute: false }) sandboxUrlProvider?: SandboxUrlProvider;
|
|
57
|
+
|
|
58
|
+
createRenderRoot() {
|
|
59
|
+
return this;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
override connectedCallback() {
|
|
63
|
+
super.connectedCallback();
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
override disconnectedCallback() {
|
|
67
|
+
super.disconnectedCallback();
|
|
68
|
+
// Note: We don't unregister the sandbox here for loadContent() mode
|
|
69
|
+
// because the caller (HtmlArtifact) owns the sandbox lifecycle.
|
|
70
|
+
// For execute() mode, the sandbox is unregistered in the cleanup function.
|
|
71
|
+
this.iframe?.remove();
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Load HTML content into sandbox and keep it displayed (for HTML artifacts)
|
|
76
|
+
* @param sandboxId Unique ID
|
|
77
|
+
* @param htmlContent Full HTML content
|
|
78
|
+
* @param providers Runtime providers to inject
|
|
79
|
+
* @param consumers Message consumers to register (optional)
|
|
80
|
+
*/
|
|
81
|
+
public loadContent(
|
|
82
|
+
sandboxId: string,
|
|
83
|
+
htmlContent: string,
|
|
84
|
+
providers: SandboxRuntimeProvider[] = [],
|
|
85
|
+
consumers: MessageConsumer[] = [],
|
|
86
|
+
): void {
|
|
87
|
+
// Unregister previous sandbox if exists
|
|
88
|
+
try {
|
|
89
|
+
RUNTIME_MESSAGE_ROUTER.unregisterSandbox(sandboxId);
|
|
90
|
+
} catch {
|
|
91
|
+
// Sandbox might not exist, that's ok
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
providers = [new ConsoleRuntimeProvider(), ...providers];
|
|
95
|
+
|
|
96
|
+
RUNTIME_MESSAGE_ROUTER.registerSandbox(sandboxId, providers, consumers);
|
|
97
|
+
|
|
98
|
+
// loadContent is always used for HTML artifacts (not standalone)
|
|
99
|
+
const completeHtml = this.prepareHtmlDocument(sandboxId, htmlContent, providers, {
|
|
100
|
+
isHtmlArtifact: true,
|
|
101
|
+
isStandalone: false,
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
// Validate HTML before loading
|
|
105
|
+
const validationError = this.validateHtml(completeHtml);
|
|
106
|
+
if (validationError) {
|
|
107
|
+
console.error("HTML validation failed:", validationError);
|
|
108
|
+
// Show error in iframe instead of crashing
|
|
109
|
+
this.iframe?.remove();
|
|
110
|
+
this.iframe = document.createElement("iframe");
|
|
111
|
+
this.iframe.style.cssText = "width: 100%; height: 100%; border: none;";
|
|
112
|
+
this.iframe.srcdoc = `
|
|
113
|
+
<html>
|
|
114
|
+
<body style="font-family: monospace; padding: 20px; background: #fff; color: #000;">
|
|
115
|
+
<h3 style="color: #c00;">HTML Validation Error</h3>
|
|
116
|
+
<pre style="background: #f5f5f5; padding: 10px; border-radius: 4px; overflow-x: auto; white-space: pre-wrap;">${validationError}</pre>
|
|
117
|
+
</body>
|
|
118
|
+
</html>
|
|
119
|
+
`;
|
|
120
|
+
this.appendChild(this.iframe);
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Remove previous iframe if exists
|
|
125
|
+
this.iframe?.remove();
|
|
126
|
+
|
|
127
|
+
if (this.sandboxUrlProvider) {
|
|
128
|
+
// Browser extension mode: use sandbox.html with postMessage
|
|
129
|
+
this.loadViaSandboxUrl(sandboxId, completeHtml);
|
|
130
|
+
} else {
|
|
131
|
+
// Web mode: use srcdoc
|
|
132
|
+
this.loadViaSrcdoc(sandboxId, completeHtml);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
private loadViaSandboxUrl(sandboxId: string, completeHtml: string): void {
|
|
137
|
+
// Create iframe pointing to sandbox URL
|
|
138
|
+
this.iframe = document.createElement("iframe");
|
|
139
|
+
this.iframe.sandbox.add("allow-scripts");
|
|
140
|
+
this.iframe.sandbox.add("allow-modals");
|
|
141
|
+
this.iframe.style.width = "100%";
|
|
142
|
+
this.iframe.style.height = "100%";
|
|
143
|
+
this.iframe.style.border = "none";
|
|
144
|
+
this.iframe.src = this.sandboxUrlProvider!();
|
|
145
|
+
|
|
146
|
+
// Update router with iframe reference BEFORE appending to DOM
|
|
147
|
+
RUNTIME_MESSAGE_ROUTER.setSandboxIframe(sandboxId, this.iframe);
|
|
148
|
+
|
|
149
|
+
// Listen for open-external-url messages from iframe
|
|
150
|
+
const externalUrlHandler = (e: MessageEvent) => {
|
|
151
|
+
if (e.data.type === "open-external-url" && e.source === this.iframe?.contentWindow) {
|
|
152
|
+
// Use chrome.tabs API to open in new tab
|
|
153
|
+
const chromeAPI = (globalThis as any).chrome;
|
|
154
|
+
if (chromeAPI?.tabs) {
|
|
155
|
+
chromeAPI.tabs.create({ url: e.data.url });
|
|
156
|
+
} else {
|
|
157
|
+
// Fallback for non-extension context
|
|
158
|
+
window.open(e.data.url, "_blank");
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
};
|
|
162
|
+
window.addEventListener("message", externalUrlHandler);
|
|
163
|
+
|
|
164
|
+
// Listen for sandbox-ready and sandbox-error messages directly
|
|
165
|
+
const readyHandler = (e: MessageEvent) => {
|
|
166
|
+
if (e.data.type === "sandbox-ready" && e.source === this.iframe?.contentWindow) {
|
|
167
|
+
window.removeEventListener("message", readyHandler);
|
|
168
|
+
window.removeEventListener("message", errorHandler);
|
|
169
|
+
|
|
170
|
+
// Send content to sandbox
|
|
171
|
+
this.iframe?.contentWindow?.postMessage(
|
|
172
|
+
{
|
|
173
|
+
type: "sandbox-load",
|
|
174
|
+
sandboxId,
|
|
175
|
+
code: completeHtml,
|
|
176
|
+
},
|
|
177
|
+
"*",
|
|
178
|
+
);
|
|
179
|
+
}
|
|
180
|
+
};
|
|
181
|
+
|
|
182
|
+
const errorHandler = (e: MessageEvent) => {
|
|
183
|
+
if (e.data.type === "sandbox-error" && e.source === this.iframe?.contentWindow) {
|
|
184
|
+
window.removeEventListener("message", readyHandler);
|
|
185
|
+
window.removeEventListener("message", errorHandler);
|
|
186
|
+
|
|
187
|
+
// The sandbox.js already sent us the error via postMessage.
|
|
188
|
+
// We need to convert it to an execution-error message that the execute() consumer will handle.
|
|
189
|
+
// Simulate receiving an execution-error from the sandbox
|
|
190
|
+
window.postMessage(
|
|
191
|
+
{
|
|
192
|
+
sandboxId: sandboxId,
|
|
193
|
+
type: "execution-error",
|
|
194
|
+
error: { message: e.data.error, stack: e.data.stack },
|
|
195
|
+
},
|
|
196
|
+
"*",
|
|
197
|
+
);
|
|
198
|
+
}
|
|
199
|
+
};
|
|
200
|
+
|
|
201
|
+
window.addEventListener("message", readyHandler);
|
|
202
|
+
window.addEventListener("message", errorHandler);
|
|
203
|
+
|
|
204
|
+
this.appendChild(this.iframe);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
private loadViaSrcdoc(sandboxId: string, completeHtml: string): void {
|
|
208
|
+
// Create iframe with srcdoc
|
|
209
|
+
this.iframe = document.createElement("iframe");
|
|
210
|
+
this.iframe.sandbox.add("allow-scripts");
|
|
211
|
+
this.iframe.sandbox.add("allow-modals");
|
|
212
|
+
this.iframe.style.width = "100%";
|
|
213
|
+
this.iframe.style.height = "100%";
|
|
214
|
+
this.iframe.style.border = "none";
|
|
215
|
+
this.iframe.srcdoc = completeHtml;
|
|
216
|
+
|
|
217
|
+
// Update router with iframe reference BEFORE appending to DOM
|
|
218
|
+
RUNTIME_MESSAGE_ROUTER.setSandboxIframe(sandboxId, this.iframe);
|
|
219
|
+
|
|
220
|
+
// Listen for open-external-url messages from iframe
|
|
221
|
+
const externalUrlHandler = (e: MessageEvent) => {
|
|
222
|
+
if (e.data.type === "open-external-url" && e.source === this.iframe?.contentWindow) {
|
|
223
|
+
// Fallback for non-extension context
|
|
224
|
+
window.open(e.data.url, "_blank");
|
|
225
|
+
}
|
|
226
|
+
};
|
|
227
|
+
window.addEventListener("message", externalUrlHandler);
|
|
228
|
+
|
|
229
|
+
this.appendChild(this.iframe);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* Execute code in sandbox
|
|
234
|
+
* @param sandboxId Unique ID for this execution
|
|
235
|
+
* @param code User code (plain JS for REPL, or full HTML for artifacts)
|
|
236
|
+
* @param providers Runtime providers to inject
|
|
237
|
+
* @param consumers Additional message consumers (optional, execute has its own internal consumer)
|
|
238
|
+
* @param signal Abort signal
|
|
239
|
+
* @returns Promise resolving to execution result
|
|
240
|
+
*/
|
|
241
|
+
public async execute(
|
|
242
|
+
sandboxId: string,
|
|
243
|
+
code: string,
|
|
244
|
+
providers: SandboxRuntimeProvider[] = [],
|
|
245
|
+
consumers: MessageConsumer[] = [],
|
|
246
|
+
signal?: AbortSignal,
|
|
247
|
+
isHtmlArtifact: boolean = false,
|
|
248
|
+
): Promise<SandboxResult> {
|
|
249
|
+
if (signal?.aborted) {
|
|
250
|
+
throw new Error("Execution aborted");
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
const consoleProvider = new ConsoleRuntimeProvider();
|
|
254
|
+
providers = [consoleProvider, ...providers];
|
|
255
|
+
RUNTIME_MESSAGE_ROUTER.registerSandbox(sandboxId, providers, consumers);
|
|
256
|
+
|
|
257
|
+
// Notify providers that execution is starting
|
|
258
|
+
for (const provider of providers) {
|
|
259
|
+
provider.onExecutionStart?.(sandboxId, signal);
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
const files: SandboxFile[] = [];
|
|
263
|
+
let completed = false;
|
|
264
|
+
|
|
265
|
+
return new Promise((resolve, reject) => {
|
|
266
|
+
// 4. Create execution consumer for lifecycle messages
|
|
267
|
+
const executionConsumer: MessageConsumer = {
|
|
268
|
+
async handleMessage(message: any): Promise<void> {
|
|
269
|
+
if (message.type === "file-returned") {
|
|
270
|
+
files.push({
|
|
271
|
+
fileName: message.fileName,
|
|
272
|
+
content: message.content,
|
|
273
|
+
mimeType: message.mimeType,
|
|
274
|
+
});
|
|
275
|
+
} else if (message.type === "execution-complete") {
|
|
276
|
+
completed = true;
|
|
277
|
+
cleanup();
|
|
278
|
+
resolve({
|
|
279
|
+
success: true,
|
|
280
|
+
console: consoleProvider.getLogs(),
|
|
281
|
+
files,
|
|
282
|
+
returnValue: message.returnValue,
|
|
283
|
+
});
|
|
284
|
+
} else if (message.type === "execution-error") {
|
|
285
|
+
completed = true;
|
|
286
|
+
cleanup();
|
|
287
|
+
resolve({ success: false, console: consoleProvider.getLogs(), error: message.error, files });
|
|
288
|
+
}
|
|
289
|
+
},
|
|
290
|
+
};
|
|
291
|
+
|
|
292
|
+
RUNTIME_MESSAGE_ROUTER.addConsumer(sandboxId, executionConsumer);
|
|
293
|
+
|
|
294
|
+
const cleanup = () => {
|
|
295
|
+
// Notify providers that execution has ended
|
|
296
|
+
for (const provider of providers) {
|
|
297
|
+
provider.onExecutionEnd?.(sandboxId);
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
RUNTIME_MESSAGE_ROUTER.unregisterSandbox(sandboxId);
|
|
301
|
+
signal?.removeEventListener("abort", abortHandler);
|
|
302
|
+
clearTimeout(timeoutId);
|
|
303
|
+
this.iframe?.remove();
|
|
304
|
+
this.iframe = undefined;
|
|
305
|
+
};
|
|
306
|
+
|
|
307
|
+
// Abort handler
|
|
308
|
+
const abortHandler = () => {
|
|
309
|
+
if (!completed) {
|
|
310
|
+
completed = true;
|
|
311
|
+
cleanup();
|
|
312
|
+
reject(new Error("Execution aborted"));
|
|
313
|
+
}
|
|
314
|
+
};
|
|
315
|
+
|
|
316
|
+
if (signal) {
|
|
317
|
+
signal.addEventListener("abort", abortHandler);
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
// Timeout handler (30 seconds)
|
|
321
|
+
const timeoutId = setTimeout(() => {
|
|
322
|
+
if (!completed) {
|
|
323
|
+
completed = true;
|
|
324
|
+
cleanup();
|
|
325
|
+
resolve({
|
|
326
|
+
success: false,
|
|
327
|
+
console: consoleProvider.getLogs(),
|
|
328
|
+
error: { message: "Execution timeout (120s)", stack: "" },
|
|
329
|
+
files,
|
|
330
|
+
});
|
|
331
|
+
}
|
|
332
|
+
}, 120000);
|
|
333
|
+
|
|
334
|
+
// 4. Prepare HTML and create iframe
|
|
335
|
+
const completeHtml = this.prepareHtmlDocument(sandboxId, code, providers, {
|
|
336
|
+
isHtmlArtifact,
|
|
337
|
+
isStandalone: false,
|
|
338
|
+
});
|
|
339
|
+
|
|
340
|
+
// 5. Validate HTML before sending to sandbox
|
|
341
|
+
const validationError = this.validateHtml(completeHtml);
|
|
342
|
+
if (validationError) {
|
|
343
|
+
reject(new Error(`HTML validation failed: ${validationError}`));
|
|
344
|
+
return;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
if (this.sandboxUrlProvider) {
|
|
348
|
+
// Browser extension mode: wait for sandbox-ready
|
|
349
|
+
this.iframe = document.createElement("iframe");
|
|
350
|
+
this.iframe.sandbox.add("allow-scripts", "allow-modals");
|
|
351
|
+
this.iframe.style.cssText = "width: 100%; height: 100%; border: none;";
|
|
352
|
+
this.iframe.src = this.sandboxUrlProvider();
|
|
353
|
+
|
|
354
|
+
// Update router with iframe reference BEFORE appending to DOM
|
|
355
|
+
RUNTIME_MESSAGE_ROUTER.setSandboxIframe(sandboxId, this.iframe);
|
|
356
|
+
|
|
357
|
+
// Listen for sandbox-ready and sandbox-error messages
|
|
358
|
+
const readyHandler = (e: MessageEvent) => {
|
|
359
|
+
if (e.data.type === "sandbox-ready" && e.source === this.iframe?.contentWindow) {
|
|
360
|
+
window.removeEventListener("message", readyHandler);
|
|
361
|
+
window.removeEventListener("message", errorHandler);
|
|
362
|
+
|
|
363
|
+
// Send content to sandbox
|
|
364
|
+
this.iframe?.contentWindow?.postMessage(
|
|
365
|
+
{
|
|
366
|
+
type: "sandbox-load",
|
|
367
|
+
sandboxId,
|
|
368
|
+
code: completeHtml,
|
|
369
|
+
},
|
|
370
|
+
"*",
|
|
371
|
+
);
|
|
372
|
+
}
|
|
373
|
+
};
|
|
374
|
+
|
|
375
|
+
const errorHandler = (e: MessageEvent) => {
|
|
376
|
+
if (e.data.type === "sandbox-error" && e.source === this.iframe?.contentWindow) {
|
|
377
|
+
window.removeEventListener("message", readyHandler);
|
|
378
|
+
window.removeEventListener("message", errorHandler);
|
|
379
|
+
|
|
380
|
+
// Convert sandbox-error to execution-error for the execution consumer
|
|
381
|
+
window.postMessage(
|
|
382
|
+
{
|
|
383
|
+
sandboxId: sandboxId,
|
|
384
|
+
type: "execution-error",
|
|
385
|
+
error: { message: e.data.error, stack: e.data.stack },
|
|
386
|
+
},
|
|
387
|
+
"*",
|
|
388
|
+
);
|
|
389
|
+
}
|
|
390
|
+
};
|
|
391
|
+
|
|
392
|
+
window.addEventListener("message", readyHandler);
|
|
393
|
+
window.addEventListener("message", errorHandler);
|
|
394
|
+
|
|
395
|
+
this.appendChild(this.iframe);
|
|
396
|
+
} else {
|
|
397
|
+
// Web mode: use srcdoc
|
|
398
|
+
this.iframe = document.createElement("iframe");
|
|
399
|
+
this.iframe.sandbox.add("allow-scripts", "allow-modals");
|
|
400
|
+
this.iframe.style.cssText = "width: 100%; height: 100%; border: none; display: none;";
|
|
401
|
+
this.iframe.srcdoc = completeHtml;
|
|
402
|
+
|
|
403
|
+
// Update router with iframe reference BEFORE appending to DOM
|
|
404
|
+
RUNTIME_MESSAGE_ROUTER.setSandboxIframe(sandboxId, this.iframe);
|
|
405
|
+
|
|
406
|
+
this.appendChild(this.iframe);
|
|
407
|
+
}
|
|
408
|
+
});
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
/**
|
|
412
|
+
* Validate HTML using DOMParser - returns error message if invalid, null if valid
|
|
413
|
+
* Note: JavaScript syntax validation is done in sandbox.js to avoid CSP restrictions
|
|
414
|
+
*/
|
|
415
|
+
private validateHtml(html: string): string | null {
|
|
416
|
+
try {
|
|
417
|
+
const parser = new DOMParser();
|
|
418
|
+
const doc = parser.parseFromString(html, "text/html");
|
|
419
|
+
|
|
420
|
+
// Check for parser errors
|
|
421
|
+
const parserError = doc.querySelector("parsererror");
|
|
422
|
+
if (parserError) {
|
|
423
|
+
return parserError.textContent || "Unknown parse error";
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
return null;
|
|
427
|
+
} catch (error: any) {
|
|
428
|
+
return error.message || "Unknown validation error";
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
/**
|
|
433
|
+
* Prepare complete HTML document with runtime + user code
|
|
434
|
+
* PUBLIC so HtmlArtifact can use it for download button
|
|
435
|
+
*/
|
|
436
|
+
public prepareHtmlDocument(
|
|
437
|
+
sandboxId: string,
|
|
438
|
+
userCode: string,
|
|
439
|
+
providers: SandboxRuntimeProvider[] = [],
|
|
440
|
+
options?: PrepareHtmlOptions,
|
|
441
|
+
): string {
|
|
442
|
+
// Default options
|
|
443
|
+
const opts: PrepareHtmlOptions = {
|
|
444
|
+
isHtmlArtifact: false,
|
|
445
|
+
isStandalone: false,
|
|
446
|
+
...options,
|
|
447
|
+
};
|
|
448
|
+
|
|
449
|
+
// Runtime script that will be injected
|
|
450
|
+
const runtime = this.getRuntimeScript(sandboxId, providers, opts.isStandalone || false);
|
|
451
|
+
|
|
452
|
+
// Only check for HTML tags if explicitly marked as HTML artifact
|
|
453
|
+
// For javascript_repl, userCode is JavaScript that may contain HTML in string literals
|
|
454
|
+
if (opts.isHtmlArtifact) {
|
|
455
|
+
// HTML Artifact - inject runtime into existing HTML
|
|
456
|
+
const headMatch = userCode.match(/<head[^>]*>/i);
|
|
457
|
+
if (headMatch) {
|
|
458
|
+
const index = headMatch.index! + headMatch[0].length;
|
|
459
|
+
return userCode.slice(0, index) + runtime + userCode.slice(index);
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
const htmlMatch = userCode.match(/<html[^>]*>/i);
|
|
463
|
+
if (htmlMatch) {
|
|
464
|
+
const index = htmlMatch.index! + htmlMatch[0].length;
|
|
465
|
+
return userCode.slice(0, index) + runtime + userCode.slice(index);
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
// Fallback: prepend runtime
|
|
469
|
+
return runtime + userCode;
|
|
470
|
+
} else {
|
|
471
|
+
// REPL - wrap code in HTML with runtime and call complete() when done
|
|
472
|
+
// Escape </script> in user code to prevent premature tag closure
|
|
473
|
+
const escapedUserCode = escapeScriptContent(userCode);
|
|
474
|
+
|
|
475
|
+
return `<!DOCTYPE html>
|
|
476
|
+
<html>
|
|
477
|
+
<head>
|
|
478
|
+
${runtime}
|
|
479
|
+
</head>
|
|
480
|
+
<body>
|
|
481
|
+
<script type="module">
|
|
482
|
+
(async () => {
|
|
483
|
+
try {
|
|
484
|
+
// Wrap user code in async function to capture return value
|
|
485
|
+
const userCodeFunc = async () => {
|
|
486
|
+
${escapedUserCode}
|
|
487
|
+
};
|
|
488
|
+
|
|
489
|
+
const returnValue = await userCodeFunc();
|
|
490
|
+
|
|
491
|
+
// Call completion callbacks before complete()
|
|
492
|
+
if (window.__completionCallbacks && window.__completionCallbacks.length > 0) {
|
|
493
|
+
try {
|
|
494
|
+
await Promise.all(window.__completionCallbacks.map(cb => cb(true)));
|
|
495
|
+
} catch (e) {
|
|
496
|
+
console.error('Completion callback error:', e);
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
await window.complete(null, returnValue);
|
|
501
|
+
} catch (error) {
|
|
502
|
+
|
|
503
|
+
// Call completion callbacks before complete() (error path)
|
|
504
|
+
if (window.__completionCallbacks && window.__completionCallbacks.length > 0) {
|
|
505
|
+
try {
|
|
506
|
+
await Promise.all(window.__completionCallbacks.map(cb => cb(false)));
|
|
507
|
+
} catch (e) {
|
|
508
|
+
console.error('Completion callback error:', e);
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
await window.complete({
|
|
513
|
+
message: error?.message || String(error),
|
|
514
|
+
stack: error?.stack || new Error().stack
|
|
515
|
+
});
|
|
516
|
+
}
|
|
517
|
+
})();
|
|
518
|
+
</script>
|
|
519
|
+
</body>
|
|
520
|
+
</html>`;
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
/**
|
|
525
|
+
* Generate runtime script from providers
|
|
526
|
+
* @param sandboxId Unique sandbox ID
|
|
527
|
+
* @param providers Runtime providers
|
|
528
|
+
* @param isStandalone If true, skip runtime bridge and navigation interceptor (for standalone downloads)
|
|
529
|
+
*/
|
|
530
|
+
private getRuntimeScript(
|
|
531
|
+
sandboxId: string,
|
|
532
|
+
providers: SandboxRuntimeProvider[] = [],
|
|
533
|
+
isStandalone: boolean = false,
|
|
534
|
+
): string {
|
|
535
|
+
// Collect all data from providers
|
|
536
|
+
const allData: Record<string, any> = {};
|
|
537
|
+
for (const provider of providers) {
|
|
538
|
+
Object.assign(allData, provider.getData());
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
// Generate bridge code (skip if standalone)
|
|
542
|
+
const bridgeCode = isStandalone
|
|
543
|
+
? ""
|
|
544
|
+
: RuntimeMessageBridge.generateBridgeCode({
|
|
545
|
+
context: "sandbox-iframe",
|
|
546
|
+
sandboxId,
|
|
547
|
+
});
|
|
548
|
+
|
|
549
|
+
// Collect all runtime functions - pass sandboxId as string literal
|
|
550
|
+
const runtimeFunctions: string[] = [];
|
|
551
|
+
for (const provider of providers) {
|
|
552
|
+
runtimeFunctions.push(`(${provider.getRuntime().toString()})(${JSON.stringify(sandboxId)});`);
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
// Build script with HTML escaping
|
|
556
|
+
// Escape </script> to prevent premature tag closure in HTML parser
|
|
557
|
+
const dataInjection = Object.entries(allData)
|
|
558
|
+
.map(([key, value]) => {
|
|
559
|
+
const jsonStr = JSON.stringify(value).replace(/<\/script/gi, "<\\/script");
|
|
560
|
+
return `window.${key} = ${jsonStr};`;
|
|
561
|
+
})
|
|
562
|
+
.join("\n");
|
|
563
|
+
|
|
564
|
+
// TODO the font-size is needed, as chrome seems to inject a stylesheet into iframes
|
|
565
|
+
// found in an extension context like sidepanel, settin body { font-size: 75% }. It's
|
|
566
|
+
// definitely not our code doing that.
|
|
567
|
+
// See https://stackoverflow.com/questions/71480433/chrome-is-injecting-some-stylesheet-in-popup-ui-which-reduces-the-font-size-to-7
|
|
568
|
+
|
|
569
|
+
// Navigation interceptor (only if NOT standalone)
|
|
570
|
+
const navigationInterceptor = isStandalone
|
|
571
|
+
? ""
|
|
572
|
+
: `
|
|
573
|
+
// Navigation interceptor: prevent all navigation and open externally
|
|
574
|
+
(function() {
|
|
575
|
+
// Intercept link clicks
|
|
576
|
+
document.addEventListener('click', function(e) {
|
|
577
|
+
const link = e.target.closest('a');
|
|
578
|
+
if (link && link.href) {
|
|
579
|
+
// Check if it's an external link (not javascript: or #hash)
|
|
580
|
+
if (link.href.startsWith('http://') || link.href.startsWith('https://')) {
|
|
581
|
+
e.preventDefault();
|
|
582
|
+
e.stopPropagation();
|
|
583
|
+
window.parent.postMessage({ type: 'open-external-url', url: link.href }, '*');
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
}, true);
|
|
587
|
+
|
|
588
|
+
// Intercept form submissions
|
|
589
|
+
document.addEventListener('submit', function(e) {
|
|
590
|
+
const form = e.target;
|
|
591
|
+
if (form && form.action) {
|
|
592
|
+
e.preventDefault();
|
|
593
|
+
e.stopPropagation();
|
|
594
|
+
window.parent.postMessage({ type: 'open-external-url', url: form.action }, '*');
|
|
595
|
+
}
|
|
596
|
+
}, true);
|
|
597
|
+
|
|
598
|
+
// Prevent window.location changes (only if not already redefined)
|
|
599
|
+
try {
|
|
600
|
+
const originalLocation = window.location;
|
|
601
|
+
Object.defineProperty(window, 'location', {
|
|
602
|
+
get: function() { return originalLocation; },
|
|
603
|
+
set: function(url) {
|
|
604
|
+
window.parent.postMessage({ type: 'open-external-url', url: url.toString() }, '*');
|
|
605
|
+
}
|
|
606
|
+
});
|
|
607
|
+
} catch (e) {
|
|
608
|
+
// Already defined, skip
|
|
609
|
+
}
|
|
610
|
+
})();
|
|
611
|
+
`;
|
|
612
|
+
|
|
613
|
+
return `<style>
|
|
614
|
+
html, body {
|
|
615
|
+
font-size: initial;
|
|
616
|
+
}
|
|
617
|
+
</style>
|
|
618
|
+
<script>
|
|
619
|
+
window.sandboxId = ${JSON.stringify(sandboxId)};
|
|
620
|
+
${dataInjection}
|
|
621
|
+
${bridgeCode}
|
|
622
|
+
${runtimeFunctions.join("\n")}
|
|
623
|
+
${navigationInterceptor}
|
|
624
|
+
</script>`;
|
|
625
|
+
}
|
|
626
|
+
}
|