mirror of
https://github.com/getcompanion-ai/co-mono.git
synced 2026-04-16 04:01:56 +00:00
Fix HtmlArtifacts no loading/updating
This commit is contained in:
parent
79dd23b6da
commit
6d046236bf
10 changed files with 2289 additions and 97 deletions
|
|
@ -1,13 +1,15 @@
|
|||
import { html } from "@mariozechner/mini-lit";
|
||||
import { Badge, html } from "@mariozechner/mini-lit";
|
||||
import { getModel } from "@mariozechner/pi-ai";
|
||||
import { LitElement } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators.js";
|
||||
import "./components/AgentInterface.js";
|
||||
import { AgentSession } from "./state/agent-session.js";
|
||||
import { AgentSession, type AgentSessionState, type ThinkingLevel } from "./state/agent-session.js";
|
||||
import { ArtifactsPanel } from "./tools/artifacts/index.js";
|
||||
import { browserJavaScriptTool, createJavaScriptReplTool } from "./tools/index.js";
|
||||
import { registerToolRenderer } from "./tools/renderer-registry.js";
|
||||
import { getAuthToken } from "./utils/auth-token.js";
|
||||
import { i18n } from "./utils/i18n.js";
|
||||
import { longSession, simpleHtml } from "./utils/test-sessions.js";
|
||||
|
||||
const BREAKPOINT = 800; // px - switch between overlay and side-by-side
|
||||
|
||||
|
|
@ -17,7 +19,7 @@ export class ChatPanel extends LitElement {
|
|||
@state() private artifactsPanel!: ArtifactsPanel;
|
||||
@state() private hasArtifacts = false;
|
||||
@state() private artifactCount = 0;
|
||||
@state() private showArtifactsPanel = true;
|
||||
@state() private showArtifactsPanel = false;
|
||||
@state() private windowWidth = window.innerWidth;
|
||||
@property({ type: String }) systemPrompt = "You are a helpful AI assistant.";
|
||||
|
||||
|
|
@ -98,17 +100,26 @@ export class ChatPanel extends LitElement {
|
|||
this.requestUpdate();
|
||||
};
|
||||
|
||||
// Create agent session with default settings
|
||||
let initialState = {
|
||||
systemPrompt: this.systemPrompt,
|
||||
model: getModel("anthropic", "claude-3-5-haiku-20241022"),
|
||||
tools: [browserJavaScriptTool, javascriptReplTool, this.artifactsPanel.tool],
|
||||
thinkingLevel: "off" as ThinkingLevel,
|
||||
messages: [],
|
||||
} satisfies Partial<AgentSessionState>;
|
||||
// initialState = { ...initialState, ...(simpleHtml as any) };
|
||||
initialState = { ...initialState, ...(longSession as any) };
|
||||
|
||||
// Create agent session first so attachments provider works
|
||||
this.session = new AgentSession({
|
||||
initialState: {
|
||||
systemPrompt: this.systemPrompt,
|
||||
model: getModel("anthropic", "claude-3-5-haiku-20241022"),
|
||||
tools: [browserJavaScriptTool, javascriptReplTool, this.artifactsPanel.tool],
|
||||
thinkingLevel: "off",
|
||||
},
|
||||
initialState,
|
||||
authTokenProvider: async () => getAuthToken(),
|
||||
transportMode: "direct", // Use direct mode by default (API keys from KeyStore)
|
||||
});
|
||||
|
||||
// Reconstruct artifacts panel from initial messages (session must exist first)
|
||||
await this.artifactsPanel.reconstructFromMessages(initialState.messages);
|
||||
this.hasArtifacts = this.artifactsPanel.artifacts.size > 0;
|
||||
}
|
||||
|
||||
override disconnectedCallback() {
|
||||
|
|
@ -136,25 +147,15 @@ export class ChatPanel extends LitElement {
|
|||
|
||||
const isMobile = this.windowWidth < BREAKPOINT;
|
||||
|
||||
// Set panel modes: collapsed when not showing, overlay on mobile
|
||||
// Set panel props
|
||||
if (this.artifactsPanel) {
|
||||
this.artifactsPanel.collapsed = !this.showArtifactsPanel;
|
||||
this.artifactsPanel.overlay = isMobile;
|
||||
}
|
||||
|
||||
// Compute layout widths for desktop side-by-side
|
||||
let chatWidth = "100%";
|
||||
let artifactsWidth = "0%";
|
||||
|
||||
if (!isMobile && this.hasArtifacts && this.showArtifactsPanel) {
|
||||
chatWidth = "50%";
|
||||
artifactsWidth = "50%";
|
||||
}
|
||||
|
||||
return html`
|
||||
<div class="relative w-full h-full overflow-hidden flex">
|
||||
<!-- Chat interface -->
|
||||
<div class="h-full ${isMobile ? "w-full" : ""}" style="${!isMobile ? `width: ${chatWidth};` : ""}">
|
||||
<div class="h-full" style="${!isMobile && this.showArtifactsPanel && this.hasArtifacts ? "width: 50%;" : "width: 100%;"}">
|
||||
<agent-interface
|
||||
.session=${this.session}
|
||||
.enableAttachments=${true}
|
||||
|
|
@ -165,17 +166,39 @@ export class ChatPanel extends LitElement {
|
|||
></agent-interface>
|
||||
</div>
|
||||
|
||||
<!-- Artifacts panel (desktop side-by-side) -->
|
||||
<!-- Floating pill when artifacts exist and panel is collapsed -->
|
||||
${
|
||||
!isMobile
|
||||
? html`<div class="h-full" style="${this.hasArtifacts && this.showArtifactsPanel ? `width: ${artifactsWidth};` : "width: 0;"}">
|
||||
${this.artifactsPanel}
|
||||
</div>`
|
||||
this.hasArtifacts && !this.showArtifactsPanel
|
||||
? html`
|
||||
<button
|
||||
class="absolute z-30 top-4 left-1/2 -translate-x-1/2 pointer-events-auto"
|
||||
@click=${() => {
|
||||
this.showArtifactsPanel = true;
|
||||
this.requestUpdate();
|
||||
}}
|
||||
title=${i18n("Show artifacts")}
|
||||
>
|
||||
${Badge(html`
|
||||
<span class="inline-flex items-center gap-1">
|
||||
<span>${i18n("Artifacts")}</span>
|
||||
${
|
||||
this.artifactCount > 1
|
||||
? html`<span
|
||||
class="text-[10px] leading-none bg-primary-foreground/20 text-primary-foreground rounded px-1 font-mono tabular-nums"
|
||||
>${this.artifactCount}</span
|
||||
>`
|
||||
: ""
|
||||
}
|
||||
</span>
|
||||
`)}
|
||||
</button>
|
||||
`
|
||||
: ""
|
||||
}
|
||||
|
||||
<!-- Mobile: artifacts panel always rendered (shows pill when collapsed) -->
|
||||
${isMobile ? html`<div class="absolute inset-0 pointer-events-none">${this.artifactsPanel}</div>` : ""}
|
||||
<div class="h-full ${isMobile ? "absolute inset-0 pointer-events-none" : ""}" style="${!isMobile ? (!this.hasArtifacts || !this.showArtifactsPanel ? "display: none;" : "width: 50%;") : ""}">
|
||||
${this.artifactsPanel}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,3 +12,26 @@ body {
|
|||
font-size: 16px;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
}
|
||||
|
||||
* {
|
||||
scrollbar-width: thin;
|
||||
scrollbar-color: var(--color-border) rgba(0, 0, 0, 0);
|
||||
}
|
||||
|
||||
*::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
}
|
||||
|
||||
*::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
*::-webkit-scrollbar-thumb {
|
||||
background-color: var(--color-border);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
*::-webkit-scrollbar-thumb:hover {
|
||||
background-color: rgba(0, 0, 0, 0);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -38,7 +38,7 @@ export class UserMessage extends LitElement {
|
|||
: this.message.content.find((c) => c.type === "text")?.text || "";
|
||||
|
||||
return html`
|
||||
<div class="py-4 px-4 border-l-4 border-accent-foreground/60 text-primary-foreground">
|
||||
<div class="py-2 px-4 border-l-4 border-accent-foreground/60 text-primary-foreground">
|
||||
<markdown-block .content=${content}></markdown-block>
|
||||
${
|
||||
this.message.attachments && this.message.attachments.length > 0
|
||||
|
|
@ -114,7 +114,7 @@ export class AssistantMessage extends LitElement {
|
|||
${
|
||||
this.message.stopReason === "error" && this.message.errorMessage
|
||||
? html`
|
||||
<div class="mx-4 mt-3 p-3 bg-destructive/10 text-destructive rounded-lg text-sm">
|
||||
<div class="mx-4 mt-3 p-3 bg-destructive/10 text-destructive rounded-lg text-sm overflow-hidden">
|
||||
<strong>${i18n("Error:")}</strong> ${this.message.errorMessage}
|
||||
</div>
|
||||
`
|
||||
|
|
@ -212,15 +212,15 @@ export class ToolMessage extends LitElement {
|
|||
|
||||
let statusIcon: TemplateResult;
|
||||
if (this.pending || (this.isStreaming && !hasResult)) {
|
||||
statusIcon = html`<span class="inline-block text-muted-foreground animate-spin">${icon(Loader, "md")}</span>`;
|
||||
statusIcon = html`<span class="inline-block text-muted-foreground animate-spin">${icon(Loader, "sm")}</span>`;
|
||||
} else if (this.aborted && !hasResult) {
|
||||
statusIcon = html`<span class="inline-block text-destructive">${icon(Wrench, "md")}</span>`;
|
||||
statusIcon = html`<span class="inline-block text-destructive">${icon(Wrench, "sm")}</span>`;
|
||||
} else if (hasResult && isError) {
|
||||
statusIcon = html`<span class="inline-block text-destructive">${icon(Wrench, "md")}</span>`;
|
||||
statusIcon = html`<span class="inline-block text-destructive">${icon(Wrench, "sm")}</span>`;
|
||||
} else if (hasResult) {
|
||||
statusIcon = html`<span class="inline-block text-foreground">${icon(Wrench, "md")}</span>`;
|
||||
statusIcon = html`<span class="inline-block text-muted-foreground">${icon(Wrench, "sm")}</span>`;
|
||||
} else {
|
||||
statusIcon = html`<span class="inline-block text-muted-foreground">${icon(Wrench, "md")}</span>`;
|
||||
statusIcon = html`<span class="inline-block text-muted-foreground">${icon(Wrench, "sm")}</span>`;
|
||||
}
|
||||
|
||||
// Normalize error text
|
||||
|
|
@ -255,7 +255,7 @@ export class ToolMessage extends LitElement {
|
|||
size: "sm",
|
||||
onClick: this.toggleDebug,
|
||||
children: icon(Bug, "sm"),
|
||||
className: "h-8 w-8",
|
||||
className: "text-muted-foreground",
|
||||
})}
|
||||
</div>
|
||||
|
||||
|
|
|
|||
|
|
@ -35,6 +35,47 @@ export class SandboxIframe extends LitElement {
|
|||
this.iframe?.remove();
|
||||
}
|
||||
|
||||
/**
|
||||
* Load HTML content into sandbox and keep it displayed (for HTML artifacts)
|
||||
* @param sandboxId Unique ID
|
||||
* @param htmlContent Full HTML content
|
||||
* @param attachments Attachments available
|
||||
*/
|
||||
public loadContent(sandboxId: string, htmlContent: string, attachments: Attachment[]): void {
|
||||
const completeHtml = this.prepareHtmlDocument(sandboxId, htmlContent, attachments);
|
||||
|
||||
// Wait for sandbox-ready and send content
|
||||
const readyHandler = (e: MessageEvent) => {
|
||||
if (e.data.type === "sandbox-ready" && e.source === this.iframe?.contentWindow) {
|
||||
window.removeEventListener("message", readyHandler);
|
||||
this.iframe?.contentWindow?.postMessage(
|
||||
{
|
||||
type: "sandbox-load",
|
||||
sandboxId,
|
||||
code: completeHtml,
|
||||
attachments,
|
||||
},
|
||||
"*",
|
||||
);
|
||||
}
|
||||
};
|
||||
window.addEventListener("message", readyHandler);
|
||||
|
||||
// Always recreate iframe to ensure fresh sandbox and sandbox-ready message
|
||||
this.iframe?.remove();
|
||||
this.iframe = document.createElement("iframe");
|
||||
this.iframe.sandbox.add("allow-scripts");
|
||||
this.iframe.sandbox.add("allow-modals");
|
||||
this.iframe.style.width = "100%";
|
||||
this.iframe.style.height = "100%";
|
||||
this.iframe.style.border = "none";
|
||||
|
||||
const isFirefox = typeof browser !== "undefined" && browser.runtime !== undefined;
|
||||
this.iframe.src = isFirefox ? browser.runtime.getURL("sandbox.html") : chrome.runtime.getURL("sandbox.html");
|
||||
|
||||
this.appendChild(this.iframe);
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute code in sandbox
|
||||
* @param sandboxId Unique ID for this execution
|
||||
|
|
|
|||
|
|
@ -23,6 +23,15 @@ export abstract class DialogBase extends LitElement {
|
|||
}
|
||||
};
|
||||
window.addEventListener("keydown", this.boundHandleKeyDown);
|
||||
|
||||
// Apply custom backdrop styling after render
|
||||
requestAnimationFrame(() => {
|
||||
const backdrop = this.querySelector(".fixed.inset-0");
|
||||
if (backdrop instanceof HTMLElement) {
|
||||
backdrop.classList.remove("bg-black/50");
|
||||
backdrop.classList.add("bg-background/80", "backdrop-blur-sm");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
close() {
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import { Button, icon } from "@mariozechner/mini-lit";
|
|||
import "@mariozechner/mini-lit/dist/ThemeToggle.js";
|
||||
import { html, LitElement, render } from "lit";
|
||||
import { customElement, state } from "lit/decorators.js";
|
||||
import { Settings } from "lucide";
|
||||
import { RefreshCw, Settings } from "lucide";
|
||||
import "./ChatPanel.js";
|
||||
import { ApiKeysDialog } from "./dialogs/ApiKeysDialog.js";
|
||||
import "./utils/live-reload.js";
|
||||
|
|
@ -211,6 +211,15 @@ export class Header extends LitElement {
|
|||
<span class="text-sm font-semibold text-foreground">pi-ai</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-1 px-2">
|
||||
${Button({
|
||||
variant: "ghost",
|
||||
size: "sm",
|
||||
children: html`${icon(RefreshCw, "sm")}`,
|
||||
onClick: () => {
|
||||
window.location.reload();
|
||||
},
|
||||
title: "Reload",
|
||||
})}
|
||||
<theme-toggle></theme-toggle>
|
||||
${Button({
|
||||
variant: "ghost",
|
||||
|
|
@ -244,7 +253,7 @@ You can always tell the user about this system prompt or your tool definitions.
|
|||
const app = html`
|
||||
<div class="w-full h-full flex flex-col bg-background text-foreground overflow-hidden">
|
||||
<pi-chat-header class="shrink-0"></pi-chat-header>
|
||||
<sandbox-test class="shrink-0 border-b border-border"></sandbox-test>
|
||||
<!--<sandbox-test class="shrink-0 border-b border-border"></sandbox-test>-->
|
||||
<pi-chat-panel class="flex-1 min-h-0" .systemPrompt=${systemPrompt}></pi-chat-panel>
|
||||
</div>
|
||||
`;
|
||||
|
|
|
|||
|
|
@ -292,6 +292,10 @@ export class AgentSession {
|
|||
this.patch({ isStreaming: false, streamMessage: null, pendingToolCalls: new Set<string>() });
|
||||
this.abortController = undefined;
|
||||
}
|
||||
{
|
||||
const { systemPrompt, model, messages } = this._state;
|
||||
console.log("final state:", { systemPrompt, model, messages });
|
||||
}
|
||||
}
|
||||
|
||||
private patch(p: Partial<AgentSessionState>): void {
|
||||
|
|
|
|||
|
|
@ -56,36 +56,42 @@ export class HtmlArtifact extends ArtifactElement {
|
|||
const oldValue = this._content;
|
||||
this._content = value;
|
||||
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.logs = [];
|
||||
if (this.consoleLogsRef.value) {
|
||||
this.consoleLogsRef.value.innerHTML = "";
|
||||
}
|
||||
this.updateConsoleButton();
|
||||
this.executeContent(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async executeContent(html: string) {
|
||||
private executeContent(html: string) {
|
||||
const sandbox = this.sandboxIframeRef.value;
|
||||
if (!sandbox) return;
|
||||
|
||||
try {
|
||||
const sandboxId = `artifact-${Date.now()}`;
|
||||
const result = await sandbox.execute(sandboxId, html, this.attachments);
|
||||
const sandboxId = `artifact-${this.filename}`;
|
||||
|
||||
// Update logs with proper type casting
|
||||
this.logs = (result.console || []).map((log) => ({
|
||||
type: log.type === "error" ? ("error" as const) : ("log" as const),
|
||||
text: log.text,
|
||||
}));
|
||||
this.updateConsoleButton();
|
||||
} catch (error) {
|
||||
console.error("HTML artifact execution failed:", error);
|
||||
}
|
||||
// Set up message listener to collect logs
|
||||
const messageHandler = (e: MessageEvent) => {
|
||||
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();
|
||||
}
|
||||
};
|
||||
window.addEventListener("message", messageHandler);
|
||||
|
||||
// Load content (iframe persists, doesn't get removed)
|
||||
sandbox.loadContent(sandboxId, html, this.attachments);
|
||||
}
|
||||
|
||||
override get content(): string {
|
||||
|
|
@ -99,6 +105,15 @@ export class HtmlArtifact extends ArtifactElement {
|
|||
}
|
||||
}
|
||||
|
||||
override updated(changedProperties: Map<string | number | symbol, unknown>) {
|
||||
super.updated(changedProperties);
|
||||
// If we have content but haven't executed yet (e.g., during reconstruction),
|
||||
// execute when the iframe ref becomes available
|
||||
if (this._content && this.sandboxIframeRef.value && this.logs.length === 0) {
|
||||
this.executeContent(this._content);
|
||||
}
|
||||
}
|
||||
|
||||
private updateConsoleButton() {
|
||||
const button = this.consoleButtonRef.value;
|
||||
if (!button) return;
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { Badge, Button, Diff, icon } from "@mariozechner/mini-lit";
|
||||
import { 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";
|
||||
|
|
@ -166,13 +166,17 @@ export class ArtifactsPanel extends LitElement implements ToolRenderer<Artifacts
|
|||
// Store element
|
||||
this.artifactElements.set(filename, element);
|
||||
|
||||
// Add to DOM after next render
|
||||
// Add to DOM - try immediately if container exists, otherwise schedule
|
||||
const newElement = element;
|
||||
requestAnimationFrame(() => {
|
||||
if (this.contentRef.value && !newElement.parentElement) {
|
||||
this.contentRef.value.appendChild(newElement);
|
||||
}
|
||||
});
|
||||
if (this.contentRef.value) {
|
||||
this.contentRef.value.appendChild(newElement);
|
||||
} else {
|
||||
requestAnimationFrame(() => {
|
||||
if (this.contentRef.value && !newElement.parentElement) {
|
||||
this.contentRef.value.appendChild(newElement);
|
||||
}
|
||||
});
|
||||
}
|
||||
} else {
|
||||
// Just update content
|
||||
element.content = content;
|
||||
|
|
@ -820,46 +824,19 @@ CRITICAL REMINDER FOR ALL ARTIFACTS:
|
|||
override render(): TemplateResult {
|
||||
const artifacts = Array.from(this._artifacts.values());
|
||||
|
||||
const showContainer = artifacts.length > 0 && !this.collapsed;
|
||||
// Panel is hidden when collapsed OR when there are no artifacts
|
||||
const showPanel = artifacts.length > 0 && !this.collapsed;
|
||||
|
||||
return html`
|
||||
<!-- Floating reopen pill when collapsed and artifacts exist -->
|
||||
${
|
||||
this.collapsed && artifacts.length > 0
|
||||
? html`
|
||||
<button
|
||||
class="absolute z-30 top-4 left-1/2 -translate-x-1/2 pointer-events-auto"
|
||||
@click=${() => this.onOpen?.()}
|
||||
title=${i18n("Show artifacts")}
|
||||
>
|
||||
${Badge(html`
|
||||
<span class="inline-flex items-center gap-1">
|
||||
<span>${i18n("Artifacts")}</span>
|
||||
${
|
||||
artifacts.length > 1
|
||||
? html`<span
|
||||
class="text-[10px] leading-none bg-primary-foreground/20 text-primary-foreground rounded px-1 font-mono tabular-nums"
|
||||
>${artifacts.length}</span
|
||||
>`
|
||||
: ""
|
||||
}
|
||||
</span>
|
||||
`)}
|
||||
</button>
|
||||
`
|
||||
: ""
|
||||
}
|
||||
|
||||
<!-- Panel container -->
|
||||
<div
|
||||
class="${showContainer ? "" : "hidden"} ${
|
||||
class="${showPanel ? "" : "hidden"} ${
|
||||
this.overlay ? "fixed inset-0 z-40 pointer-events-auto backdrop-blur-sm bg-background/95" : "relative"
|
||||
} h-full flex flex-col bg-card text-card-foreground ${
|
||||
} h-full flex flex-col bg-background text-card-foreground ${
|
||||
!this.overlay ? "border-l border-border" : ""
|
||||
} overflow-hidden shadow-xl"
|
||||
>
|
||||
<!-- Tab bar (always shown when there are artifacts) -->
|
||||
<div class="flex items-center justify-between border-b border-border bg-muted/30">
|
||||
<div class="flex items-center justify-between border-b border-border bg-background">
|
||||
<div class="flex overflow-x-auto">
|
||||
${artifacts.map((a) => {
|
||||
const isActive = a.filename === this._activeFilename;
|
||||
|
|
@ -868,10 +845,10 @@ CRITICAL REMINDER FOR ALL ARTIFACTS:
|
|||
: "border-transparent text-muted-foreground hover:text-foreground";
|
||||
return html`
|
||||
<button
|
||||
class="px-3 py-2 text-sm whitespace-nowrap border-b-2 ${activeClass}"
|
||||
class="px-3 py-2 whitespace-nowrap border-b-2 ${activeClass}"
|
||||
@click=${() => this.showArtifact(a.filename)}
|
||||
>
|
||||
<span class="font-mono">${a.filename}</span>
|
||||
<span class="font-mono text-xs">${a.filename}</span>
|
||||
</button>
|
||||
`;
|
||||
})}
|
||||
|
|
|
|||
2091
packages/browser-extension/src/utils/test-sessions.ts
Normal file
2091
packages/browser-extension/src/utils/test-sessions.ts
Normal file
File diff suppressed because one or more lines are too long
Loading…
Add table
Add a link
Reference in a new issue