From 4d2ca6ab2aae9690ff6a26eeafa117d9d42b3fa7 Mon Sep 17 00:00:00 2001 From: Mario Zechner Date: Thu, 9 Oct 2025 04:07:59 +0200 Subject: [PATCH] Add artifact message persistence for session reconstruction - Add ArtifactMessage type as core part of AppMessage union (not CustomMessages) - ArtifactsRuntimeProvider appends artifact messages on create/update/delete - MessageList filters out artifact messages (UI display only) - artifacts.ts reconstructFromMessages handles artifact messages - Export ARTIFACTS_RUNTIME_PROVIDER_DESCRIPTION from main index - Fix artifact creation bug: pass filename as title instead of mimeType Changes: - web-ui/src/components/Messages.ts: Add ArtifactMessage to BaseMessage union - web-ui/src/components/MessageList.ts: Skip artifact messages in render - web-ui/src/components/sandbox/ArtifactsRuntimeProvider.ts: Append messages, fix title parameter - web-ui/src/ChatPanel.ts: Pass agent.appendMessage callback - web-ui/src/tools/artifacts/artifacts.ts: Handle artifact messages in reconstructFromMessages - web-ui/src/index.ts: Export ARTIFACTS_RUNTIME_PROVIDER_DESCRIPTION - web-ui/example/src/custom-messages.ts: Update message transformer to filter artifacts --- .../web-ui/example/src/custom-messages.ts | 5 + packages/web-ui/example/src/main.ts | 9 +- packages/web-ui/src/ChatPanel.ts | 104 +++++--- packages/web-ui/src/agent/agent.ts | 4 +- packages/web-ui/src/components/MessageList.ts | 5 + packages/web-ui/src/components/Messages.ts | 12 +- .../web-ui/src/components/SandboxedIframe.ts | 2 +- .../sandbox/ArtifactsRuntimeProvider.ts | 232 ++++++++++++++++ .../sandbox/AttachmentsRuntimeProvider.ts | 5 + .../sandbox/SandboxMessageRouter.ts | 8 +- .../sandbox/SandboxRuntimeProvider.ts | 8 +- packages/web-ui/src/index.ts | 2 + packages/web-ui/src/prompts/tool-prompts.ts | 251 ++++++++++++++++++ .../src/tools/artifacts/ArtifactElement.ts | 1 - .../src/tools/artifacts/HtmlArtifact.ts | 3 +- .../src/tools/artifacts/MarkdownArtifact.ts | 1 - .../web-ui/src/tools/artifacts/SvgArtifact.ts | 1 - .../src/tools/artifacts/TextArtifact.ts | 1 - .../web-ui/src/tools/artifacts/artifacts.ts | 173 ++++-------- packages/web-ui/src/tools/javascript-repl.ts | 81 +----- 20 files changed, 669 insertions(+), 239 deletions(-) create mode 100644 packages/web-ui/src/components/sandbox/ArtifactsRuntimeProvider.ts create mode 100644 packages/web-ui/src/prompts/tool-prompts.ts diff --git a/packages/web-ui/example/src/custom-messages.ts b/packages/web-ui/example/src/custom-messages.ts index 876785e0..a7c6fa45 100644 --- a/packages/web-ui/example/src/custom-messages.ts +++ b/packages/web-ui/example/src/custom-messages.ts @@ -78,6 +78,11 @@ export function createSystemNotification( export function customMessageTransformer(messages: AppMessage[]): Message[] { return messages .filter((m) => { + // Filter out artifact messages - they're for session reconstruction only + if (m.role === "artifact") { + return false; + } + // Keep LLM-compatible messages + custom messages return ( m.role === "user" || diff --git a/packages/web-ui/example/src/main.ts b/packages/web-ui/example/src/main.ts index aaeb7d0e..bbb7b09c 100644 --- a/packages/web-ui/example/src/main.ts +++ b/packages/web-ui/example/src/main.ts @@ -194,7 +194,11 @@ Feel free to use these tools when needed to provide accurate and helpful respons } }); - await chatPanel.setAgent(agent); + await chatPanel.setAgent(agent, { + onApiKeyRequired: async (provider: string) => { + return await ApiKeyPromptDialog.prompt(provider); + } + }); }; const loadSession = async (sessionId: string): Promise => { @@ -377,9 +381,6 @@ async function initApp() { // Create ChatPanel chatPanel = new ChatPanel(); - chatPanel.onApiKeyRequired = async (provider: string) => { - return await ApiKeyPromptDialog.prompt(provider); - }; // Check for session in URL const urlParams = new URLSearchParams(window.location.search); diff --git a/packages/web-ui/src/ChatPanel.ts b/packages/web-ui/src/ChatPanel.ts index 9db4eb74..7bc78392 100644 --- a/packages/web-ui/src/ChatPanel.ts +++ b/packages/web-ui/src/ChatPanel.ts @@ -1,9 +1,11 @@ import { Badge, html } from "@mariozechner/mini-lit"; import { LitElement } from "lit"; -import { customElement, property, state } from "lit/decorators.js"; +import { customElement, state } from "lit/decorators.js"; import type { Agent } from "./agent/agent.js"; import "./components/AgentInterface.js"; +import type { AgentTool } from "@mariozechner/pi-ai"; import type { AgentInterface } from "./components/AgentInterface.js"; +import { ArtifactsRuntimeProvider } from "./components/sandbox/ArtifactsRuntimeProvider.js"; import { AttachmentsRuntimeProvider } from "./components/sandbox/AttachmentsRuntimeProvider.js"; import type { SandboxRuntimeProvider } from "./components/sandbox/SandboxRuntimeProvider.js"; import { ArtifactsPanel, ArtifactsToolRenderer } from "./tools/artifacts/index.js"; @@ -23,25 +25,6 @@ export class ChatPanel extends LitElement { @state() private artifactCount = 0; @state() private showArtifactsPanel = false; @state() private windowWidth = 0; - @property({ attribute: false }) runtimeProvidersFactory = () => { - const attachments: Attachment[] = []; - for (const message of this.agent!.state.messages) { - if (message.role === "user") { - message.attachments?.forEach((a) => { - attachments.push(a); - }); - } - } - const providers: SandboxRuntimeProvider[] = []; - if (attachments.length > 0) { - providers.push(new AttachmentsRuntimeProvider(attachments)); - } - return providers; - }; - @property({ attribute: false }) sandboxUrlProvider?: () => string; - @property({ attribute: false }) onApiKeyRequired?: (provider: string) => Promise; - @property({ attribute: false }) onBeforeSend?: () => void | Promise; - @property({ attribute: false }) additionalTools?: any[]; private resizeHandler = () => { this.windowWidth = window.innerWidth; @@ -72,7 +55,19 @@ export class ChatPanel extends LitElement { window.removeEventListener("resize", this.resizeHandler); } - async setAgent(agent: Agent) { + async setAgent( + agent: Agent, + config?: { + onApiKeyRequired?: (provider: string) => Promise; + onBeforeSend?: () => void | Promise; + sandboxUrlProvider?: () => string; + toolsFactory?: ( + agent: Agent, + agentInterface: AgentInterface, + artifactsPanel: ArtifactsPanel, + ) => AgentTool[]; + }, + ) { this.agent = agent; // Create AgentInterface @@ -82,26 +77,74 @@ export class ChatPanel extends LitElement { this.agentInterface.enableModelSelector = true; this.agentInterface.enableThinkingSelector = true; this.agentInterface.showThemeToggle = false; - this.agentInterface.onApiKeyRequired = this.onApiKeyRequired; - this.agentInterface.onBeforeSend = this.onBeforeSend; + this.agentInterface.onApiKeyRequired = config?.onApiKeyRequired; + this.agentInterface.onBeforeSend = config?.onBeforeSend; // Create JavaScript REPL tool const javascriptReplTool = createJavaScriptReplTool(); - if (this.sandboxUrlProvider) { - javascriptReplTool.sandboxUrlProvider = this.sandboxUrlProvider; + if (config?.sandboxUrlProvider) { + javascriptReplTool.sandboxUrlProvider = config.sandboxUrlProvider; } // Set up artifacts panel this.artifactsPanel = new ArtifactsPanel(); - if (this.sandboxUrlProvider) { - this.artifactsPanel.sandboxUrlProvider = this.sandboxUrlProvider; + if (config?.sandboxUrlProvider) { + this.artifactsPanel.sandboxUrlProvider = config.sandboxUrlProvider; } // Register the standalone tool renderer (not the panel itself) registerToolRenderer("artifacts", new ArtifactsToolRenderer(this.artifactsPanel)); // Runtime providers factory - javascriptReplTool.runtimeProvidersFactory = this.runtimeProvidersFactory; - this.artifactsPanel.runtimeProvidersFactory = this.runtimeProvidersFactory; + const runtimeProvidersFactory = () => { + const attachments: Attachment[] = []; + for (const message of this.agent!.state.messages) { + if (message.role === "user") { + message.attachments?.forEach((a) => { + attachments.push(a); + }); + } + } + const providers: SandboxRuntimeProvider[] = []; + + // Add attachments provider if there are attachments + if (attachments.length > 0) { + providers.push(new AttachmentsRuntimeProvider(attachments)); + } + + // Add artifacts provider (always available) + providers.push( + new ArtifactsRuntimeProvider( + () => this.artifactsPanel!.artifacts, + async (filename: string, content: string) => { + await this.artifactsPanel!.tool.execute("", { + command: "create", + filename, + content, + }); + }, + async (filename: string, content: string) => { + await this.artifactsPanel!.tool.execute("", { + command: "rewrite", + filename, + content, + }); + }, + async (filename: string) => { + await this.artifactsPanel!.tool.execute("", { + command: "delete", + filename, + }); + }, + (message: any) => { + this.agent!.appendMessage(message); + }, + ), + ); + + return providers; + }; + javascriptReplTool.runtimeProvidersFactory = runtimeProvidersFactory; + this.artifactsPanel.runtimeProvidersFactory = runtimeProvidersFactory; this.artifactsPanel.onArtifactsChange = () => { const count = this.artifactsPanel?.artifacts?.size ?? 0; @@ -125,7 +168,8 @@ export class ChatPanel extends LitElement { }; // Set tools on the agent - const tools = [javascriptReplTool, this.artifactsPanel.tool, ...(this.additionalTools || [])]; + const additionalTools = config?.toolsFactory?.(agent, this.agentInterface, this.artifactsPanel) || []; + const tools = [javascriptReplTool, this.artifactsPanel.tool, ...additionalTools]; this.agent.setTools(tools); // Reconstruct artifacts from existing messages diff --git a/packages/web-ui/src/agent/agent.ts b/packages/web-ui/src/agent/agent.ts index befc59d1..abf98eba 100644 --- a/packages/web-ui/src/agent/agent.ts +++ b/packages/web-ui/src/agent/agent.ts @@ -293,10 +293,10 @@ export class Agent { this.patch({ isStreaming: false, streamMessage: null, pendingToolCalls: new Set() }); this.abortController = undefined; } - /*{ + { const { systemPrompt, model, messages } = this._state; console.log("final state:", { systemPrompt, model, messages }); - }*/ + } } private patch(p: Partial): void { diff --git a/packages/web-ui/src/components/MessageList.ts b/packages/web-ui/src/components/MessageList.ts index 28628c48..59d2567f 100644 --- a/packages/web-ui/src/components/MessageList.ts +++ b/packages/web-ui/src/components/MessageList.ts @@ -37,6 +37,11 @@ export class MessageList extends LitElement { const items: Array<{ key: string; template: TemplateResult }> = []; let index = 0; for (const msg of this.messages) { + // Skip artifact messages - they're for session persistence only, not UI display + if (msg.role === "artifact") { + continue; + } + // Try custom renderer first const customTemplate = renderMessage(msg); if (customTemplate) { diff --git a/packages/web-ui/src/components/Messages.ts b/packages/web-ui/src/components/Messages.ts index 4261f85f..8e121123 100644 --- a/packages/web-ui/src/components/Messages.ts +++ b/packages/web-ui/src/components/Messages.ts @@ -15,8 +15,18 @@ import { i18n } from "../utils/i18n.js"; export type UserMessageWithAttachments = UserMessageType & { attachments?: Attachment[] }; +// Artifact message type for session persistence +export interface ArtifactMessage { + role: "artifact"; + action: "create" | "update" | "delete"; + filename: string; + content?: string; + title?: string; + timestamp: string; +} + // Base message union -type BaseMessage = AssistantMessageType | UserMessageWithAttachments | ToolResultMessageType; +type BaseMessage = AssistantMessageType | UserMessageWithAttachments | ToolResultMessageType | ArtifactMessage; // Extensible interface - apps can extend via declaration merging // Example: diff --git a/packages/web-ui/src/components/SandboxedIframe.ts b/packages/web-ui/src/components/SandboxedIframe.ts index 4f17c267..a3379b8f 100644 --- a/packages/web-ui/src/components/SandboxedIframe.ts +++ b/packages/web-ui/src/components/SandboxedIframe.ts @@ -169,7 +169,7 @@ export class SandboxIframe extends LitElement { return new Promise((resolve, reject) => { // 4. Create execution consumer for lifecycle messages const executionConsumer: MessageConsumer = { - handleMessage(message: any): boolean { + async handleMessage(message: any): Promise { if (message.type === "console") { logs.push({ type: message.method === "error" ? "error" : "log", diff --git a/packages/web-ui/src/components/sandbox/ArtifactsRuntimeProvider.ts b/packages/web-ui/src/components/sandbox/ArtifactsRuntimeProvider.ts new file mode 100644 index 00000000..413f537e --- /dev/null +++ b/packages/web-ui/src/components/sandbox/ArtifactsRuntimeProvider.ts @@ -0,0 +1,232 @@ +import { ARTIFACTS_RUNTIME_PROVIDER_DESCRIPTION } from "../../prompts/tool-prompts.js"; +import type { SandboxRuntimeProvider } from "./SandboxRuntimeProvider.js"; + +/** + * Artifacts Runtime Provider + * + * Provides programmatic access to session artifacts from sandboxed code. + * Allows code to create, read, update, and delete artifacts dynamically. + */ +export class ArtifactsRuntimeProvider implements SandboxRuntimeProvider { + constructor( + private getArtifactsFn: () => Map, + private createArtifactFn: (filename: string, content: string, title?: string) => Promise, + private updateArtifactFn: (filename: string, content: string, title?: string) => Promise, + private deleteArtifactFn: (filename: string) => Promise, + private appendMessageFn?: (message: any) => void, + ) {} + + getData(): Record { + // No initial data injection needed - artifacts are accessed via async functions + return {}; + } + + getRuntime(): (sandboxId: string) => void { + // This function will be stringified, so no external references! + return (sandboxId: string) => { + // Helper to send message and wait for response + const sendArtifactMessage = (action: string, data: any): Promise => { + console.log("Sending artifact message:", action, data); + return new Promise((resolve, reject) => { + const messageId = `artifact_${Date.now()}_${Math.random().toString(36).substring(2, 9)}`; + + const handler = (event: MessageEvent) => { + if (event.data.type === "artifact-response" && event.data.messageId === messageId) { + window.removeEventListener("message", handler); + if (event.data.success) { + resolve(event.data.result); + } else { + reject(new Error(event.data.error || "Artifact operation failed")); + } + } + }; + + window.addEventListener("message", handler); + + window.parent.postMessage( + { + type: "artifact-operation", + sandboxId, + messageId, + action, + data, + }, + "*", + ); + }); + }; + + // Auto-parse/stringify for .json files + const isJsonFile = (filename: string) => filename.endsWith(".json"); + + (window as any).hasArtifact = async (filename: string): Promise => { + return await sendArtifactMessage("has", { filename }); + }; + + (window as any).getArtifact = async (filename: string): Promise => { + const content = await sendArtifactMessage("get", { filename }); + // Auto-parse .json files + if (isJsonFile(filename)) { + try { + return JSON.parse(content); + } catch (e) { + throw new Error(`Failed to parse JSON from ${filename}: ${e}`); + } + } + return content; + }; + + (window as any).createArtifact = async (filename: string, content: any, mimeType?: string): Promise => { + let finalContent = content; + let finalMimeType = mimeType; + + // Auto-stringify .json files + if (isJsonFile(filename) && typeof content !== "string") { + finalContent = JSON.stringify(content, null, 2); + finalMimeType = mimeType || "application/json"; + } else if (typeof content === "string") { + finalContent = content; + finalMimeType = mimeType || "text/plain"; + } else { + finalContent = JSON.stringify(content, null, 2); + finalMimeType = mimeType || "application/json"; + } + + await sendArtifactMessage("create", { filename, content: finalContent, mimeType: finalMimeType }); + }; + + (window as any).updateArtifact = async (filename: string, content: any, mimeType?: string): Promise => { + let finalContent = content; + let finalMimeType = mimeType; + + // Auto-stringify .json files + if (isJsonFile(filename) && typeof content !== "string") { + finalContent = JSON.stringify(content, null, 2); + finalMimeType = mimeType || "application/json"; + } else if (typeof content === "string") { + finalContent = content; + finalMimeType = mimeType || "text/plain"; + } else { + finalContent = JSON.stringify(content, null, 2); + finalMimeType = mimeType || "application/json"; + } + + await sendArtifactMessage("update", { filename, content: finalContent, mimeType: finalMimeType }); + }; + + (window as any).deleteArtifact = async (filename: string): Promise => { + await sendArtifactMessage("delete", { filename }); + }; + }; + } + + async handleMessage(message: any, respond: (response: any) => void): Promise { + if (message.type !== "artifact-operation") { + return false; + } + + const { action, data, messageId } = message; + + const sendResponse = (success: boolean, result?: any, error?: string) => { + respond({ + type: "artifact-response", + messageId, + success, + result, + error, + }); + }; + + try { + switch (action) { + case "has": { + const artifacts = this.getArtifactsFn(); + const exists = artifacts.has(data.filename); + sendResponse(true, exists); + break; + } + + case "get": { + const artifacts = this.getArtifactsFn(); + const artifact = artifacts.get(data.filename); + if (!artifact) { + sendResponse(false, undefined, `Artifact not found: ${data.filename}`); + } else { + sendResponse(true, artifact.content); + } + break; + } + + case "create": { + try { + // Note: mimeType parameter is ignored - artifact type is inferred from filename extension + // Third parameter is title, defaults to filename + await this.createArtifactFn(data.filename, data.content, data.filename); + // Append artifact message for session persistence + this.appendMessageFn?.({ + role: "artifact", + action: "create", + filename: data.filename, + content: data.content, + title: data.filename, + timestamp: new Date().toISOString(), + }); + sendResponse(true); + } catch (err: any) { + sendResponse(false, undefined, err.message); + } + break; + } + + case "update": { + try { + // Note: mimeType parameter is ignored - artifact type is inferred from filename extension + // Third parameter is title, defaults to filename + await this.updateArtifactFn(data.filename, data.content, data.filename); + // Append artifact message for session persistence + this.appendMessageFn?.({ + role: "artifact", + action: "update", + filename: data.filename, + content: data.content, + timestamp: new Date().toISOString(), + }); + sendResponse(true); + } catch (err: any) { + sendResponse(false, undefined, err.message); + } + break; + } + + case "delete": { + try { + await this.deleteArtifactFn(data.filename); + // Append artifact message for session persistence + this.appendMessageFn?.({ + role: "artifact", + action: "delete", + filename: data.filename, + timestamp: new Date().toISOString(), + }); + sendResponse(true); + } catch (err: any) { + sendResponse(false, undefined, err.message); + } + break; + } + + default: + sendResponse(false, undefined, `Unknown artifact action: ${action}`); + } + + return true; + } catch (error: any) { + sendResponse(false, undefined, error.message); + return true; + } + } + + getDescription(): string { + return ARTIFACTS_RUNTIME_PROVIDER_DESCRIPTION; + } +} diff --git a/packages/web-ui/src/components/sandbox/AttachmentsRuntimeProvider.ts b/packages/web-ui/src/components/sandbox/AttachmentsRuntimeProvider.ts index a6628dca..ee5f9662 100644 --- a/packages/web-ui/src/components/sandbox/AttachmentsRuntimeProvider.ts +++ b/packages/web-ui/src/components/sandbox/AttachmentsRuntimeProvider.ts @@ -1,3 +1,4 @@ +import { ATTACHMENTS_RUNTIME_DESCRIPTION } from "../../prompts/tool-prompts.js"; import type { Attachment } from "../../utils/attachment-utils.js"; import type { SandboxRuntimeProvider } from "./SandboxRuntimeProvider.js"; @@ -96,4 +97,8 @@ export class AttachmentsRuntimeProvider implements SandboxRuntimeProvider { }; }; } + + getDescription(): string { + return ATTACHMENTS_RUNTIME_DESCRIPTION; + } } diff --git a/packages/web-ui/src/components/sandbox/SandboxMessageRouter.ts b/packages/web-ui/src/components/sandbox/SandboxMessageRouter.ts index 6c3c4450..d4896a72 100644 --- a/packages/web-ui/src/components/sandbox/SandboxMessageRouter.ts +++ b/packages/web-ui/src/components/sandbox/SandboxMessageRouter.ts @@ -8,7 +8,7 @@ export interface MessageConsumer { * Handle a message from a sandbox. * @returns true if message was consumed (stops propagation), false otherwise */ - handleMessage(message: any): boolean; + handleMessage(message: any): Promise; } /** @@ -111,7 +111,7 @@ export class SandboxMessageRouter { private setupListener(): void { if (this.messageListener) return; - this.messageListener = (e: MessageEvent) => { + this.messageListener = async (e: MessageEvent) => { const { sandboxId } = e.data; if (!sandboxId) return; @@ -129,14 +129,14 @@ export class SandboxMessageRouter { // 1. Try provider handlers first (for bidirectional comm like memory) for (const provider of context.providers) { if (provider.handleMessage) { - const handled = provider.handleMessage(e.data, respond); + const handled = await provider.handleMessage(e.data, respond); if (handled) return; // Stop if handled } } // 2. Broadcast to consumers (for one-way messages like console) for (const consumer of context.consumers) { - const consumed = consumer.handleMessage(e.data); + const consumed = await consumer.handleMessage(e.data); if (consumed) break; // Stop if consumed } }; diff --git a/packages/web-ui/src/components/sandbox/SandboxRuntimeProvider.ts b/packages/web-ui/src/components/sandbox/SandboxRuntimeProvider.ts index e1a4acb1..dd0831aa 100644 --- a/packages/web-ui/src/components/sandbox/SandboxRuntimeProvider.ts +++ b/packages/web-ui/src/components/sandbox/SandboxRuntimeProvider.ts @@ -26,5 +26,11 @@ export interface SandboxRuntimeProvider { * @param respond - Function to send a response back to the sandbox * @returns true if message was handled, false otherwise */ - handleMessage?(message: any, respond: (response: any) => void): boolean; + handleMessage?(message: any, respond: (response: any) => void): Promise; + + /** + * Optional documentation describing what globals/functions this provider injects. + * This will be appended to tool descriptions dynamically so the LLM knows what's available. + */ + getDescription?(): string; } diff --git a/packages/web-ui/src/index.ts b/packages/web-ui/src/index.ts index 0e0f2990..334da2e3 100644 --- a/packages/web-ui/src/index.ts +++ b/packages/web-ui/src/index.ts @@ -42,6 +42,8 @@ export { ModelSelector } from "./dialogs/ModelSelector.js"; export { PersistentStorageDialog } from "./dialogs/PersistentStorageDialog.js"; export { SessionListDialog } from "./dialogs/SessionListDialog.js"; export { ApiKeysTab, ProxyTab, SettingsDialog, SettingsTab } from "./dialogs/SettingsDialog.js"; +// Prompts +export { ARTIFACTS_RUNTIME_PROVIDER_DESCRIPTION } from "./prompts/tool-prompts.js"; // Storage export { AppStorage, getAppStorage, setAppStorage } from "./storage/app-storage.js"; export { IndexedDBStorageBackend } from "./storage/backends/indexeddb-storage-backend.js"; diff --git a/packages/web-ui/src/prompts/tool-prompts.ts b/packages/web-ui/src/prompts/tool-prompts.ts new file mode 100644 index 00000000..305475c4 --- /dev/null +++ b/packages/web-ui/src/prompts/tool-prompts.ts @@ -0,0 +1,251 @@ +/** + * Centralized tool prompts/descriptions. + * Each prompt is either a string constant or a template function. + */ + +// ============================================================================ +// JavaScript REPL Tool +// ============================================================================ + +export const JAVASCRIPT_REPL_BASE_DESCRIPTION = `Execute JavaScript code in a sandboxed browser environment with full modern browser capabilities. + +Environment: Modern browser with ALL Web APIs available: +- ES2023+ JavaScript (async/await, optional chaining, nullish coalescing, etc.) +- DOM APIs (document, window, Canvas, WebGL, etc.) +- Fetch API for HTTP requests + +Loading external libraries via dynamic imports (use esm.run): +- XLSX (Excel files): const XLSX = await import('https://esm.run/xlsx'); +- Papa Parse (CSV): const Papa = (await import('https://esm.run/papaparse')).default; +- Lodash: const _ = await import('https://esm.run/lodash-es'); +- D3.js: const d3 = await import('https://esm.run/d3'); +- Chart.js: const Chart = (await import('https://esm.run/chart.js/auto')).default; +- Three.js: const THREE = await import('https://esm.run/three'); +- Any npm package: await import('https://esm.run/package-name') + +IMPORTANT for graphics/canvas: +- Use fixed dimensions like 400x400 or 800x600, NOT window.innerWidth/Height +- For Three.js: renderer.setSize(400, 400) and camera aspect ratio of 1 +- For Chart.js: Set options: { responsive: false, animation: false } to ensure immediate rendering +- Web Storage (localStorage, sessionStorage, IndexedDB) +- Web Workers, WebAssembly, WebSockets +- Media APIs (Audio, Video, WebRTC) +- File APIs (Blob, FileReader, etc.) +- Crypto API for cryptography +- And much more - anything a modern browser supports! + +Output: +- console.log() - All output is captured as text`; + +export const JAVASCRIPT_REPL_CHART_EXAMPLE = ` + - Chart.js example: + const Chart = (await import('https://esm.run/chart.js/auto')).default; + const canvas = document.createElement('canvas'); + canvas.width = 400; canvas.height = 300; + document.body.appendChild(canvas); + new Chart(canvas, { + type: 'line', + data: { + labels: ['Jan', 'Feb', 'Mar', 'Apr'], + datasets: [{ label: 'Sales', data: [10, 20, 15, 25], borderColor: 'blue' }] + }, + options: { responsive: false, animation: false } + }); + const blob = await new Promise(resolve => canvas.toBlob(resolve, 'image/png')); + await returnFile('chart.png', blob, 'image/png');`; + +export const JAVASCRIPT_REPL_FOOTER = ` + +- All standard browser globals (window, document, fetch, etc.)`; + +/** + * Build complete JavaScript REPL description with optional provider docs. + */ +export function buildJavaScriptReplDescription(providerDocs?: string): string { + return ( + JAVASCRIPT_REPL_BASE_DESCRIPTION + + (providerDocs ? "\n" + providerDocs + JAVASCRIPT_REPL_CHART_EXAMPLE : "") + + JAVASCRIPT_REPL_FOOTER + ); +} + +// ============================================================================ +// Artifacts Tool +// ============================================================================ + +export const ARTIFACTS_BASE_DESCRIPTION = `Creates and manages file artifacts. Each artifact is a file with a filename and content. + +IMPORTANT: Always prefer updating existing files over creating new ones. Check available files first. + +Commands: +1. create: Create a new file + - filename: Name with extension (required, e.g., 'index.html', 'script.js', 'README.md') + - title: Display name for the tab (optional, defaults to filename) + - content: File content (required) + +2. update: Update part of an existing file + - filename: File to update (required) + - old_str: Exact string to replace (required) + - new_str: Replacement string (required) + +3. rewrite: Completely replace a file's content + - filename: File to rewrite (required) + - content: New content (required) + - title: Optionally update display title + +4. get: Retrieve the full content of a file + - filename: File to retrieve (required) + - Returns the complete file content + +5. delete: Delete a file + - filename: File to delete (required) + +6. logs: Get console logs and errors (HTML files only) + - filename: HTML file to get logs for (required) + - Returns all console output and runtime errors`; + +export const ARTIFACTS_RUNTIME_EXAMPLE = `- Example HTML artifact that processes a CSV attachment: + +`; + +export const ARTIFACTS_HTML_SECTION = ` +For text/html artifacts: +- Must be a single self-contained file +- External scripts: Use CDNs like https://esm.sh, https://unpkg.com, or https://cdnjs.cloudflare.com +- Preferred: Use https://esm.sh for npm packages (e.g., https://esm.sh/three for Three.js) +- For ES modules, use: +- For Three.js specifically: import from 'https://esm.sh/three' or 'https://esm.sh/three@0.160.0' +- For addons: import from 'https://esm.sh/three/examples/jsm/controls/OrbitControls.js' +- No localStorage/sessionStorage - use in-memory variables only +- CSS should be included inline +- CRITICAL REMINDER FOR HTML ARTIFACTS: + - ALWAYS set a background color inline in