blob: 9018275a9bdf705af502198fe98299d033fc6d1f [file] [log] [blame]
banksean333aa672025-07-13 19:49:21 +00001import { html, TemplateResult } from "lit";
2import { customElement, property, state } from "lit/decorators.js";
3import { ToolCall } from "../types";
4import { SketchTailwindElement } from "./sketch-tailwind-element";
5
6@customElement("sketch-tool-card-base")
7export class SketchToolCardBase extends SketchTailwindElement {
8 @property() toolCall: ToolCall;
9 @property() open: boolean;
10 @property() summaryContent: TemplateResult | string = "";
11 @property() inputContent: TemplateResult | string = "";
12 @property() resultContent: TemplateResult | string = "";
13 @state() detailsVisible: boolean = false;
14
15 _cancelToolCall = async (tool_call_id: string, button: HTMLButtonElement) => {
16 button.innerText = "Cancelling";
17 button.disabled = true;
18 try {
19 const response = await fetch("cancel", {
20 method: "POST",
21 headers: { "Content-Type": "application/json" },
22 body: JSON.stringify({
23 tool_call_id: tool_call_id,
24 reason: "user requested cancellation",
25 }),
26 });
27 if (response.ok) {
28 button.parentElement.removeChild(button);
29 } else {
30 button.innerText = "Cancel";
31 }
32 } catch (e) {
33 console.error("cancel", tool_call_id, e);
34 }
35 };
36
37 _toggleDetails(e: Event) {
38 e.stopPropagation();
39 this.detailsVisible = !this.detailsVisible;
40 }
41
42 render() {
43 // Status indicator based on result
44 let statusIcon = html`<span
45 class="flex items-center justify-center text-sm text-yellow-500 animate-spin"
46 >⏳</span
47 >`;
48 if (this.toolCall?.result_message) {
49 statusIcon = this.toolCall?.result_message.tool_error
50 ? html`<span
banksean1ee0bc62025-07-22 23:24:18 +000051 class="flex items-center justify-center text-sm text-gray-500 dark:text-gray-400"
banksean333aa672025-07-13 19:49:21 +000052 >〰️</span
53 >`
54 : html`<span
banksean1ee0bc62025-07-22 23:24:18 +000055 class="flex items-center justify-center text-sm text-green-600 dark:text-green-400"
banksean333aa672025-07-13 19:49:21 +000056 >✓</span
57 >`;
58 }
59
60 // Cancel button for pending operations
61 const cancelButton = this.toolCall?.result_message
62 ? ""
63 : html`<button
64 class="cursor-pointer text-white bg-red-600 hover:bg-red-700 disabled:bg-gray-400 disabled:cursor-not-allowed border-none rounded text-xs px-1.5 py-0.5 whitespace-nowrap min-w-[50px]"
65 title="Cancel this operation"
66 @click=${(e: Event) => {
67 e.stopPropagation();
68 this._cancelToolCall(
69 this.toolCall?.tool_call_id,
70 e.target as HTMLButtonElement,
71 );
72 }}
73 >
74 Cancel
75 </button>`;
76
77 // Elapsed time display
78 const elapsed = this.toolCall?.result_message?.elapsed
79 ? html`<span
banksean1ee0bc62025-07-22 23:24:18 +000080 class="text-xs text-gray-600 dark:text-gray-400 whitespace-nowrap min-w-[40px] text-right"
banksean333aa672025-07-13 19:49:21 +000081 >${(this.toolCall?.result_message?.elapsed / 1e9).toFixed(1)}s</span
82 >`
83 : html`<span
banksean1ee0bc62025-07-22 23:24:18 +000084 class="text-xs text-gray-600 dark:text-gray-400 whitespace-nowrap min-w-[40px] text-right"
banksean333aa672025-07-13 19:49:21 +000085 ></span>`;
86
87 // Initialize details visibility based on open property
88 if (this.open && !this.detailsVisible) {
89 this.detailsVisible = true;
90 }
91
92 return html`<div class="block max-w-full w-full box-border overflow-hidden">
93 <div class="flex flex-col w-full">
94 <div
banksean1ee0bc62025-07-22 23:24:18 +000095 class="flex w-full box-border py-1.5 px-2 pl-3 items-center gap-2 cursor-pointer rounded relative overflow-hidden flex-wrap hover:bg-black/[0.02] dark:hover:bg-white/[0.05]"
banksean333aa672025-07-13 19:49:21 +000096 @click=${this._toggleDetails}
97 >
98 <span
banksean1ee0bc62025-07-22 23:24:18 +000099 class="font-mono font-medium text-gray-700 dark:text-gray-300 bg-black/[0.05] dark:bg-white/[0.1] rounded px-1.5 py-0.5 flex-shrink-0 min-w-[45px] text-xs text-center whitespace-nowrap"
banksean333aa672025-07-13 19:49:21 +0000100 >${this.toolCall?.name}</span
101 >
102 <span
banksean1ee0bc62025-07-22 23:24:18 +0000103 class="whitespace-normal break-words flex-grow flex-shrink text-gray-700 dark:text-gray-300 font-mono text-xs px-1 min-w-[50px] max-w-[calc(100%-150px)] inline-block"
banksean333aa672025-07-13 19:49:21 +0000104 >${this.summaryContent}</span
105 >
106 <div
107 class="flex items-center gap-3 ml-auto flex-shrink-0 min-w-[120px] justify-end pr-2"
108 >
109 ${statusIcon} ${elapsed} ${cancelButton}
110 </div>
111 </div>
112 <div
113 class="${this.detailsVisible
114 ? "block"
banksean1ee0bc62025-07-22 23:24:18 +0000115 : "hidden"} p-2 bg-black/[0.02] dark:bg-white/[0.05] mt-px border-t border-black/[0.05] dark:border-white/[0.1] font-mono text-xs text-gray-800 dark:text-gray-200 rounded-b max-w-full w-full box-border overflow-hidden"
banksean333aa672025-07-13 19:49:21 +0000116 >
117 ${this.inputContent
118 ? html`<div class="mb-2">${this.inputContent}</div>`
119 : ""}
120 ${this.resultContent
Josh Bleecher Snyder3dd3e412025-07-22 20:32:03 -0700121 ? html`<div class="${this.inputContent ? "mt-2" : ""}">
122 ${this.resultContent}
123 </div>`
banksean333aa672025-07-13 19:49:21 +0000124 : ""}
125 </div>
126 </div>
127 </div>`;
128 }
129}
130
131declare global {
132 interface HTMLElementTagNameMap {
133 "sketch-tool-card-base": SketchToolCardBase;
134 }
135}