mirror of
https://github.com/getcompanion-ai/co-mono.git
synced 2026-04-18 12:03:03 +00:00
Refactor artifacts renderer and add Console component
- Extract ArtifactsToolRenderer from ArtifactsPanel into standalone renderer - Fix ChatPanel to register ArtifactsToolRenderer instead of panel - Implement command-specific rendering logic (create/update/rewrite/get/logs/delete) - Create reusable Console component with copy button and autoscroll toggle - Replace custom console implementation with ExpandableSection and Console - Fix Lit reactivity for HtmlArtifact logs using spread operator - Add Lucide icons (FileCode2, ChevronsDown, Lock) for UI consistency - Follow skill.ts patterns with renderHeader and state handling - Add i18n strings for all artifact actions and console features
This commit is contained in:
parent
a8159f504f
commit
8ec9805112
19 changed files with 716 additions and 526 deletions
93
packages/web-ui/src/tools/artifacts/Console.ts
Normal file
93
packages/web-ui/src/tools/artifacts/Console.ts
Normal file
|
|
@ -0,0 +1,93 @@
|
|||
import { icon } from "@mariozechner/mini-lit";
|
||||
import "@mariozechner/mini-lit/dist/CopyButton.js";
|
||||
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 { repeat } from "lit/directives/repeat.js";
|
||||
import { ChevronDown, ChevronRight, ChevronsDown, Lock } from "lucide";
|
||||
import { i18n } from "../../utils/i18n.js";
|
||||
|
||||
interface LogEntry {
|
||||
type: "log" | "error";
|
||||
text: string;
|
||||
}
|
||||
|
||||
@customElement("artifact-console")
|
||||
export class Console extends LitElement {
|
||||
@property({ attribute: false }) logs: LogEntry[] = [];
|
||||
@state() private expanded = false;
|
||||
@state() private autoscroll = true;
|
||||
private logsContainerRef: Ref<HTMLDivElement> = createRef();
|
||||
|
||||
protected createRenderRoot() {
|
||||
return this; // light DOM
|
||||
}
|
||||
|
||||
override updated() {
|
||||
// Autoscroll to bottom when new logs arrive
|
||||
if (this.autoscroll && this.expanded && this.logsContainerRef.value) {
|
||||
this.logsContainerRef.value.scrollTop = this.logsContainerRef.value.scrollHeight;
|
||||
}
|
||||
}
|
||||
|
||||
private getLogsText(): string {
|
||||
return this.logs.map((l) => `[${l.type}] ${l.text}`).join("\n");
|
||||
}
|
||||
|
||||
override render(): TemplateResult {
|
||||
const errorCount = this.logs.filter((l) => l.type === "error").length;
|
||||
const summary =
|
||||
errorCount > 0
|
||||
? `${i18n("console")} (${errorCount} ${errorCount === 1 ? "error" : "errors"})`
|
||||
: `${i18n("console")} (${this.logs.length})`;
|
||||
|
||||
return html`
|
||||
<div class="border-t border-border p-2">
|
||||
<div class="flex items-center gap-2 w-full">
|
||||
<button
|
||||
@click=${() => {
|
||||
this.expanded = !this.expanded;
|
||||
}}
|
||||
class="flex items-center gap-2 text-sm text-muted-foreground hover:text-foreground transition-colors flex-1 text-left"
|
||||
>
|
||||
${icon(this.expanded ? ChevronDown : ChevronRight, "sm")}
|
||||
<span>${summary}</span>
|
||||
</button>
|
||||
${
|
||||
this.expanded
|
||||
? html`
|
||||
<button
|
||||
@click=${() => {
|
||||
this.autoscroll = !this.autoscroll;
|
||||
}}
|
||||
class="p-1 rounded transition-colors ${this.autoscroll ? "bg-accent text-accent-foreground" : "hover:bg-muted"}"
|
||||
title=${this.autoscroll ? i18n("Autoscroll enabled") : i18n("Autoscroll disabled")}
|
||||
>
|
||||
${icon(this.autoscroll ? ChevronsDown : Lock, "sm")}
|
||||
</button>
|
||||
<copy-button .text=${this.getLogsText()} title=${i18n("Copy logs")} .showText=${false} class="!bg-transparent hover:!bg-accent"></copy-button>
|
||||
`
|
||||
: ""
|
||||
}
|
||||
</div>
|
||||
${
|
||||
this.expanded
|
||||
? html`
|
||||
<div class="max-h-48 overflow-y-auto space-y-1 mt-2" ${ref(this.logsContainerRef)}>
|
||||
${repeat(
|
||||
this.logs,
|
||||
(_log, index) => index,
|
||||
(log) => html`
|
||||
<div class="text-xs font-mono ${log.type === "error" ? "text-destructive" : "text-muted-foreground"}">
|
||||
[${log.type}] ${log.text}
|
||||
</div>
|
||||
`,
|
||||
)}
|
||||
</div>
|
||||
`
|
||||
: ""
|
||||
}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
|
@ -9,6 +9,8 @@ import type { Attachment } from "../../utils/attachment-utils.js";
|
|||
import { i18n } from "../../utils/i18n.js";
|
||||
import "../../components/SandboxedIframe.js";
|
||||
import { ArtifactElement } from "./ArtifactElement.js";
|
||||
import type { Console } from "./Console.js";
|
||||
import "./Console.js";
|
||||
|
||||
@customElement("html-artifact")
|
||||
export class HtmlArtifact extends ArtifactElement {
|
||||
|
|
@ -22,14 +24,12 @@ export class HtmlArtifact extends ArtifactElement {
|
|||
|
||||
// Refs for DOM elements
|
||||
private sandboxIframeRef: Ref<SandboxIframe> = createRef();
|
||||
private consoleLogsRef: Ref<HTMLDivElement> = createRef();
|
||||
private consoleButtonRef: Ref<HTMLButtonElement> = createRef();
|
||||
private consoleRef: Ref<Console> = createRef();
|
||||
|
||||
// Store message handler so we can remove it
|
||||
private messageHandler?: (e: MessageEvent) => void;
|
||||
|
||||
@state() private viewMode: "preview" | "code" = "preview";
|
||||
@state() private consoleOpen = false;
|
||||
|
||||
private setViewMode(mode: "preview" | "code") {
|
||||
this.viewMode = mode;
|
||||
|
|
@ -62,13 +62,9 @@ export class HtmlArtifact extends ArtifactElement {
|
|||
if (oldValue !== value) {
|
||||
// Reset logs when content changes
|
||||
this.logs = [];
|
||||
if (this.consoleLogsRef.value) {
|
||||
this.consoleLogsRef.value.innerHTML = "";
|
||||
}
|
||||
this.requestUpdate();
|
||||
// Execute content in sandbox if it exists
|
||||
if (this.sandboxIframeRef.value && value) {
|
||||
this.updateConsoleButton();
|
||||
this.executeContent(value);
|
||||
}
|
||||
}
|
||||
|
|
@ -95,11 +91,15 @@ export class HtmlArtifact extends ArtifactElement {
|
|||
if (e.data.sandboxId !== sandboxId) return;
|
||||
|
||||
if (e.data.type === "console") {
|
||||
this.logs.push({
|
||||
type: e.data.method === "error" ? "error" : "log",
|
||||
text: e.data.text,
|
||||
});
|
||||
this.updateConsoleButton();
|
||||
// Create new array reference for Lit reactivity
|
||||
this.logs = [
|
||||
...this.logs,
|
||||
{
|
||||
type: e.data.method === "error" ? "error" : "log",
|
||||
text: e.data.text,
|
||||
},
|
||||
];
|
||||
this.requestUpdate(); // Re-render to show console
|
||||
}
|
||||
};
|
||||
window.addEventListener("message", this.messageHandler);
|
||||
|
|
@ -137,39 +137,6 @@ export class HtmlArtifact extends ArtifactElement {
|
|||
}
|
||||
}
|
||||
|
||||
private updateConsoleButton() {
|
||||
const button = this.consoleButtonRef.value;
|
||||
if (!button) return;
|
||||
|
||||
const errorCount = this.logs.filter((l) => l.type === "error").length;
|
||||
const text =
|
||||
errorCount > 0
|
||||
? `${i18n("console")} <span class="text-destructive">${errorCount} errors</span>`
|
||||
: `${i18n("console")} (${this.logs.length})`;
|
||||
button.innerHTML = `<span>${text}</span><span>${this.consoleOpen ? "▼" : "▶"}</span>`;
|
||||
}
|
||||
|
||||
private toggleConsole() {
|
||||
this.consoleOpen = !this.consoleOpen;
|
||||
this.requestUpdate();
|
||||
|
||||
// Populate console logs if opening
|
||||
if (this.consoleOpen) {
|
||||
requestAnimationFrame(() => {
|
||||
if (this.consoleLogsRef.value) {
|
||||
// Populate with existing logs
|
||||
this.consoleLogsRef.value.innerHTML = "";
|
||||
this.logs.forEach((log) => {
|
||||
const logEl = document.createElement("div");
|
||||
logEl.className = `text-xs font-mono ${log.type === "error" ? "text-destructive" : "text-muted-foreground"}`;
|
||||
logEl.textContent = `[${log.type}] ${log.text}`;
|
||||
this.consoleLogsRef.value!.appendChild(logEl);
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public getLogs(): string {
|
||||
if (this.logs.length === 0) return i18n("No logs for {filename}").replace("{filename}", this.filename);
|
||||
return this.logs.map((l) => `[${l.type}] ${l.text}`).join("\n");
|
||||
|
|
@ -184,26 +151,7 @@ export class HtmlArtifact extends ArtifactElement {
|
|||
<sandbox-iframe class="flex-1" ${ref(this.sandboxIframeRef)}></sandbox-iframe>
|
||||
${
|
||||
this.logs.length > 0
|
||||
? html`
|
||||
<div class="border-t border-border">
|
||||
<button
|
||||
@click=${() => this.toggleConsole()}
|
||||
class="w-full px-3 py-1 text-xs text-left hover:bg-muted flex items-center justify-between"
|
||||
${ref(this.consoleButtonRef)}
|
||||
>
|
||||
<span
|
||||
>${i18n("console")}
|
||||
${
|
||||
this.logs.filter((l) => l.type === "error").length > 0
|
||||
? html`<span class="text-destructive">${this.logs.filter((l) => l.type === "error").length} errors</span>`
|
||||
: `(${this.logs.length})`
|
||||
}</span
|
||||
>
|
||||
<span>${this.consoleOpen ? "▼" : "▶"}</span>
|
||||
</button>
|
||||
${this.consoleOpen ? html` <div class="max-h-48 overflow-y-auto bg-muted/50 p-2" ${ref(this.consoleLogsRef)}></div> ` : ""}
|
||||
</div>
|
||||
`
|
||||
? html`<artifact-console .logs=${this.logs} ${ref(this.consoleRef)}></artifact-console>`
|
||||
: ""
|
||||
}
|
||||
</div>
|
||||
|
|
|
|||
209
packages/web-ui/src/tools/artifacts/artifacts-tool-renderer.ts
Normal file
209
packages/web-ui/src/tools/artifacts/artifacts-tool-renderer.ts
Normal file
|
|
@ -0,0 +1,209 @@
|
|||
import { Diff, html, type TemplateResult } from "@mariozechner/mini-lit";
|
||||
import "@mariozechner/mini-lit/dist/CodeBlock.js";
|
||||
import type { ToolResultMessage } from "@mariozechner/pi-ai";
|
||||
import { FileCode2 } from "lucide";
|
||||
import "../../components/ConsoleBlock.js";
|
||||
import { i18n } from "../../utils/i18n.js";
|
||||
import { renderHeader } from "../renderer-registry.js";
|
||||
import type { ToolRenderer } from "../types.js";
|
||||
import type { ArtifactsParams } from "./artifacts.js";
|
||||
|
||||
// Helper to determine language for syntax highlighting
|
||||
function getLanguageFromFilename(filename?: string): string {
|
||||
if (!filename) return "text";
|
||||
const ext = filename.split(".").pop()?.toLowerCase();
|
||||
const languageMap: Record<string, string> = {
|
||||
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";
|
||||
}
|
||||
|
||||
export class ArtifactsToolRenderer implements ToolRenderer<ArtifactsParams, undefined> {
|
||||
render(
|
||||
params: ArtifactsParams | undefined,
|
||||
result: ToolResultMessage<undefined> | undefined,
|
||||
isStreaming?: boolean,
|
||||
): TemplateResult {
|
||||
const state = result ? (result.isError ? "error" : "complete") : "inprogress";
|
||||
|
||||
// Helper to get command labels
|
||||
const getCommandLabels = (command: string): { streaming: string; complete: string } => {
|
||||
const labels: Record<string, { streaming: string; complete: string }> = {
|
||||
create: { streaming: i18n("Creating artifact"), complete: i18n("Created artifact") },
|
||||
update: { streaming: i18n("Updating artifact"), complete: i18n("Updated artifact") },
|
||||
rewrite: { streaming: i18n("Rewriting artifact"), complete: i18n("Rewrote artifact") },
|
||||
get: { streaming: i18n("Getting artifact"), complete: i18n("Got artifact") },
|
||||
delete: { streaming: i18n("Deleting artifact"), complete: i18n("Deleted artifact") },
|
||||
logs: { streaming: i18n("Getting logs"), complete: i18n("Got logs") },
|
||||
};
|
||||
return labels[command] || { streaming: i18n("Processing artifact"), complete: i18n("Processed artifact") };
|
||||
};
|
||||
|
||||
// Error handling
|
||||
if (result?.isError) {
|
||||
const command = params?.command;
|
||||
const filename = params?.filename;
|
||||
const labels = command
|
||||
? getCommandLabels(command)
|
||||
: { streaming: i18n("Processing artifact"), complete: i18n("Processed artifact") };
|
||||
const headerText = filename ? `${labels.streaming} ${filename}` : labels.streaming;
|
||||
|
||||
// For create/update/rewrite errors, show code block + console/error
|
||||
if (command === "create" || command === "update" || command === "rewrite") {
|
||||
const content = command === "update" ? params?.new_str || params?.old_str || "" : params?.content || "";
|
||||
|
||||
const isHtml = filename?.endsWith(".html");
|
||||
|
||||
return html`
|
||||
<div class="space-y-3">
|
||||
${renderHeader(state, FileCode2, headerText)}
|
||||
${content ? html`<code-block .code=${content} language=${getLanguageFromFilename(filename)}></code-block>` : ""}
|
||||
${
|
||||
isHtml
|
||||
? html`<console-block .content=${result.output || i18n("An error occurred")} variant="error"></console-block>`
|
||||
: html`<div class="text-sm text-destructive">${result.output || i18n("An error occurred")}</div>`
|
||||
}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
// For other errors, just show error message
|
||||
return html`
|
||||
<div class="space-y-3">
|
||||
${renderHeader(state, FileCode2, headerText)}
|
||||
<div class="text-sm text-destructive">${result.output || i18n("An error occurred")}</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
// Full params + result
|
||||
if (result && params) {
|
||||
const { command, filename, content } = params;
|
||||
const labels = command
|
||||
? getCommandLabels(command)
|
||||
: { streaming: i18n("Processing artifact"), complete: i18n("Processed artifact") };
|
||||
const headerText = filename ? `${labels.complete} ${filename}` : labels.complete;
|
||||
|
||||
// GET command: show code block with file content
|
||||
if (command === "get") {
|
||||
const fileContent = result.output || i18n("(no output)");
|
||||
return html`
|
||||
<div class="space-y-3">
|
||||
${renderHeader(state, FileCode2, headerText)}
|
||||
<code-block .code=${fileContent} language=${getLanguageFromFilename(filename)}></code-block>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
// LOGS command: show console block
|
||||
if (command === "logs") {
|
||||
const logs = result.output || i18n("(no output)");
|
||||
return html`
|
||||
<div class="space-y-3">
|
||||
${renderHeader(state, FileCode2, headerText)}
|
||||
<console-block .content=${logs}></console-block>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
// CREATE/UPDATE/REWRITE: always show code block, + console block for .html files
|
||||
if (command === "create" || command === "update" || command === "rewrite") {
|
||||
const codeContent = content || "";
|
||||
const isHtml = filename?.endsWith(".html");
|
||||
const logs = result.output || "";
|
||||
|
||||
return html`
|
||||
<div class="space-y-3">
|
||||
${renderHeader(state, FileCode2, headerText)}
|
||||
${codeContent ? html`<code-block .code=${codeContent} language=${getLanguageFromFilename(filename)}></code-block>` : ""}
|
||||
${isHtml && logs ? html`<console-block .content=${logs}></console-block>` : ""}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
// For DELETE, just show header
|
||||
return html`
|
||||
<div class="space-y-3">
|
||||
${renderHeader(state, FileCode2, headerText)}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
// Params only (streaming or waiting for result)
|
||||
if (params) {
|
||||
const { command, filename, content, old_str, new_str } = params;
|
||||
|
||||
// If no command yet
|
||||
if (!command) {
|
||||
return renderHeader(state, FileCode2, i18n("Preparing artifact..."));
|
||||
}
|
||||
|
||||
const labels = getCommandLabels(command);
|
||||
const headerText = filename ? `${labels.streaming} ${filename}` : labels.streaming;
|
||||
|
||||
// Render based on command type
|
||||
switch (command) {
|
||||
case "create":
|
||||
case "rewrite":
|
||||
return html`
|
||||
<div class="space-y-3">
|
||||
${renderHeader(state, FileCode2, headerText)}
|
||||
${
|
||||
content
|
||||
? html`<code-block .code=${content} language=${getLanguageFromFilename(filename)} class="mt-2"></code-block>`
|
||||
: ""
|
||||
}
|
||||
</div>
|
||||
`;
|
||||
|
||||
case "update":
|
||||
return html`
|
||||
<div class="space-y-3">
|
||||
${renderHeader(state, FileCode2, headerText)}
|
||||
${
|
||||
old_str !== undefined && new_str !== undefined
|
||||
? Diff({ oldText: old_str, newText: new_str, className: "mt-2" })
|
||||
: ""
|
||||
}
|
||||
</div>
|
||||
`;
|
||||
|
||||
case "get":
|
||||
case "delete":
|
||||
case "logs":
|
||||
default:
|
||||
return renderHeader(state, FileCode2, headerText);
|
||||
}
|
||||
}
|
||||
|
||||
// No params or result yet
|
||||
return renderHeader(state, FileCode2, i18n("Preparing artifact..."));
|
||||
}
|
||||
}
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
import { Button, Diff, icon } from "@mariozechner/mini-lit";
|
||||
import { Button, 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";
|
||||
|
|
@ -7,14 +7,12 @@ 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 {
|
||||
|
|
@ -38,13 +36,8 @@ const artifactsParamsSchema = Type.Object({
|
|||
});
|
||||
export type ArtifactsParams = Static<typeof artifactsParamsSchema>;
|
||||
|
||||
// Minimal helper to render plain text outputs consistently
|
||||
function plainOutput(text: string): TemplateResult {
|
||||
return html`<div class="text-xs text-muted-foreground whitespace-pre-wrap font-mono">${text}</div>`;
|
||||
}
|
||||
|
||||
@customElement("artifacts-panel")
|
||||
export class ArtifactsPanel extends LitElement implements ToolRenderer<ArtifactsParams, undefined> {
|
||||
export class ArtifactsPanel extends LitElement {
|
||||
@state() private _artifacts = new Map<string, Artifact>();
|
||||
@state() private _activeFilename: string | null = null;
|
||||
|
||||
|
|
@ -107,43 +100,6 @@ export class ArtifactsPanel extends LitElement implements ToolRenderer<Artifacts
|
|||
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<string, string> = {
|
||||
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);
|
||||
|
|
@ -330,153 +286,6 @@ CRITICAL REMINDER FOR ALL ARTIFACTS:
|
|||
};
|
||||
}
|
||||
|
||||
// ToolRenderer implementation
|
||||
renderParams(params: ArtifactsParams, isStreaming?: boolean): TemplateResult {
|
||||
if (isStreaming && !params.command) {
|
||||
return html`<div class="text-sm text-muted-foreground">${i18n("Processing artifact...")}</div>`;
|
||||
}
|
||||
|
||||
let commandLabel = i18n("Processing");
|
||||
if (params.command) {
|
||||
switch (params.command) {
|
||||
case "create":
|
||||
commandLabel = i18n("Create");
|
||||
break;
|
||||
case "update":
|
||||
commandLabel = i18n("Update");
|
||||
break;
|
||||
case "rewrite":
|
||||
commandLabel = i18n("Rewrite");
|
||||
break;
|
||||
case "get":
|
||||
commandLabel = i18n("Get");
|
||||
break;
|
||||
case "delete":
|
||||
commandLabel = i18n("Delete");
|
||||
break;
|
||||
case "logs":
|
||||
commandLabel = i18n("Get logs");
|
||||
break;
|
||||
default:
|
||||
commandLabel = params.command.charAt(0).toUpperCase() + params.command.slice(1);
|
||||
}
|
||||
}
|
||||
const filename = params.filename || "";
|
||||
|
||||
switch (params.command) {
|
||||
case "create":
|
||||
return html`
|
||||
<div
|
||||
class="text-sm cursor-pointer hover:bg-muted/50 rounded-sm px-2 py-1"
|
||||
@click=${() => this.openArtifact(params.filename)}
|
||||
>
|
||||
<div>
|
||||
<span class="font-medium">${i18n("Create")}</span>
|
||||
<span class="text-muted-foreground ml-1">${filename}</span>
|
||||
</div>
|
||||
${
|
||||
params.content
|
||||
? html`<code-block
|
||||
.code=${params.content}
|
||||
language=${this.getLanguageFromFilename(params.filename)}
|
||||
class="mt-2"
|
||||
></code-block>`
|
||||
: ""
|
||||
}
|
||||
</div>
|
||||
`;
|
||||
case "update":
|
||||
return html`
|
||||
<div
|
||||
class="text-sm cursor-pointer hover:bg-muted/50 rounded-sm px-2 py-1"
|
||||
@click=${() => this.openArtifact(params.filename)}
|
||||
>
|
||||
<div>
|
||||
<span class="font-medium">${i18n("Update")}</span>
|
||||
<span class="text-muted-foreground ml-1">${filename}</span>
|
||||
</div>
|
||||
${
|
||||
params.old_str !== undefined && params.new_str !== undefined
|
||||
? Diff({ oldText: params.old_str, newText: params.new_str, className: "mt-2" })
|
||||
: ""
|
||||
}
|
||||
</div>
|
||||
`;
|
||||
case "rewrite":
|
||||
return html`
|
||||
<div
|
||||
class="text-sm cursor-pointer hover:bg-muted/50 rounded-sm px-2 py-1"
|
||||
@click=${() => this.openArtifact(params.filename)}
|
||||
>
|
||||
<div>
|
||||
<span class="font-medium">${i18n("Rewrite")}</span>
|
||||
<span class="text-muted-foreground ml-1">${filename}</span>
|
||||
</div>
|
||||
${
|
||||
params.content
|
||||
? html`<code-block
|
||||
.code=${params.content}
|
||||
language=${this.getLanguageFromFilename(params.filename)}
|
||||
class="mt-2"
|
||||
></code-block>`
|
||||
: ""
|
||||
}
|
||||
</div>
|
||||
`;
|
||||
case "get":
|
||||
return html`
|
||||
<div
|
||||
class="text-sm cursor-pointer hover:bg-muted/50 rounded-sm px-2 py-1"
|
||||
@click=${() => this.openArtifact(params.filename)}
|
||||
>
|
||||
<span class="font-medium">${i18n("Get")}</span>
|
||||
<span class="text-muted-foreground ml-1">${filename}</span>
|
||||
</div>
|
||||
`;
|
||||
case "delete":
|
||||
return html`
|
||||
<div
|
||||
class="text-sm cursor-pointer hover:bg-muted/50 rounded-sm px-2 py-1"
|
||||
@click=${() => this.openArtifact(params.filename)}
|
||||
>
|
||||
<span class="font-medium">${i18n("Delete")}</span>
|
||||
<span class="text-muted-foreground ml-1">${filename}</span>
|
||||
</div>
|
||||
`;
|
||||
case "logs":
|
||||
return html`
|
||||
<div
|
||||
class="text-sm cursor-pointer hover:bg-muted/50 rounded-sm px-2 py-1"
|
||||
@click=${() => this.openArtifact(params.filename)}
|
||||
>
|
||||
<span class="font-medium">${i18n("Get logs")}</span>
|
||||
<span class="text-muted-foreground ml-1">${filename}</span>
|
||||
</div>
|
||||
`;
|
||||
default:
|
||||
// Fallback for any command not yet handled during streaming
|
||||
return html`
|
||||
<div
|
||||
class="text-sm cursor-pointer hover:bg-muted/50 rounded-sm px-2 py-1"
|
||||
@click=${() => this.openArtifact(params.filename)}
|
||||
>
|
||||
<span class="font-medium">${commandLabel}</span>
|
||||
<span class="text-muted-foreground ml-1">${filename}</span>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
renderResult(params: ArtifactsParams, result: ToolResultMessage<undefined>): TemplateResult {
|
||||
// Make result clickable to focus the referenced file when applicable
|
||||
const content = result.output || i18n("(no output)");
|
||||
return html`
|
||||
<div class="cursor-pointer hover:bg-muted/50 rounded-sm px-2 py-1" @click=${() => this.openArtifact(params.filename)}>
|
||||
${plainOutput(content)}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
// Re-apply artifacts by scanning a message list (optional utility)
|
||||
public async reconstructFromMessages(messages: Array<Message | { role: "aborted" }>): Promise<void> {
|
||||
const toolCalls = new Map<string, ToolCall>();
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
export { ArtifactElement } from "./ArtifactElement.js";
|
||||
export { type Artifact, ArtifactsPanel, type ArtifactsParams } from "./artifacts.js";
|
||||
export { ArtifactsToolRenderer } from "./artifacts-tool-renderer.js";
|
||||
export { HtmlArtifact } from "./HtmlArtifact.js";
|
||||
export { MarkdownArtifact } from "./MarkdownArtifact.js";
|
||||
export { SvgArtifact } from "./SvgArtifact.js";
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue