import { Badge, Button, Diff, icon } from "@mariozechner/mini-lit"; import { type AgentTool, type Message, StringEnum, type ToolCall, type ToolResultMessage } from "@mariozechner/pi-ai"; import { type Static, Type } from "@sinclair/typebox"; import { html, LitElement, type TemplateResult } from "lit"; import { customElement, property, state } from "lit/decorators.js"; import { createRef, type Ref, ref } from "lit/directives/ref.js"; import { X } from "lucide"; import type { Attachment } from "../../utils/attachment-utils.js"; import { i18n } from "../../utils/i18n.js"; import type { ToolRenderer } from "../types.js"; import type { ArtifactElement } from "./ArtifactElement.js"; import { HtmlArtifact } from "./HtmlArtifact.js"; import { MarkdownArtifact } from "./MarkdownArtifact.js"; import { SvgArtifact } from "./SvgArtifact.js"; import { TextArtifact } from "./TextArtifact.js"; import "@mariozechner/mini-lit/dist/MarkdownBlock.js"; import "@mariozechner/mini-lit/dist/CodeBlock.js"; // Simple artifact model export interface Artifact { filename: string; title: string; content: string; createdAt: Date; updatedAt: Date; } // JSON-schema friendly parameters object (LLM-facing) const artifactsParamsSchema = Type.Object({ command: StringEnum(["create", "update", "rewrite", "get", "delete", "logs"], { description: "The operation to perform", }), filename: Type.String({ description: "Filename including extension (e.g., 'index.html', 'script.js')" }), title: Type.Optional(Type.String({ description: "Display title for the tab (defaults to filename)" })), content: Type.Optional(Type.String({ description: "File content" })), old_str: Type.Optional(Type.String({ description: "String to replace (for update command)" })), new_str: Type.Optional(Type.String({ description: "Replacement string (for update command)" })), }); export type ArtifactsParams = Static; // Minimal helper to render plain text outputs consistently function plainOutput(text: string): TemplateResult { return html`
${text}
`; } @customElement("artifacts-panel") export class ArtifactsPanel extends LitElement implements ToolRenderer { @state() private _artifacts = new Map(); @state() private _activeFilename: string | null = null; // Programmatically managed artifact elements private artifactElements = new Map(); private contentRef: Ref = createRef(); // External provider for attachments (decouples panel from AgentInterface) @property({ attribute: false }) attachmentsProvider?: () => Attachment[]; // Callbacks @property({ attribute: false }) onArtifactsChange?: () => void; @property({ attribute: false }) onClose?: () => void; @property({ attribute: false }) onOpen?: () => void; // Collapsed mode: hides panel content but can show a floating reopen pill @property({ type: Boolean }) collapsed = false; // Overlay mode: when true, panel renders full-screen overlay (mobile) @property({ type: Boolean }) overlay = false; // Public getter for artifacts get artifacts() { return this._artifacts; } protected override createRenderRoot(): HTMLElement | DocumentFragment { return this; // light DOM for shared styles } override connectedCallback(): void { super.connectedCallback(); this.style.display = "block"; // Reattach existing artifact elements when panel is re-inserted into the DOM requestAnimationFrame(() => { const container = this.contentRef.value; if (!container) return; // Ensure we have an active filename if (!this._activeFilename && this._artifacts.size > 0) { this._activeFilename = Array.from(this._artifacts.keys())[0]; } this.artifactElements.forEach((element, name) => { if (!element.parentElement) container.appendChild(element); element.style.display = name === this._activeFilename ? "block" : "none"; }); }); } override disconnectedCallback() { super.disconnectedCallback(); // Do not tear down artifact elements; keep them to restore on next mount } // Helper to determine file type from extension private getFileType(filename: string): "html" | "svg" | "markdown" | "text" { const ext = filename.split(".").pop()?.toLowerCase(); if (ext === "html") return "html"; if (ext === "svg") return "svg"; if (ext === "md" || ext === "markdown") return "markdown"; return "text"; } // Helper to determine language for syntax highlighting private getLanguageFromFilename(filename?: string): string { if (!filename) return "text"; const ext = filename.split(".").pop()?.toLowerCase(); const languageMap: Record = { js: "javascript", jsx: "javascript", ts: "typescript", tsx: "typescript", html: "html", css: "css", scss: "scss", json: "json", py: "python", md: "markdown", svg: "xml", xml: "xml", yaml: "yaml", yml: "yaml", sh: "bash", bash: "bash", sql: "sql", java: "java", c: "c", cpp: "cpp", cs: "csharp", go: "go", rs: "rust", php: "php", rb: "ruby", swift: "swift", kt: "kotlin", r: "r", }; return languageMap[ext || ""] || "text"; } // Get or create artifact element private getOrCreateArtifactElement(filename: string, content: string, title: string): ArtifactElement { let element = this.artifactElements.get(filename); if (!element) { const type = this.getFileType(filename); if (type === "html") { element = new HtmlArtifact(); (element as HtmlArtifact).attachments = this.attachmentsProvider?.() || []; } else if (type === "svg") { element = new SvgArtifact(); } else if (type === "markdown") { element = new MarkdownArtifact(); } else { element = new TextArtifact(); } element.filename = filename; element.displayTitle = title; element.content = content; element.style.display = "none"; element.style.height = "100%"; // Store element this.artifactElements.set(filename, element); // Add to DOM after next render const newElement = element; requestAnimationFrame(() => { if (this.contentRef.value && !newElement.parentElement) { this.contentRef.value.appendChild(newElement); } }); } else { // Just update content element.content = content; element.displayTitle = title; if (element instanceof HtmlArtifact) { element.attachments = this.attachmentsProvider?.() || []; } } return element; } // Show/hide artifact elements private showArtifact(filename: string) { // Ensure the active element is in the DOM requestAnimationFrame(() => { this.artifactElements.forEach((element, name) => { if (this.contentRef.value && !element.parentElement) { this.contentRef.value.appendChild(element); } element.style.display = name === filename ? "block" : "none"; }); }); this._activeFilename = filename; this.requestUpdate(); // Only for tab bar update } // Open panel and focus an artifact tab by filename private openArtifact(filename: string) { if (this._artifacts.has(filename)) { this.showArtifact(filename); // Ask host to open panel (AgentInterface demo listens to onOpen) this.onOpen?.(); } } // Build the AgentTool (no details payload; return only output strings) public get tool(): AgentTool { return { label: "Artifacts", name: "artifacts", 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 For text/html artifacts with attachments: - HTML artifacts automatically have access to user attachments via JavaScript - Available global functions in HTML artifacts: * listFiles() - Returns array of {id, fileName, mimeType, size} for all attachments * readTextFile(attachmentId) - Returns text content of attachment (for CSV, JSON, text files) * readBinaryFile(attachmentId) - Returns Uint8Array of binary data (for images, Excel, etc.) - Example HTML artifact that processes a CSV attachment: 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