@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,713 @@
|
|
|
1
|
+
import { icon } from "@mariozechner/mini-lit";
|
|
2
|
+
import "@mariozechner/mini-lit/dist/MarkdownBlock.js";
|
|
3
|
+
import { Button } from "@mariozechner/mini-lit/dist/Button.js";
|
|
4
|
+
import type { Agent, AgentMessage, AgentTool } from "@oh-my-pi/pi-agent-core";
|
|
5
|
+
import { StringEnum, type ToolCall } from "@oh-my-pi/pi-ai";
|
|
6
|
+
import { type Static, Type } from "@sinclair/typebox";
|
|
7
|
+
import { html, LitElement, type TemplateResult } from "lit";
|
|
8
|
+
import { customElement, property, state } from "lit/decorators.js";
|
|
9
|
+
import { createRef, type Ref, ref } from "lit/directives/ref.js";
|
|
10
|
+
import { X } from "lucide";
|
|
11
|
+
import type { ArtifactMessage } from "../../components/Messages.js";
|
|
12
|
+
import { ArtifactsRuntimeProvider } from "../../components/sandbox/ArtifactsRuntimeProvider.js";
|
|
13
|
+
import { AttachmentsRuntimeProvider } from "../../components/sandbox/AttachmentsRuntimeProvider.js";
|
|
14
|
+
import type { SandboxRuntimeProvider } from "../../components/sandbox/SandboxRuntimeProvider.js";
|
|
15
|
+
import {
|
|
16
|
+
ARTIFACTS_RUNTIME_PROVIDER_DESCRIPTION_RO,
|
|
17
|
+
ARTIFACTS_TOOL_DESCRIPTION,
|
|
18
|
+
ATTACHMENTS_RUNTIME_DESCRIPTION,
|
|
19
|
+
} from "../../prompts/prompts.js";
|
|
20
|
+
import type { Attachment } from "../../utils/attachment-utils.js";
|
|
21
|
+
import { i18n } from "../../utils/i18n.js";
|
|
22
|
+
import type { ArtifactElement } from "./ArtifactElement.js";
|
|
23
|
+
import { DocxArtifact } from "./DocxArtifact.js";
|
|
24
|
+
import { ExcelArtifact } from "./ExcelArtifact.js";
|
|
25
|
+
import { GenericArtifact } from "./GenericArtifact.js";
|
|
26
|
+
import { HtmlArtifact } from "./HtmlArtifact.js";
|
|
27
|
+
import { ImageArtifact } from "./ImageArtifact.js";
|
|
28
|
+
import { MarkdownArtifact } from "./MarkdownArtifact.js";
|
|
29
|
+
import { PdfArtifact } from "./PdfArtifact.js";
|
|
30
|
+
import { SvgArtifact } from "./SvgArtifact.js";
|
|
31
|
+
import { TextArtifact } from "./TextArtifact.js";
|
|
32
|
+
|
|
33
|
+
// Simple artifact model
|
|
34
|
+
export interface Artifact {
|
|
35
|
+
filename: string;
|
|
36
|
+
content: string;
|
|
37
|
+
createdAt: Date;
|
|
38
|
+
updatedAt: Date;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// JSON-schema friendly parameters object (LLM-facing)
|
|
42
|
+
const artifactsParamsSchema = Type.Object({
|
|
43
|
+
command: StringEnum(["create", "update", "rewrite", "get", "delete", "logs"], {
|
|
44
|
+
description: "The operation to perform",
|
|
45
|
+
}),
|
|
46
|
+
filename: Type.String({ description: "Filename including extension (e.g., 'index.html', 'script.js')" }),
|
|
47
|
+
content: Type.Optional(Type.String({ description: "File content" })),
|
|
48
|
+
old_str: Type.Optional(Type.String({ description: "String to replace (for update command)" })),
|
|
49
|
+
new_str: Type.Optional(Type.String({ description: "Replacement string (for update command)" })),
|
|
50
|
+
});
|
|
51
|
+
export type ArtifactsParams = Static<typeof artifactsParamsSchema>;
|
|
52
|
+
|
|
53
|
+
@customElement("artifacts-panel")
|
|
54
|
+
export class ArtifactsPanel extends LitElement {
|
|
55
|
+
@state() private _artifacts = new Map<string, Artifact>();
|
|
56
|
+
@state() private _activeFilename: string | null = null;
|
|
57
|
+
|
|
58
|
+
// Programmatically managed artifact elements
|
|
59
|
+
private artifactElements = new Map<string, ArtifactElement>();
|
|
60
|
+
private contentRef: Ref<HTMLDivElement> = createRef();
|
|
61
|
+
|
|
62
|
+
// Agent reference (needed to get attachments for HTML artifacts)
|
|
63
|
+
@property({ attribute: false }) agent?: Agent;
|
|
64
|
+
// Sandbox URL provider for browser extensions (optional)
|
|
65
|
+
@property({ attribute: false }) sandboxUrlProvider?: () => string;
|
|
66
|
+
// Callbacks
|
|
67
|
+
@property({ attribute: false }) onArtifactsChange?: () => void;
|
|
68
|
+
@property({ attribute: false }) onClose?: () => void;
|
|
69
|
+
@property({ attribute: false }) onOpen?: () => void;
|
|
70
|
+
// Collapsed mode: hides panel content but can show a floating reopen pill
|
|
71
|
+
@property({ type: Boolean }) collapsed = false;
|
|
72
|
+
// Overlay mode: when true, panel renders full-screen overlay (mobile)
|
|
73
|
+
@property({ type: Boolean }) overlay = false;
|
|
74
|
+
|
|
75
|
+
// Public getter for artifacts
|
|
76
|
+
get artifacts() {
|
|
77
|
+
return this._artifacts;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Get runtime providers for HTML artifacts (read-only: attachments + artifacts)
|
|
81
|
+
private getHtmlArtifactRuntimeProviders(): SandboxRuntimeProvider[] {
|
|
82
|
+
const providers: SandboxRuntimeProvider[] = [];
|
|
83
|
+
|
|
84
|
+
// Get attachments from agent messages
|
|
85
|
+
if (this.agent) {
|
|
86
|
+
const attachments: Attachment[] = [];
|
|
87
|
+
for (const message of this.agent.state.messages) {
|
|
88
|
+
if (message.role === "user-with-attachments" && message.attachments) {
|
|
89
|
+
attachments.push(...message.attachments);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
if (attachments.length > 0) {
|
|
93
|
+
providers.push(new AttachmentsRuntimeProvider(attachments));
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Add read-only artifacts provider
|
|
98
|
+
providers.push(new ArtifactsRuntimeProvider(this, this.agent, false));
|
|
99
|
+
|
|
100
|
+
return providers;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
protected override createRenderRoot(): HTMLElement | DocumentFragment {
|
|
104
|
+
return this; // light DOM for shared styles
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
override connectedCallback(): void {
|
|
108
|
+
super.connectedCallback();
|
|
109
|
+
this.style.display = "block";
|
|
110
|
+
this.style.height = "100%";
|
|
111
|
+
// Reattach existing artifact elements when panel is re-inserted into the DOM
|
|
112
|
+
requestAnimationFrame(() => {
|
|
113
|
+
const container = this.contentRef.value;
|
|
114
|
+
if (!container) return;
|
|
115
|
+
// Ensure we have an active filename
|
|
116
|
+
if (!this._activeFilename && this._artifacts.size > 0) {
|
|
117
|
+
this._activeFilename = Array.from(this._artifacts.keys())[0];
|
|
118
|
+
}
|
|
119
|
+
this.artifactElements.forEach((element, name) => {
|
|
120
|
+
if (!element.parentElement) container.appendChild(element);
|
|
121
|
+
element.style.display = name === this._activeFilename ? "block" : "none";
|
|
122
|
+
});
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
override disconnectedCallback() {
|
|
127
|
+
super.disconnectedCallback();
|
|
128
|
+
// Do not tear down artifact elements; keep them to restore on next mount
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Helper to determine file type from extension
|
|
132
|
+
private getFileType(
|
|
133
|
+
filename: string,
|
|
134
|
+
): "html" | "svg" | "markdown" | "image" | "pdf" | "excel" | "docx" | "text" | "generic" {
|
|
135
|
+
const ext = filename.split(".").pop()?.toLowerCase();
|
|
136
|
+
if (ext === "html") return "html";
|
|
137
|
+
if (ext === "svg") return "svg";
|
|
138
|
+
if (ext === "md" || ext === "markdown") return "markdown";
|
|
139
|
+
if (ext === "pdf") return "pdf";
|
|
140
|
+
if (ext === "xlsx" || ext === "xls") return "excel";
|
|
141
|
+
if (ext === "docx") return "docx";
|
|
142
|
+
if (
|
|
143
|
+
ext === "png" ||
|
|
144
|
+
ext === "jpg" ||
|
|
145
|
+
ext === "jpeg" ||
|
|
146
|
+
ext === "gif" ||
|
|
147
|
+
ext === "webp" ||
|
|
148
|
+
ext === "bmp" ||
|
|
149
|
+
ext === "ico"
|
|
150
|
+
)
|
|
151
|
+
return "image";
|
|
152
|
+
// Text files
|
|
153
|
+
if (
|
|
154
|
+
ext === "txt" ||
|
|
155
|
+
ext === "json" ||
|
|
156
|
+
ext === "xml" ||
|
|
157
|
+
ext === "yaml" ||
|
|
158
|
+
ext === "yml" ||
|
|
159
|
+
ext === "csv" ||
|
|
160
|
+
ext === "js" ||
|
|
161
|
+
ext === "ts" ||
|
|
162
|
+
ext === "jsx" ||
|
|
163
|
+
ext === "tsx" ||
|
|
164
|
+
ext === "py" ||
|
|
165
|
+
ext === "java" ||
|
|
166
|
+
ext === "c" ||
|
|
167
|
+
ext === "cpp" ||
|
|
168
|
+
ext === "h" ||
|
|
169
|
+
ext === "css" ||
|
|
170
|
+
ext === "scss" ||
|
|
171
|
+
ext === "sass" ||
|
|
172
|
+
ext === "less" ||
|
|
173
|
+
ext === "sh"
|
|
174
|
+
)
|
|
175
|
+
return "text";
|
|
176
|
+
// Everything else gets generic fallback
|
|
177
|
+
return "generic";
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// Get or create artifact element
|
|
181
|
+
private getOrCreateArtifactElement(filename: string, content: string): ArtifactElement {
|
|
182
|
+
let element = this.artifactElements.get(filename);
|
|
183
|
+
|
|
184
|
+
if (!element) {
|
|
185
|
+
const type = this.getFileType(filename);
|
|
186
|
+
if (type === "html") {
|
|
187
|
+
element = new HtmlArtifact();
|
|
188
|
+
(element as HtmlArtifact).runtimeProviders = this.getHtmlArtifactRuntimeProviders();
|
|
189
|
+
if (this.sandboxUrlProvider) {
|
|
190
|
+
(element as HtmlArtifact).sandboxUrlProvider = this.sandboxUrlProvider;
|
|
191
|
+
}
|
|
192
|
+
} else if (type === "svg") {
|
|
193
|
+
element = new SvgArtifact();
|
|
194
|
+
} else if (type === "markdown") {
|
|
195
|
+
element = new MarkdownArtifact();
|
|
196
|
+
} else if (type === "image") {
|
|
197
|
+
element = new ImageArtifact();
|
|
198
|
+
} else if (type === "pdf") {
|
|
199
|
+
element = new PdfArtifact();
|
|
200
|
+
} else if (type === "excel") {
|
|
201
|
+
element = new ExcelArtifact();
|
|
202
|
+
} else if (type === "docx") {
|
|
203
|
+
element = new DocxArtifact();
|
|
204
|
+
} else if (type === "text") {
|
|
205
|
+
element = new TextArtifact();
|
|
206
|
+
} else {
|
|
207
|
+
element = new GenericArtifact();
|
|
208
|
+
}
|
|
209
|
+
element.filename = filename;
|
|
210
|
+
element.content = content;
|
|
211
|
+
element.style.display = "none";
|
|
212
|
+
element.style.height = "100%";
|
|
213
|
+
|
|
214
|
+
// Store element
|
|
215
|
+
this.artifactElements.set(filename, element);
|
|
216
|
+
|
|
217
|
+
// Add to DOM - try immediately if container exists, otherwise schedule
|
|
218
|
+
const newElement = element;
|
|
219
|
+
if (this.contentRef.value) {
|
|
220
|
+
this.contentRef.value.appendChild(newElement);
|
|
221
|
+
} else {
|
|
222
|
+
requestAnimationFrame(() => {
|
|
223
|
+
if (this.contentRef.value && !newElement.parentElement) {
|
|
224
|
+
this.contentRef.value.appendChild(newElement);
|
|
225
|
+
}
|
|
226
|
+
});
|
|
227
|
+
}
|
|
228
|
+
} else {
|
|
229
|
+
// Just update content
|
|
230
|
+
element.content = content;
|
|
231
|
+
if (element instanceof HtmlArtifact) {
|
|
232
|
+
element.runtimeProviders = this.getHtmlArtifactRuntimeProviders();
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
return element;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// Show/hide artifact elements
|
|
240
|
+
private showArtifact(filename: string) {
|
|
241
|
+
// Ensure the active element is in the DOM
|
|
242
|
+
requestAnimationFrame(() => {
|
|
243
|
+
this.artifactElements.forEach((element, name) => {
|
|
244
|
+
if (this.contentRef.value && !element.parentElement) {
|
|
245
|
+
this.contentRef.value.appendChild(element);
|
|
246
|
+
}
|
|
247
|
+
element.style.display = name === filename ? "block" : "none";
|
|
248
|
+
});
|
|
249
|
+
});
|
|
250
|
+
this._activeFilename = filename;
|
|
251
|
+
this.requestUpdate(); // Only for tab bar update
|
|
252
|
+
|
|
253
|
+
// Scroll the active tab into view after render
|
|
254
|
+
requestAnimationFrame(() => {
|
|
255
|
+
const activeButton = this.querySelector(`button[data-filename="${filename}"]`);
|
|
256
|
+
if (activeButton) {
|
|
257
|
+
activeButton.scrollIntoView({ behavior: "smooth", block: "nearest", inline: "center" });
|
|
258
|
+
}
|
|
259
|
+
});
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// Open panel and focus an artifact tab by filename
|
|
263
|
+
public openArtifact(filename: string) {
|
|
264
|
+
if (this._artifacts.has(filename)) {
|
|
265
|
+
this.showArtifact(filename);
|
|
266
|
+
// Ask host to open panel (AgentInterface demo listens to onOpen)
|
|
267
|
+
this.onOpen?.();
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// Build the AgentTool (no details payload; return only output strings)
|
|
272
|
+
public get tool(): AgentTool<typeof artifactsParamsSchema, undefined> {
|
|
273
|
+
return {
|
|
274
|
+
label: "Artifacts",
|
|
275
|
+
name: "artifacts",
|
|
276
|
+
get description() {
|
|
277
|
+
// HTML artifacts have read-only access to attachments and artifacts
|
|
278
|
+
const runtimeProviderDescriptions = [
|
|
279
|
+
ATTACHMENTS_RUNTIME_DESCRIPTION,
|
|
280
|
+
ARTIFACTS_RUNTIME_PROVIDER_DESCRIPTION_RO,
|
|
281
|
+
];
|
|
282
|
+
return ARTIFACTS_TOOL_DESCRIPTION(runtimeProviderDescriptions);
|
|
283
|
+
},
|
|
284
|
+
parameters: artifactsParamsSchema,
|
|
285
|
+
// Execute mutates our local store and returns a plain output
|
|
286
|
+
execute: async (_toolCallId: string, args: Static<typeof artifactsParamsSchema>, _signal?: AbortSignal) => {
|
|
287
|
+
const output = await this.executeCommand(args);
|
|
288
|
+
return { content: [{ type: "text", text: output }], details: undefined };
|
|
289
|
+
},
|
|
290
|
+
};
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
// Re-apply artifacts by scanning a message list (optional utility)
|
|
294
|
+
public async reconstructFromMessages(
|
|
295
|
+
messages: Array<AgentMessage | { role: "aborted" } | { role: "artifact" }>,
|
|
296
|
+
): Promise<void> {
|
|
297
|
+
const toolCalls = new Map<string, ToolCall>();
|
|
298
|
+
const artifactToolName = "artifacts";
|
|
299
|
+
|
|
300
|
+
// 1) Collect tool calls from assistant messages
|
|
301
|
+
for (const message of messages) {
|
|
302
|
+
if (message.role === "assistant") {
|
|
303
|
+
for (const block of message.content) {
|
|
304
|
+
if (block.type === "toolCall" && block.name === artifactToolName) {
|
|
305
|
+
toolCalls.set(block.id, block);
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
// 2) Build an ordered list of successful artifact operations
|
|
312
|
+
const operations: Array<ArtifactsParams> = [];
|
|
313
|
+
for (const m of messages) {
|
|
314
|
+
if ((m as any).role === "artifact") {
|
|
315
|
+
const artifactMsg = m as ArtifactMessage;
|
|
316
|
+
switch (artifactMsg.action) {
|
|
317
|
+
case "create":
|
|
318
|
+
operations.push({
|
|
319
|
+
command: "create",
|
|
320
|
+
filename: artifactMsg.filename,
|
|
321
|
+
content: artifactMsg.content,
|
|
322
|
+
});
|
|
323
|
+
break;
|
|
324
|
+
case "update":
|
|
325
|
+
operations.push({
|
|
326
|
+
command: "rewrite",
|
|
327
|
+
filename: artifactMsg.filename,
|
|
328
|
+
content: artifactMsg.content,
|
|
329
|
+
});
|
|
330
|
+
break;
|
|
331
|
+
case "delete":
|
|
332
|
+
operations.push({
|
|
333
|
+
command: "delete",
|
|
334
|
+
filename: artifactMsg.filename,
|
|
335
|
+
});
|
|
336
|
+
break;
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
// Handle tool result messages (from artifacts tool calls)
|
|
340
|
+
else if ((m as any).role === "toolResult" && (m as any).toolName === artifactToolName && !(m as any).isError) {
|
|
341
|
+
const toolCallId = (m as any).toolCallId as string;
|
|
342
|
+
const call = toolCalls.get(toolCallId);
|
|
343
|
+
if (!call) continue;
|
|
344
|
+
const params = call.arguments as ArtifactsParams;
|
|
345
|
+
if (params.command === "get" || params.command === "logs") continue; // no state change
|
|
346
|
+
operations.push(params);
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
// 3) Compute final state per filename by simulating operations in-memory
|
|
351
|
+
const finalArtifacts = new Map<string, string>();
|
|
352
|
+
for (const op of operations) {
|
|
353
|
+
const filename = op.filename;
|
|
354
|
+
switch (op.command) {
|
|
355
|
+
case "create": {
|
|
356
|
+
if (op.content) {
|
|
357
|
+
finalArtifacts.set(filename, op.content);
|
|
358
|
+
}
|
|
359
|
+
break;
|
|
360
|
+
}
|
|
361
|
+
case "rewrite": {
|
|
362
|
+
if (op.content) {
|
|
363
|
+
finalArtifacts.set(filename, op.content);
|
|
364
|
+
}
|
|
365
|
+
break;
|
|
366
|
+
}
|
|
367
|
+
case "update": {
|
|
368
|
+
let existing = finalArtifacts.get(filename);
|
|
369
|
+
if (!existing) break; // skip invalid update (shouldn't happen for successful results)
|
|
370
|
+
if (op.old_str !== undefined && op.new_str !== undefined) {
|
|
371
|
+
existing = existing.replace(op.old_str, op.new_str);
|
|
372
|
+
finalArtifacts.set(filename, existing);
|
|
373
|
+
}
|
|
374
|
+
break;
|
|
375
|
+
}
|
|
376
|
+
case "delete": {
|
|
377
|
+
finalArtifacts.delete(filename);
|
|
378
|
+
break;
|
|
379
|
+
}
|
|
380
|
+
case "get":
|
|
381
|
+
case "logs":
|
|
382
|
+
// Ignored above, just for completeness
|
|
383
|
+
break;
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
// 4) Reset current UI state before bulk create
|
|
388
|
+
this._artifacts.clear();
|
|
389
|
+
this.artifactElements.forEach((el) => {
|
|
390
|
+
el.remove();
|
|
391
|
+
});
|
|
392
|
+
this.artifactElements.clear();
|
|
393
|
+
this._activeFilename = null;
|
|
394
|
+
this._artifacts = new Map(this._artifacts);
|
|
395
|
+
|
|
396
|
+
// 5) Create artifacts in a single pass without waiting for iframe execution or tab switching
|
|
397
|
+
for (const [filename, content] of finalArtifacts.entries()) {
|
|
398
|
+
const createParams: ArtifactsParams = { command: "create", filename, content } as const;
|
|
399
|
+
try {
|
|
400
|
+
await this.createArtifact(createParams, { skipWait: true, silent: true });
|
|
401
|
+
} catch {
|
|
402
|
+
// Ignore failures during reconstruction
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
// 6) Show first artifact if any exist, and notify listeners once
|
|
407
|
+
if (!this._activeFilename && this._artifacts.size > 0) {
|
|
408
|
+
this.showArtifact(Array.from(this._artifacts.keys())[0]);
|
|
409
|
+
}
|
|
410
|
+
this.onArtifactsChange?.();
|
|
411
|
+
this.requestUpdate();
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
// Core command executor
|
|
415
|
+
private async executeCommand(
|
|
416
|
+
params: ArtifactsParams,
|
|
417
|
+
options: { skipWait?: boolean; silent?: boolean } = {},
|
|
418
|
+
): Promise<string> {
|
|
419
|
+
switch (params.command) {
|
|
420
|
+
case "create":
|
|
421
|
+
return await this.createArtifact(params, options);
|
|
422
|
+
case "update":
|
|
423
|
+
return await this.updateArtifact(params, options);
|
|
424
|
+
case "rewrite":
|
|
425
|
+
return await this.rewriteArtifact(params, options);
|
|
426
|
+
case "get":
|
|
427
|
+
return this.getArtifact(params);
|
|
428
|
+
case "delete":
|
|
429
|
+
return this.deleteArtifact(params);
|
|
430
|
+
case "logs":
|
|
431
|
+
return this.getLogs(params);
|
|
432
|
+
default:
|
|
433
|
+
// Should never happen with TypeBox validation
|
|
434
|
+
return `Error: Unknown command ${(params as any).command}`;
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
// Wait for HTML artifact execution and get logs
|
|
439
|
+
private async waitForHtmlExecution(filename: string): Promise<string> {
|
|
440
|
+
const element = this.artifactElements.get(filename);
|
|
441
|
+
if (!(element instanceof HtmlArtifact)) {
|
|
442
|
+
return "";
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
return new Promise((resolve) => {
|
|
446
|
+
// Fallback timeout - just get logs after execution should complete
|
|
447
|
+
setTimeout(() => {
|
|
448
|
+
// Get whatever logs we have
|
|
449
|
+
const logs = element.getLogs();
|
|
450
|
+
resolve(logs);
|
|
451
|
+
}, 1500);
|
|
452
|
+
});
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
// Reload all HTML artifacts (called when any artifact changes)
|
|
456
|
+
private reloadAllHtmlArtifacts() {
|
|
457
|
+
this.artifactElements.forEach((element) => {
|
|
458
|
+
if (element instanceof HtmlArtifact && element.sandboxIframeRef.value) {
|
|
459
|
+
// Update runtime providers with latest artifact state
|
|
460
|
+
element.runtimeProviders = this.getHtmlArtifactRuntimeProviders();
|
|
461
|
+
// Re-execute the HTML content
|
|
462
|
+
element.executeContent(element.content);
|
|
463
|
+
}
|
|
464
|
+
});
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
private async createArtifact(
|
|
468
|
+
params: ArtifactsParams,
|
|
469
|
+
options: { skipWait?: boolean; silent?: boolean } = {},
|
|
470
|
+
): Promise<string> {
|
|
471
|
+
if (!params.filename || !params.content) {
|
|
472
|
+
return "Error: create command requires filename and content";
|
|
473
|
+
}
|
|
474
|
+
if (this._artifacts.has(params.filename)) {
|
|
475
|
+
return `Error: File ${params.filename} already exists`;
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
const artifact: Artifact = {
|
|
479
|
+
filename: params.filename,
|
|
480
|
+
content: params.content,
|
|
481
|
+
createdAt: new Date(),
|
|
482
|
+
updatedAt: new Date(),
|
|
483
|
+
};
|
|
484
|
+
this._artifacts.set(params.filename, artifact);
|
|
485
|
+
this._artifacts = new Map(this._artifacts);
|
|
486
|
+
|
|
487
|
+
// Create or update element
|
|
488
|
+
this.getOrCreateArtifactElement(params.filename, params.content);
|
|
489
|
+
if (!options.silent) {
|
|
490
|
+
this.showArtifact(params.filename);
|
|
491
|
+
this.onArtifactsChange?.();
|
|
492
|
+
this.requestUpdate();
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
// Reload all HTML artifacts since they might depend on this new artifact
|
|
496
|
+
this.reloadAllHtmlArtifacts();
|
|
497
|
+
|
|
498
|
+
// For HTML files, wait for execution
|
|
499
|
+
let result = `Created file ${params.filename}`;
|
|
500
|
+
if (this.getFileType(params.filename) === "html" && !options.skipWait) {
|
|
501
|
+
const logs = await this.waitForHtmlExecution(params.filename);
|
|
502
|
+
result += `\n${logs}`;
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
return result;
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
private async updateArtifact(
|
|
509
|
+
params: ArtifactsParams,
|
|
510
|
+
options: { skipWait?: boolean; silent?: boolean } = {},
|
|
511
|
+
): Promise<string> {
|
|
512
|
+
const artifact = this._artifacts.get(params.filename);
|
|
513
|
+
if (!artifact) {
|
|
514
|
+
const files = Array.from(this._artifacts.keys());
|
|
515
|
+
if (files.length === 0) return `Error: File ${params.filename} not found. No files have been created yet.`;
|
|
516
|
+
return `Error: File ${params.filename} not found. Available files: ${files.join(", ")}`;
|
|
517
|
+
}
|
|
518
|
+
if (!params.old_str || params.new_str === undefined) {
|
|
519
|
+
return "Error: update command requires old_str and new_str";
|
|
520
|
+
}
|
|
521
|
+
if (!artifact.content.includes(params.old_str)) {
|
|
522
|
+
return `Error: String not found in file. Here is the full content:\n\n${artifact.content}`;
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
artifact.content = artifact.content.replace(params.old_str, params.new_str);
|
|
526
|
+
artifact.updatedAt = new Date();
|
|
527
|
+
this._artifacts.set(params.filename, artifact);
|
|
528
|
+
|
|
529
|
+
// Update element
|
|
530
|
+
this.getOrCreateArtifactElement(params.filename, artifact.content);
|
|
531
|
+
if (!options.silent) {
|
|
532
|
+
this.onArtifactsChange?.();
|
|
533
|
+
this.requestUpdate();
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
// Show the artifact
|
|
537
|
+
this.showArtifact(params.filename);
|
|
538
|
+
|
|
539
|
+
// Reload all HTML artifacts since they might depend on this updated artifact
|
|
540
|
+
this.reloadAllHtmlArtifacts();
|
|
541
|
+
|
|
542
|
+
// For HTML files, wait for execution
|
|
543
|
+
let result = `Updated file ${params.filename}`;
|
|
544
|
+
if (this.getFileType(params.filename) === "html" && !options.skipWait) {
|
|
545
|
+
const logs = await this.waitForHtmlExecution(params.filename);
|
|
546
|
+
result += `\n${logs}`;
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
return result;
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
private async rewriteArtifact(
|
|
553
|
+
params: ArtifactsParams,
|
|
554
|
+
options: { skipWait?: boolean; silent?: boolean } = {},
|
|
555
|
+
): Promise<string> {
|
|
556
|
+
const artifact = this._artifacts.get(params.filename);
|
|
557
|
+
if (!artifact) {
|
|
558
|
+
const files = Array.from(this._artifacts.keys());
|
|
559
|
+
if (files.length === 0) return `Error: File ${params.filename} not found. No files have been created yet.`;
|
|
560
|
+
return `Error: File ${params.filename} not found. Available files: ${files.join(", ")}`;
|
|
561
|
+
}
|
|
562
|
+
if (!params.content) {
|
|
563
|
+
return "Error: rewrite command requires content";
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
artifact.content = params.content;
|
|
567
|
+
artifact.updatedAt = new Date();
|
|
568
|
+
this._artifacts.set(params.filename, artifact);
|
|
569
|
+
|
|
570
|
+
// Update element
|
|
571
|
+
this.getOrCreateArtifactElement(params.filename, artifact.content);
|
|
572
|
+
if (!options.silent) {
|
|
573
|
+
this.onArtifactsChange?.();
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
// Show the artifact
|
|
577
|
+
this.showArtifact(params.filename);
|
|
578
|
+
|
|
579
|
+
// Reload all HTML artifacts since they might depend on this rewritten artifact
|
|
580
|
+
this.reloadAllHtmlArtifacts();
|
|
581
|
+
|
|
582
|
+
// For HTML files, wait for execution
|
|
583
|
+
let result = "";
|
|
584
|
+
if (this.getFileType(params.filename) === "html" && !options.skipWait) {
|
|
585
|
+
const logs = await this.waitForHtmlExecution(params.filename);
|
|
586
|
+
result += `\n${logs}`;
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
return result;
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
private getArtifact(params: ArtifactsParams): string {
|
|
593
|
+
const artifact = this._artifacts.get(params.filename);
|
|
594
|
+
if (!artifact) {
|
|
595
|
+
const files = Array.from(this._artifacts.keys());
|
|
596
|
+
if (files.length === 0) return `Error: File ${params.filename} not found. No files have been created yet.`;
|
|
597
|
+
return `Error: File ${params.filename} not found. Available files: ${files.join(", ")}`;
|
|
598
|
+
}
|
|
599
|
+
return artifact.content;
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
private deleteArtifact(params: ArtifactsParams): string {
|
|
603
|
+
const artifact = this._artifacts.get(params.filename);
|
|
604
|
+
if (!artifact) {
|
|
605
|
+
const files = Array.from(this._artifacts.keys());
|
|
606
|
+
if (files.length === 0) return `Error: File ${params.filename} not found. No files have been created yet.`;
|
|
607
|
+
return `Error: File ${params.filename} not found. Available files: ${files.join(", ")}`;
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
this._artifacts.delete(params.filename);
|
|
611
|
+
this._artifacts = new Map(this._artifacts);
|
|
612
|
+
|
|
613
|
+
// Remove element
|
|
614
|
+
const element = this.artifactElements.get(params.filename);
|
|
615
|
+
if (element) {
|
|
616
|
+
element.remove();
|
|
617
|
+
this.artifactElements.delete(params.filename);
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
// Show another artifact if this was active
|
|
621
|
+
if (this._activeFilename === params.filename) {
|
|
622
|
+
const remaining = Array.from(this._artifacts.keys());
|
|
623
|
+
if (remaining.length > 0) {
|
|
624
|
+
this.showArtifact(remaining[0]);
|
|
625
|
+
} else {
|
|
626
|
+
this._activeFilename = null;
|
|
627
|
+
this.requestUpdate();
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
this.onArtifactsChange?.();
|
|
631
|
+
this.requestUpdate();
|
|
632
|
+
|
|
633
|
+
// Reload all HTML artifacts since they might have depended on this deleted artifact
|
|
634
|
+
this.reloadAllHtmlArtifacts();
|
|
635
|
+
|
|
636
|
+
return `Deleted file ${params.filename}`;
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
private getLogs(params: ArtifactsParams): string {
|
|
640
|
+
const element = this.artifactElements.get(params.filename);
|
|
641
|
+
if (!element) {
|
|
642
|
+
const files = Array.from(this._artifacts.keys());
|
|
643
|
+
if (files.length === 0) return `Error: File ${params.filename} not found. No files have been created yet.`;
|
|
644
|
+
return `Error: File ${params.filename} not found. Available files: ${files.join(", ")}`;
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
if (!(element instanceof HtmlArtifact)) {
|
|
648
|
+
return `Error: File ${params.filename} is not an HTML file. Logs are only available for HTML files.`;
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
return element.getLogs();
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
override render(): TemplateResult {
|
|
655
|
+
const artifacts = Array.from(this._artifacts.values());
|
|
656
|
+
|
|
657
|
+
// Panel is hidden when collapsed OR when there are no artifacts
|
|
658
|
+
const showPanel = artifacts.length > 0 && !this.collapsed;
|
|
659
|
+
|
|
660
|
+
return html`
|
|
661
|
+
<div
|
|
662
|
+
class="${showPanel ? "" : "hidden"} ${
|
|
663
|
+
this.overlay ? "fixed inset-0 z-40 pointer-events-auto backdrop-blur-sm bg-background/95" : "relative"
|
|
664
|
+
} h-full flex flex-col bg-background text-card-foreground ${
|
|
665
|
+
!this.overlay ? "border-l border-border" : ""
|
|
666
|
+
} overflow-hidden shadow-xl"
|
|
667
|
+
>
|
|
668
|
+
<!-- Tab bar (always shown when there are artifacts) -->
|
|
669
|
+
<div class="flex items-center justify-between border-b border-border bg-background">
|
|
670
|
+
<div class="flex overflow-x-auto">
|
|
671
|
+
${artifacts.map((a) => {
|
|
672
|
+
const isActive = a.filename === this._activeFilename;
|
|
673
|
+
const activeClass = isActive
|
|
674
|
+
? "border-primary text-primary"
|
|
675
|
+
: "border-transparent text-muted-foreground hover:text-foreground";
|
|
676
|
+
return html`
|
|
677
|
+
<button
|
|
678
|
+
class="px-3 py-2 whitespace-nowrap border-b-2 ${activeClass}"
|
|
679
|
+
data-filename="${a.filename}"
|
|
680
|
+
@click=${() => this.showArtifact(a.filename)}
|
|
681
|
+
>
|
|
682
|
+
<span class="font-mono text-xs">${a.filename}</span>
|
|
683
|
+
</button>
|
|
684
|
+
`;
|
|
685
|
+
})}
|
|
686
|
+
</div>
|
|
687
|
+
<div class="flex items-center gap-1 px-2">
|
|
688
|
+
${(() => {
|
|
689
|
+
const active = this._activeFilename ? this.artifactElements.get(this._activeFilename) : undefined;
|
|
690
|
+
return active ? active.getHeaderButtons() : "";
|
|
691
|
+
})()}
|
|
692
|
+
${Button({
|
|
693
|
+
variant: "ghost",
|
|
694
|
+
size: "sm",
|
|
695
|
+
onClick: () => this.onClose?.(),
|
|
696
|
+
title: i18n("Close artifacts"),
|
|
697
|
+
children: icon(X, "sm"),
|
|
698
|
+
})}
|
|
699
|
+
</div>
|
|
700
|
+
</div>
|
|
701
|
+
|
|
702
|
+
<!-- Content area where artifact elements are added programmatically -->
|
|
703
|
+
<div class="flex-1 overflow-hidden" ${ref(this.contentRef)}></div>
|
|
704
|
+
</div>
|
|
705
|
+
`;
|
|
706
|
+
}
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
declare global {
|
|
710
|
+
interface HTMLElementTagNameMap {
|
|
711
|
+
"artifacts-panel": ArtifactsPanel;
|
|
712
|
+
}
|
|
713
|
+
}
|