mirror of
https://github.com/getcompanion-ai/co-mono.git
synced 2026-04-21 11:04:35 +00:00
Add collapsible tool renderers with animated expand/collapse
- Add renderCollapsibleHeader() to renderer-registry - Places chevron on right, spinner on left - Toggles between ChevronRight (collapsed) and ChevronDown (expanded) - Uses max-h-0/max-h-[2000px] with transition-all for smooth animation - Dynamically adds/removes mt-3 to avoid margin when collapsed - Update javascript-repl renderer to use collapsible sections - Code and console output hidden by default - Only file attachments remain visible - 300ms smooth animation on expand/collapse - Export renderCollapsibleHeader from web-ui index
This commit is contained in:
parent
8ec9805112
commit
f646a29d1a
3 changed files with 95 additions and 29 deletions
|
|
@ -69,7 +69,7 @@ export { TextArtifact } from "./tools/artifacts/TextArtifact.js";
|
||||||
// Tools
|
// Tools
|
||||||
export { getToolRenderer, registerToolRenderer, renderTool } from "./tools/index.js";
|
export { getToolRenderer, registerToolRenderer, renderTool } from "./tools/index.js";
|
||||||
export { createJavaScriptReplTool, javascriptReplTool } from "./tools/javascript-repl.js";
|
export { createJavaScriptReplTool, javascriptReplTool } from "./tools/javascript-repl.js";
|
||||||
export { renderHeader } from "./tools/renderer-registry.js";
|
export { renderCollapsibleHeader, renderHeader } from "./tools/renderer-registry.js";
|
||||||
export { BashRenderer } from "./tools/renderers/BashRenderer.js";
|
export { BashRenderer } from "./tools/renderers/BashRenderer.js";
|
||||||
export { CalculateRenderer } from "./tools/renderers/CalculateRenderer.js";
|
export { CalculateRenderer } from "./tools/renderers/CalculateRenderer.js";
|
||||||
// Tool renderers
|
// Tool renderers
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,12 @@
|
||||||
import { html, i18n, type TemplateResult } from "@mariozechner/mini-lit";
|
import { html, i18n, type TemplateResult } from "@mariozechner/mini-lit";
|
||||||
import type { AgentTool, ToolResultMessage } from "@mariozechner/pi-ai";
|
import type { AgentTool, ToolResultMessage } from "@mariozechner/pi-ai";
|
||||||
import { type Static, Type } from "@sinclair/typebox";
|
import { type Static, Type } from "@sinclair/typebox";
|
||||||
|
import { createRef, ref } from "lit/directives/ref.js";
|
||||||
import { Code } from "lucide";
|
import { Code } from "lucide";
|
||||||
import { type SandboxFile, SandboxIframe, type SandboxResult } from "../components/SandboxedIframe.js";
|
import { type SandboxFile, SandboxIframe, type SandboxResult } from "../components/SandboxedIframe.js";
|
||||||
import type { Attachment } from "../utils/attachment-utils.js";
|
import type { Attachment } from "../utils/attachment-utils.js";
|
||||||
|
|
||||||
import { registerToolRenderer, renderHeader } from "./renderer-registry.js";
|
import { registerToolRenderer, renderCollapsibleHeader, renderHeader } from "./renderer-registry.js";
|
||||||
import type { ToolRenderer } from "./types.js";
|
import type { ToolRenderer } from "./types.js";
|
||||||
|
|
||||||
// Execute JavaScript code with attachments using SandboxedIframe
|
// Execute JavaScript code with attachments using SandboxedIframe
|
||||||
|
|
@ -93,10 +94,25 @@ export type JavaScriptReplToolResult = {
|
||||||
};
|
};
|
||||||
|
|
||||||
const javascriptReplSchema = Type.Object({
|
const javascriptReplSchema = Type.Object({
|
||||||
title: Type.String({ description: "Brief title describing what the code snippet tries to achieve" }),
|
title: Type.String({
|
||||||
|
description:
|
||||||
|
"Brief title describing what the code snippet tries to achieve in active form, e.g. 'Calculating sum'",
|
||||||
|
}),
|
||||||
code: Type.String({ description: "JavaScript code to execute" }),
|
code: Type.String({ description: "JavaScript code to execute" }),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export type JavaScriptReplParams = Static<typeof javascriptReplSchema>;
|
||||||
|
|
||||||
|
interface JavaScriptReplResult {
|
||||||
|
output?: string;
|
||||||
|
files?: Array<{
|
||||||
|
fileName: string;
|
||||||
|
mimeType: string;
|
||||||
|
size: number;
|
||||||
|
contentBase64: string;
|
||||||
|
}>;
|
||||||
|
}
|
||||||
|
|
||||||
export function createJavaScriptReplTool(): AgentTool<typeof javascriptReplSchema, JavaScriptReplToolResult> & {
|
export function createJavaScriptReplTool(): AgentTool<typeof javascriptReplSchema, JavaScriptReplToolResult> & {
|
||||||
attachmentsProvider?: () => Attachment[];
|
attachmentsProvider?: () => Attachment[];
|
||||||
sandboxUrlProvider?: () => string;
|
sandboxUrlProvider?: () => string;
|
||||||
|
|
@ -230,22 +246,6 @@ Global variables:
|
||||||
// Export a default instance for backward compatibility
|
// Export a default instance for backward compatibility
|
||||||
export const javascriptReplTool = createJavaScriptReplTool();
|
export const javascriptReplTool = createJavaScriptReplTool();
|
||||||
|
|
||||||
// JavaScript REPL renderer with streaming support
|
|
||||||
|
|
||||||
interface JavaScriptReplParams {
|
|
||||||
code: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface JavaScriptReplResult {
|
|
||||||
output?: string;
|
|
||||||
files?: Array<{
|
|
||||||
fileName: string;
|
|
||||||
mimeType: string;
|
|
||||||
size: number;
|
|
||||||
contentBase64: string;
|
|
||||||
}>;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const javascriptReplRenderer: ToolRenderer<JavaScriptReplParams, JavaScriptReplResult> = {
|
export const javascriptReplRenderer: ToolRenderer<JavaScriptReplParams, JavaScriptReplResult> = {
|
||||||
render(
|
render(
|
||||||
params: JavaScriptReplParams | undefined,
|
params: JavaScriptReplParams | undefined,
|
||||||
|
|
@ -255,6 +255,10 @@ export const javascriptReplRenderer: ToolRenderer<JavaScriptReplParams, JavaScri
|
||||||
// Determine status
|
// Determine status
|
||||||
const state = result ? (result.isError ? "error" : "complete") : isStreaming ? "inprogress" : "inprogress";
|
const state = result ? (result.isError ? "error" : "complete") : isStreaming ? "inprogress" : "inprogress";
|
||||||
|
|
||||||
|
// Create refs for collapsible code section
|
||||||
|
const codeContentRef = createRef<HTMLDivElement>();
|
||||||
|
const codeChevronRef = createRef<HTMLSpanElement>();
|
||||||
|
|
||||||
// With result: show params + result
|
// With result: show params + result
|
||||||
if (result && params) {
|
if (result && params) {
|
||||||
const output = result.output || "";
|
const output = result.output || "";
|
||||||
|
|
@ -290,13 +294,15 @@ export const javascriptReplRenderer: ToolRenderer<JavaScriptReplParams, JavaScri
|
||||||
});
|
});
|
||||||
|
|
||||||
return html`
|
return html`
|
||||||
<div class="space-y-3">
|
<div>
|
||||||
${renderHeader(state, Code, i18n("Executing JavaScript"))}
|
${renderCollapsibleHeader(state, Code, params.title ? params.title : i18n("Executing JavaScript"), codeContentRef, codeChevronRef, false)}
|
||||||
<code-block .code=${params.code || ""} language="javascript"></code-block>
|
<div ${ref(codeContentRef)} class="max-h-0 overflow-hidden transition-all duration-300 space-y-3">
|
||||||
${output ? html`<console-block .content=${output} .variant=${result.isError ? "error" : "default"}></console-block>` : ""}
|
<code-block .code=${params.code || ""} language="javascript"></code-block>
|
||||||
|
${output ? html`<console-block .content=${output} .variant=${result.isError ? "error" : "default"}></console-block>` : ""}
|
||||||
|
</div>
|
||||||
${
|
${
|
||||||
attachments.length
|
attachments.length
|
||||||
? html`<div class="flex flex-wrap gap-2">
|
? html`<div class="flex flex-wrap gap-2 mt-3">
|
||||||
${attachments.map((att) => html`<attachment-tile .attachment=${att}></attachment-tile>`)}
|
${attachments.map((att) => html`<attachment-tile .attachment=${att}></attachment-tile>`)}
|
||||||
</div>`
|
</div>`
|
||||||
: ""
|
: ""
|
||||||
|
|
@ -308,9 +314,11 @@ export const javascriptReplRenderer: ToolRenderer<JavaScriptReplParams, JavaScri
|
||||||
// Just params (streaming or waiting for result)
|
// Just params (streaming or waiting for result)
|
||||||
if (params) {
|
if (params) {
|
||||||
return html`
|
return html`
|
||||||
<div class="space-y-3">
|
<div>
|
||||||
${renderHeader(state, Code, i18n("Executing JavaScript"))}
|
${renderCollapsibleHeader(state, Code, params.title ? params.title : i18n("Executing JavaScript"), codeContentRef, codeChevronRef, false)}
|
||||||
${params.code ? html`<code-block .code=${params.code} language="javascript"></code-block>` : ""}
|
<div ${ref(codeContentRef)} class="max-h-0 overflow-hidden transition-all duration-300">
|
||||||
|
${params.code ? html`<code-block .code=${params.code} language="javascript"></code-block>` : ""}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,7 @@
|
||||||
import { html, icon, type TemplateResult } from "@mariozechner/mini-lit";
|
import { html, icon, iconDOM, type TemplateResult } from "@mariozechner/mini-lit";
|
||||||
import { Loader } from "lucide";
|
import type { Ref } from "lit/directives/ref.js";
|
||||||
|
import { ref } from "lit/directives/ref.js";
|
||||||
|
import { ChevronDown, ChevronRight, Loader } from "lucide";
|
||||||
import type { ToolRenderer } from "./types.js";
|
import type { ToolRenderer } from "./types.js";
|
||||||
|
|
||||||
// Registry of tool renderers
|
// Registry of tool renderers
|
||||||
|
|
@ -54,3 +56,59 @@ export function renderHeader(state: "inprogress" | "complete" | "error", toolIco
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper to render a collapsible header for tool renderers
|
||||||
|
* Same as renderHeader but with a chevron button that toggles visibility of content
|
||||||
|
*/
|
||||||
|
export function renderCollapsibleHeader(
|
||||||
|
state: "inprogress" | "complete" | "error",
|
||||||
|
toolIcon: any,
|
||||||
|
text: string,
|
||||||
|
contentRef: Ref<HTMLElement>,
|
||||||
|
chevronRef: Ref<HTMLElement>,
|
||||||
|
defaultExpanded = false,
|
||||||
|
): TemplateResult {
|
||||||
|
const statusIcon = (iconComponent: any, color: string) =>
|
||||||
|
html`<span class="inline-block ${color}">${icon(iconComponent, "sm")}</span>`;
|
||||||
|
|
||||||
|
const toggleContent = (e: Event) => {
|
||||||
|
e.preventDefault();
|
||||||
|
const content = contentRef.value;
|
||||||
|
const chevron = chevronRef.value;
|
||||||
|
if (content && chevron) {
|
||||||
|
const isCollapsed = content.classList.contains("max-h-0");
|
||||||
|
if (isCollapsed) {
|
||||||
|
content.classList.remove("max-h-0");
|
||||||
|
content.classList.add("max-h-[2000px]", "mt-3");
|
||||||
|
chevron.innerHTML = iconDOM(ChevronDown, "sm").outerHTML;
|
||||||
|
} else {
|
||||||
|
content.classList.remove("max-h-[2000px]", "mt-3");
|
||||||
|
content.classList.add("max-h-0");
|
||||||
|
chevron.innerHTML = iconDOM(ChevronRight, "sm").outerHTML;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const toolIconColor =
|
||||||
|
state === "complete"
|
||||||
|
? "text-green-600 dark:text-green-500"
|
||||||
|
: state === "error"
|
||||||
|
? "text-destructive"
|
||||||
|
: "text-foreground";
|
||||||
|
|
||||||
|
return html`
|
||||||
|
<div class="flex items-center justify-between gap-2 text-sm text-muted-foreground">
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
${state === "inprogress" ? statusIcon(Loader, "text-foreground animate-spin") : ""}
|
||||||
|
${statusIcon(toolIcon, toolIconColor)}
|
||||||
|
<span>${text}</span>
|
||||||
|
</div>
|
||||||
|
<button @click=${toggleContent} class="hover:text-foreground transition-colors">
|
||||||
|
<span class="inline-block text-muted-foreground" ${ref(chevronRef)}>
|
||||||
|
${icon(defaultExpanded ? ChevronDown : ChevronRight, "sm")}
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue