| Sean McCullough | ec3ad1a | 2025-04-18 13:55:16 -0700 | [diff] [blame] | 1 | import { css, html, LitElement } from "lit"; |
| Philip Zeyliger | 16fa8b4 | 2025-05-02 04:28:16 +0000 | [diff] [blame] | 2 | import { customElement, property, state } from "lit/decorators.js"; |
| Pokey Rule | ef58e06 | 2025-05-07 13:32:58 +0100 | [diff] [blame^] | 3 | import { ToolCall } from "../types"; |
| Pokey Rule | 5e8aead | 2025-05-06 16:21:57 +0100 | [diff] [blame] | 4 | |
| Sean McCullough | ec3ad1a | 2025-04-18 13:55:16 -0700 | [diff] [blame] | 5 | @customElement("sketch-tool-card") |
| 6 | export class SketchToolCard extends LitElement { |
| Pokey Rule | 5e8aead | 2025-05-06 16:21:57 +0100 | [diff] [blame] | 7 | @property() toolCall: ToolCall; |
| 8 | @property() open: boolean; |
| 9 | @state() detailsVisible: boolean = false; |
| Philip Zeyliger | 16fa8b4 | 2025-05-02 04:28:16 +0000 | [diff] [blame] | 10 | |
| Sean McCullough | ec3ad1a | 2025-04-18 13:55:16 -0700 | [diff] [blame] | 11 | static styles = css` |
| 12 | .tool-call { |
| 13 | display: flex; |
| Philip Zeyliger | 16fa8b4 | 2025-05-02 04:28:16 +0000 | [diff] [blame] | 14 | flex-direction: column; |
| 15 | width: 100%; |
| 16 | } |
| Philip Zeyliger | 16fa8b4 | 2025-05-02 04:28:16 +0000 | [diff] [blame] | 17 | .tool-row { |
| 18 | display: flex; |
| 19 | width: 100%; |
| 20 | box-sizing: border-box; |
| 21 | padding: 6px 8px 6px 12px; |
| Sean McCullough | ec3ad1a | 2025-04-18 13:55:16 -0700 | [diff] [blame] | 22 | align-items: center; |
| Pokey Rule | 5e8aead | 2025-05-06 16:21:57 +0100 | [diff] [blame] | 23 | gap: 8px; |
| Philip Zeyliger | 16fa8b4 | 2025-05-02 04:28:16 +0000 | [diff] [blame] | 24 | cursor: pointer; |
| 25 | border-radius: 4px; |
| 26 | position: relative; |
| Pokey Rule | 5e8aead | 2025-05-06 16:21:57 +0100 | [diff] [blame] | 27 | overflow: hidden; |
| Philip Zeyliger | 16fa8b4 | 2025-05-02 04:28:16 +0000 | [diff] [blame] | 28 | } |
| Philip Zeyliger | 16fa8b4 | 2025-05-02 04:28:16 +0000 | [diff] [blame] | 29 | .tool-row:hover { |
| 30 | background-color: rgba(0, 0, 0, 0.02); |
| 31 | } |
| Philip Zeyliger | 16fa8b4 | 2025-05-02 04:28:16 +0000 | [diff] [blame] | 32 | .tool-name { |
| 33 | font-family: monospace; |
| 34 | font-weight: 500; |
| 35 | color: #444; |
| 36 | background-color: rgba(0, 0, 0, 0.05); |
| 37 | border-radius: 3px; |
| 38 | padding: 2px 6px; |
| 39 | flex-shrink: 0; |
| 40 | min-width: 45px; |
| Philip Zeyliger | 16fa8b4 | 2025-05-02 04:28:16 +0000 | [diff] [blame] | 41 | font-size: 12px; |
| 42 | text-align: center; |
| Sean McCullough | ec3ad1a | 2025-04-18 13:55:16 -0700 | [diff] [blame] | 43 | white-space: nowrap; |
| 44 | } |
| Philip Zeyliger | 16fa8b4 | 2025-05-02 04:28:16 +0000 | [diff] [blame] | 45 | .tool-success { |
| 46 | color: #5cb85c; |
| 47 | font-size: 14px; |
| 48 | } |
| Philip Zeyliger | 16fa8b4 | 2025-05-02 04:28:16 +0000 | [diff] [blame] | 49 | .tool-error { |
| Josh Bleecher Snyder | e750ec9 | 2025-05-05 23:01:57 +0000 | [diff] [blame] | 50 | color: #6c757d; |
| Philip Zeyliger | 16fa8b4 | 2025-05-02 04:28:16 +0000 | [diff] [blame] | 51 | font-size: 14px; |
| 52 | } |
| Philip Zeyliger | 16fa8b4 | 2025-05-02 04:28:16 +0000 | [diff] [blame] | 53 | .tool-pending { |
| 54 | color: #f0ad4e; |
| 55 | font-size: 14px; |
| 56 | } |
| Philip Zeyliger | 16fa8b4 | 2025-05-02 04:28:16 +0000 | [diff] [blame] | 57 | .summary-text { |
| 58 | white-space: nowrap; |
| 59 | text-overflow: ellipsis; |
| 60 | overflow: hidden; |
| 61 | flex-grow: 1; |
| 62 | flex-shrink: 1; |
| 63 | color: #444; |
| 64 | font-family: monospace; |
| 65 | font-size: 12px; |
| 66 | padding: 0 4px; |
| 67 | min-width: 50px; |
| Pokey Rule | 5e8aead | 2025-05-06 16:21:57 +0100 | [diff] [blame] | 68 | max-width: calc(100% - 250px); |
| 69 | display: inline-block; |
| Philip Zeyliger | 16fa8b4 | 2025-05-02 04:28:16 +0000 | [diff] [blame] | 70 | } |
| Philip Zeyliger | 16fa8b4 | 2025-05-02 04:28:16 +0000 | [diff] [blame] | 71 | .tool-status { |
| 72 | display: flex; |
| 73 | align-items: center; |
| 74 | gap: 12px; |
| 75 | margin-left: auto; |
| 76 | flex-shrink: 0; |
| Pokey Rule | 5e8aead | 2025-05-06 16:21:57 +0100 | [diff] [blame] | 77 | min-width: 120px; |
| Philip Zeyliger | 16fa8b4 | 2025-05-02 04:28:16 +0000 | [diff] [blame] | 78 | justify-content: flex-end; |
| 79 | padding-right: 8px; |
| 80 | } |
| Sean McCullough | ec3ad1a | 2025-04-18 13:55:16 -0700 | [diff] [blame] | 81 | .tool-call-status { |
| Philip Zeyliger | 16fa8b4 | 2025-05-02 04:28:16 +0000 | [diff] [blame] | 82 | display: flex; |
| 83 | align-items: center; |
| 84 | justify-content: center; |
| Sean McCullough | ec3ad1a | 2025-04-18 13:55:16 -0700 | [diff] [blame] | 85 | } |
| Sean McCullough | ec3ad1a | 2025-04-18 13:55:16 -0700 | [diff] [blame] | 86 | .tool-call-status.spinner { |
| 87 | animation: spin 1s infinite linear; |
| Sean McCullough | ec3ad1a | 2025-04-18 13:55:16 -0700 | [diff] [blame] | 88 | } |
| Sean McCullough | ec3ad1a | 2025-04-18 13:55:16 -0700 | [diff] [blame] | 89 | @keyframes spin { |
| 90 | 0% { |
| 91 | transform: rotate(0deg); |
| 92 | } |
| 93 | 100% { |
| 94 | transform: rotate(360deg); |
| 95 | } |
| 96 | } |
| Philip Zeyliger | 16fa8b4 | 2025-05-02 04:28:16 +0000 | [diff] [blame] | 97 | .elapsed { |
| 98 | font-size: 11px; |
| 99 | color: #777; |
| 100 | white-space: nowrap; |
| 101 | min-width: 40px; |
| 102 | text-align: right; |
| 103 | } |
| Philip Zeyliger | 16fa8b4 | 2025-05-02 04:28:16 +0000 | [diff] [blame] | 104 | .tool-details { |
| 105 | padding: 8px; |
| 106 | background-color: rgba(0, 0, 0, 0.02); |
| 107 | margin-top: 1px; |
| 108 | border-top: 1px solid rgba(0, 0, 0, 0.05); |
| 109 | display: none; |
| 110 | font-family: monospace; |
| 111 | font-size: 12px; |
| 112 | color: #333; |
| 113 | border-radius: 0 0 4px 4px; |
| 114 | max-width: 100%; |
| 115 | width: 100%; |
| 116 | box-sizing: border-box; |
| Pokey Rule | 5e8aead | 2025-05-06 16:21:57 +0100 | [diff] [blame] | 117 | overflow: hidden; |
| Philip Zeyliger | 16fa8b4 | 2025-05-02 04:28:16 +0000 | [diff] [blame] | 118 | } |
| Philip Zeyliger | 16fa8b4 | 2025-05-02 04:28:16 +0000 | [diff] [blame] | 119 | .tool-details.visible { |
| 120 | display: block; |
| 121 | } |
| Sean McCullough | ec3ad1a | 2025-04-18 13:55:16 -0700 | [diff] [blame] | 122 | .cancel-button { |
| Sean McCullough | ec3ad1a | 2025-04-18 13:55:16 -0700 | [diff] [blame] | 123 | cursor: pointer; |
| Philip Zeyliger | 16fa8b4 | 2025-05-02 04:28:16 +0000 | [diff] [blame] | 124 | color: white; |
| 125 | background-color: #d9534f; |
| 126 | border: none; |
| 127 | border-radius: 3px; |
| 128 | font-size: 11px; |
| 129 | padding: 2px 6px; |
| 130 | white-space: nowrap; |
| 131 | min-width: 50px; |
| Sean McCullough | ec3ad1a | 2025-04-18 13:55:16 -0700 | [diff] [blame] | 132 | } |
| Sean McCullough | ec3ad1a | 2025-04-18 13:55:16 -0700 | [diff] [blame] | 133 | .cancel-button:hover { |
| Philip Zeyliger | 16fa8b4 | 2025-05-02 04:28:16 +0000 | [diff] [blame] | 134 | background-color: #c9302c; |
| Sean McCullough | ec3ad1a | 2025-04-18 13:55:16 -0700 | [diff] [blame] | 135 | } |
| Philip Zeyliger | 16fa8b4 | 2025-05-02 04:28:16 +0000 | [diff] [blame] | 136 | .cancel-button[disabled] { |
| 137 | background-color: #999; |
| 138 | cursor: not-allowed; |
| Sean McCullough | ec3ad1a | 2025-04-18 13:55:16 -0700 | [diff] [blame] | 139 | } |
| Sean McCullough | ec3ad1a | 2025-04-18 13:55:16 -0700 | [diff] [blame] | 140 | `; |
| 141 | |
| Sean McCullough | ec3ad1a | 2025-04-18 13:55:16 -0700 | [diff] [blame] | 142 | _cancelToolCall = async (tool_call_id: string, button: HTMLButtonElement) => { |
| Sean McCullough | ec3ad1a | 2025-04-18 13:55:16 -0700 | [diff] [blame] | 143 | button.innerText = "Cancelling"; |
| 144 | button.disabled = true; |
| 145 | try { |
| 146 | const response = await fetch("cancel", { |
| 147 | method: "POST", |
| Pokey Rule | 5e8aead | 2025-05-06 16:21:57 +0100 | [diff] [blame] | 148 | headers: { "Content-Type": "application/json" }, |
| Sean McCullough | ec3ad1a | 2025-04-18 13:55:16 -0700 | [diff] [blame] | 149 | body: JSON.stringify({ |
| 150 | tool_call_id: tool_call_id, |
| 151 | reason: "user requested cancellation", |
| 152 | }), |
| 153 | }); |
| 154 | if (response.ok) { |
| Sean McCullough | ec3ad1a | 2025-04-18 13:55:16 -0700 | [diff] [blame] | 155 | button.parentElement.removeChild(button); |
| 156 | } else { |
| 157 | button.innerText = "Cancel"; |
| Sean McCullough | ec3ad1a | 2025-04-18 13:55:16 -0700 | [diff] [blame] | 158 | } |
| 159 | } catch (e) { |
| 160 | console.error("cancel", tool_call_id, e); |
| 161 | } |
| 162 | }; |
| 163 | |
| Philip Zeyliger | 16fa8b4 | 2025-05-02 04:28:16 +0000 | [diff] [blame] | 164 | _toggleDetails(e: Event) { |
| 165 | e.stopPropagation(); |
| 166 | this.detailsVisible = !this.detailsVisible; |
| 167 | } |
| Sean McCullough | ec3ad1a | 2025-04-18 13:55:16 -0700 | [diff] [blame] | 168 | |
| Philip Zeyliger | 16fa8b4 | 2025-05-02 04:28:16 +0000 | [diff] [blame] | 169 | render() { |
| Pokey Rule | 5e8aead | 2025-05-06 16:21:57 +0100 | [diff] [blame] | 170 | // Status indicator based on result |
| 171 | let statusIcon = html`<span class="tool-call-status spinner tool-pending" |
| 172 | >⏳</span |
| 173 | >`; |
| 174 | if (this.toolCall?.result_message) { |
| 175 | statusIcon = this.toolCall?.result_message.tool_error |
| 176 | ? html`<span class="tool-call-status tool-error">🔔</span>` |
| 177 | : html`<span class="tool-call-status tool-success">✓</span>`; |
| Philip Zeyliger | 16fa8b4 | 2025-05-02 04:28:16 +0000 | [diff] [blame] | 178 | } |
| 179 | |
| 180 | // Cancel button for pending operations |
| Sean McCullough | ec3ad1a | 2025-04-18 13:55:16 -0700 | [diff] [blame] | 181 | const cancelButton = this.toolCall?.result_message |
| 182 | ? "" |
| 183 | : html`<button |
| 184 | class="cancel-button" |
| 185 | title="Cancel this operation" |
| 186 | @click=${(e: Event) => { |
| 187 | e.stopPropagation(); |
| Pokey Rule | 5e8aead | 2025-05-06 16:21:57 +0100 | [diff] [blame] | 188 | this._cancelToolCall( |
| 189 | this.toolCall?.tool_call_id, |
| 190 | e.target as HTMLButtonElement, |
| 191 | ); |
| Sean McCullough | ec3ad1a | 2025-04-18 13:55:16 -0700 | [diff] [blame] | 192 | }} |
| 193 | > |
| 194 | Cancel |
| 195 | </button>`; |
| 196 | |
| Philip Zeyliger | 16fa8b4 | 2025-05-02 04:28:16 +0000 | [diff] [blame] | 197 | // Elapsed time display |
| 198 | const elapsed = this.toolCall?.result_message?.elapsed |
| Sean McCullough | 2deac84 | 2025-04-21 18:17:57 -0700 | [diff] [blame] | 199 | ? html`<span class="elapsed" |
| Philip Zeyliger | 16fa8b4 | 2025-05-02 04:28:16 +0000 | [diff] [blame] | 200 | >${(this.toolCall?.result_message?.elapsed / 1e9).toFixed(1)}s</span |
| Sean McCullough | 2deac84 | 2025-04-21 18:17:57 -0700 | [diff] [blame] | 201 | >` |
| Pokey Rule | 5e8aead | 2025-05-06 16:21:57 +0100 | [diff] [blame] | 202 | : html`<span class="elapsed"></span>`; |
| Sean McCullough | 2deac84 | 2025-04-21 18:17:57 -0700 | [diff] [blame] | 203 | |
| Philip Zeyliger | 16fa8b4 | 2025-05-02 04:28:16 +0000 | [diff] [blame] | 204 | // Initialize details visibility based on open property |
| 205 | if (this.open && !this.detailsVisible) { |
| 206 | this.detailsVisible = true; |
| 207 | } |
| 208 | |
| 209 | return html`<div class="tool-call"> |
| 210 | <div class="tool-row" @click=${this._toggleDetails}> |
| 211 | <span class="tool-name">${this.toolCall?.name}</span> |
| 212 | <span class="summary-text"><slot name="summary"></slot></span> |
| 213 | <div class="tool-status">${statusIcon} ${elapsed} ${cancelButton}</div> |
| 214 | </div> |
| 215 | <div class="tool-details ${this.detailsVisible ? "visible" : ""}"> |
| Sean McCullough | ec3ad1a | 2025-04-18 13:55:16 -0700 | [diff] [blame] | 216 | <slot name="input"></slot> |
| 217 | <slot name="result"></slot> |
| Philip Zeyliger | 16fa8b4 | 2025-05-02 04:28:16 +0000 | [diff] [blame] | 218 | </div> |
| 219 | </div>`; |
| Sean McCullough | ec3ad1a | 2025-04-18 13:55:16 -0700 | [diff] [blame] | 220 | } |
| 221 | } |
| 222 | |
| Sean McCullough | ec3ad1a | 2025-04-18 13:55:16 -0700 | [diff] [blame] | 223 | declare global { |
| 224 | interface HTMLElementTagNameMap { |
| 225 | "sketch-tool-card": SketchToolCard; |
| Sean McCullough | ec3ad1a | 2025-04-18 13:55:16 -0700 | [diff] [blame] | 226 | } |
| 227 | } |