|
11 | 11 | Gauge, |
12 | 12 | Clock, |
13 | 13 | WholeWord, |
14 | | - ChartNoAxesColumn |
| 14 | + ChartNoAxesColumn, |
| 15 | + Wrench |
15 | 16 | } from '@lucide/svelte'; |
16 | 17 | import { Button } from '$lib/components/ui/button'; |
17 | 18 | import { Checkbox } from '$lib/components/ui/checkbox'; |
|
21 | 22 | import { config } from '$lib/stores/settings.svelte'; |
22 | 23 | import { modelName as serverModelName } from '$lib/stores/server.svelte'; |
23 | 24 | import { copyToClipboard } from '$lib/utils/copy'; |
| 25 | + import type { ApiChatCompletionToolCall } from '$lib/types/api'; |
24 | 26 |
|
25 | 27 | interface Props { |
26 | 28 | class?: string; |
|
51 | 53 | siblingInfo?: ChatMessageSiblingInfo | null; |
52 | 54 | textareaElement?: HTMLTextAreaElement; |
53 | 55 | thinkingContent: string | null; |
| 56 | + toolCallContent: ApiChatCompletionToolCall[] | string | null; |
54 | 57 | } |
55 | 58 |
|
56 | 59 | let { |
|
76 | 79 | shouldBranchAfterEdit = false, |
77 | 80 | siblingInfo = null, |
78 | 81 | textareaElement = $bindable(), |
79 | | - thinkingContent |
| 82 | + thinkingContent, |
| 83 | + toolCallContent = null |
80 | 84 | }: Props = $props(); |
81 | 85 |
|
| 86 | + const toolCalls = $derived( |
| 87 | + Array.isArray(toolCallContent) ? (toolCallContent as ApiChatCompletionToolCall[]) : null |
| 88 | + ); |
| 89 | + const fallbackToolCalls = $derived(typeof toolCallContent === 'string' ? toolCallContent : null); |
| 90 | +
|
82 | 91 | const processingState = useProcessingState(); |
83 | 92 | let currentConfig = $derived(config()); |
84 | 93 | let serverModel = $derived(serverModelName()); |
|
97 | 106 |
|
98 | 107 | void copyToClipboard(model ?? ''); |
99 | 108 | } |
| 109 | +
|
| 110 | + function formatToolCallBadge(toolCall: ApiChatCompletionToolCall, index: number) { |
| 111 | + const callNumber = index + 1; |
| 112 | + const functionName = toolCall.function?.name?.trim(); |
| 113 | + const label = functionName || `Call #${callNumber}`; |
| 114 | +
|
| 115 | + const payload: Record<string, unknown> = {}; |
| 116 | +
|
| 117 | + const id = toolCall.id?.trim(); |
| 118 | + if (id) { |
| 119 | + payload.id = id; |
| 120 | + } |
| 121 | +
|
| 122 | + const type = toolCall.type?.trim(); |
| 123 | + if (type) { |
| 124 | + payload.type = type; |
| 125 | + } |
| 126 | +
|
| 127 | + if (toolCall.function) { |
| 128 | + const fnPayload: Record<string, unknown> = {}; |
| 129 | +
|
| 130 | + const name = toolCall.function.name?.trim(); |
| 131 | + if (name) { |
| 132 | + fnPayload.name = name; |
| 133 | + } |
| 134 | +
|
| 135 | + const rawArguments = toolCall.function.arguments?.trim(); |
| 136 | + if (rawArguments) { |
| 137 | + try { |
| 138 | + fnPayload.arguments = JSON.parse(rawArguments); |
| 139 | + } catch { |
| 140 | + fnPayload.arguments = rawArguments; |
| 141 | + } |
| 142 | + } |
| 143 | +
|
| 144 | + if (Object.keys(fnPayload).length > 0) { |
| 145 | + payload.function = fnPayload; |
| 146 | + } |
| 147 | + } |
| 148 | +
|
| 149 | + const formattedPayload = JSON.stringify(payload, null, 2); |
| 150 | +
|
| 151 | + return { |
| 152 | + label, |
| 153 | + tooltip: formattedPayload, |
| 154 | + copyValue: formattedPayload |
| 155 | + }; |
| 156 | + } |
| 157 | +
|
| 158 | + function handleCopyToolCall(payload: string) { |
| 159 | + void copyToClipboard(payload, 'Tool call copied to clipboard'); |
| 160 | + } |
100 | 161 | </script> |
101 | 162 |
|
102 | 163 | <div |
|
189 | 250 | </span> |
190 | 251 | {/if} |
191 | 252 |
|
| 253 | + {#if config().showToolCalls} |
| 254 | + {#if (toolCalls && toolCalls.length > 0) || fallbackToolCalls} |
| 255 | + <span class="inline-flex flex-wrap items-center gap-2 text-xs text-muted-foreground"> |
| 256 | + <span class="inline-flex items-center gap-1"> |
| 257 | + <Wrench class="h-3.5 w-3.5" /> |
| 258 | + |
| 259 | + <span>Tool calls:</span> |
| 260 | + </span> |
| 261 | + |
| 262 | + {#if toolCalls && toolCalls.length > 0} |
| 263 | + {#each toolCalls as toolCall, index (toolCall.id ?? `${index}`)} |
| 264 | + {@const badge = formatToolCallBadge(toolCall, index)} |
| 265 | + <button |
| 266 | + type="button" |
| 267 | + class="tool-call-badge inline-flex cursor-pointer items-center gap-1 rounded-sm bg-muted-foreground/15 px-1.5 py-0.75" |
| 268 | + title={badge.tooltip} |
| 269 | + aria-label={`Copy tool call ${badge.label}`} |
| 270 | + onclick={() => handleCopyToolCall(badge.copyValue)} |
| 271 | + > |
| 272 | + {badge.label} |
| 273 | + |
| 274 | + <Copy class="ml-1 h-3 w-3" /> |
| 275 | + </button> |
| 276 | + {/each} |
| 277 | + {:else if fallbackToolCalls} |
| 278 | + <button |
| 279 | + type="button" |
| 280 | + class="tool-call-badge tool-call-badge--fallback inline-flex cursor-pointer items-center gap-1 rounded-sm bg-muted-foreground/15 px-1.5 py-0.75" |
| 281 | + title={fallbackToolCalls} |
| 282 | + aria-label="Copy tool call payload" |
| 283 | + onclick={() => handleCopyToolCall(fallbackToolCalls)} |
| 284 | + > |
| 285 | + {fallbackToolCalls} |
| 286 | + |
| 287 | + <Copy class="ml-1 h-3 w-3" /> |
| 288 | + </button> |
| 289 | + {/if} |
| 290 | + </span> |
| 291 | + {/if} |
| 292 | + {/if} |
| 293 | + |
192 | 294 | {#if currentConfig.showMessageStats && message.timings && message.timings.predicted_n && message.timings.predicted_ms} |
193 | 295 | {@const tokensPerSecond = (message.timings.predicted_n / message.timings.predicted_ms) * 1000} |
194 | 296 | <span class="inline-flex items-center gap-2 text-xs text-muted-foreground"> |
|
287 | 389 | white-space: pre-wrap; |
288 | 390 | word-break: break-word; |
289 | 391 | } |
| 392 | +
|
| 393 | + .tool-call-badge { |
| 394 | + max-width: 12rem; |
| 395 | + white-space: nowrap; |
| 396 | + overflow: hidden; |
| 397 | + text-overflow: ellipsis; |
| 398 | + } |
| 399 | +
|
| 400 | + .tool-call-badge--fallback { |
| 401 | + max-width: 20rem; |
| 402 | + white-space: normal; |
| 403 | + word-break: break-word; |
| 404 | + } |
290 | 405 | </style> |
0 commit comments