Spaces:
Paused
Paused
| <script lang="ts"> | |
| import { MessageToolUpdateType, type MessageToolUpdate } from "$lib/types/MessageUpdate"; | |
| import { | |
| isMessageToolCallUpdate, | |
| isMessageToolErrorUpdate, | |
| isMessageToolResultUpdate, | |
| } from "$lib/utils/messageUpdates"; | |
| import CarbonTools from "~icons/carbon/tools"; | |
| import { ToolResultStatus, type ToolFront } from "$lib/types/Tool"; | |
| import { page } from "$app/state"; | |
| import { onDestroy } from "svelte"; | |
| import { browser } from "$app/environment"; | |
| interface Props { | |
| tool: MessageToolUpdate[]; | |
| loading?: boolean; | |
| } | |
| let { tool, loading = false }: Props = $props(); | |
| const toolFnName = tool.find(isMessageToolCallUpdate)?.call.name; | |
| let toolError = $derived(tool.some(isMessageToolErrorUpdate)); | |
| let toolDone = $derived(tool.some(isMessageToolResultUpdate)); | |
| let eta = $derived(tool.find((el) => el.subtype === MessageToolUpdateType.ETA)?.eta); | |
| const availableTools: ToolFront[] = page.data.tools; | |
| let loadingBarEl: HTMLDivElement | undefined = $state(); | |
| let animation: Animation | undefined = $state(undefined); | |
| let isShowingLoadingBar = $state(false); | |
| $effect(() => { | |
| !toolError && | |
| !toolDone && | |
| loading && | |
| loadingBarEl && | |
| eta && | |
| (() => { | |
| loadingBarEl.classList.remove("hidden"); | |
| isShowingLoadingBar = true; | |
| animation = loadingBarEl.animate([{ width: "0%" }, { width: "calc(100%+1rem)" }], { | |
| duration: eta * 1000, | |
| fill: "forwards", | |
| }); | |
| })(); | |
| }); | |
| onDestroy(() => { | |
| if (animation) { | |
| animation.cancel(); | |
| } | |
| }); | |
| // go to 100% quickly if loading is done | |
| $effect(() => { | |
| (!loading || toolDone || toolError) && | |
| browser && | |
| loadingBarEl && | |
| isShowingLoadingBar && | |
| (() => { | |
| isShowingLoadingBar = false; | |
| loadingBarEl.classList.remove("hidden"); | |
| animation?.cancel(); | |
| animation = loadingBarEl.animate( | |
| [{ width: loadingBarEl.style.width }, { width: "calc(100%+1rem)" }], | |
| { | |
| duration: 300, | |
| fill: "forwards", | |
| } | |
| ); | |
| setTimeout(() => { | |
| loadingBarEl?.classList.add("hidden"); | |
| }, 300); | |
| })(); | |
| }); | |
| </script> | |
| {#if toolFnName && toolFnName !== "websearch"} | |
| <details | |
| class="group/tool my-2.5 w-fit cursor-pointer rounded-lg border border-gray-200 bg-white pl-1 pr-2.5 text-sm shadow-sm transition-all open:mb-3 | |
| open:border-purple-500/10 open:bg-purple-600/5 open:shadow-sm dark:border-gray-800 dark:bg-gray-900 open:dark:border-purple-800/40 open:dark:bg-purple-800/10" | |
| > | |
| <summary | |
| class="relative flex select-none list-none items-center gap-1.5 py-1 group-open/tool:text-purple-700 group-open/tool:dark:text-purple-300" | |
| > | |
| <div | |
| bind:this={loadingBarEl} | |
| class="absolute -m-1 hidden h-full w-[calc(100%+1rem)] rounded-lg bg-purple-500/5 transition-all dark:bg-purple-500/10" | |
| ></div> | |
| <div | |
| class="relative grid size-[22px] place-items-center rounded bg-purple-600/10 dark:bg-purple-600/20" | |
| > | |
| <svg | |
| class="absolute inset-0 text-purple-500/40 transition-opacity" | |
| class:invisible={toolDone || toolError} | |
| width="22" | |
| height="22" | |
| viewBox="0 0 38 38" | |
| fill="none" | |
| xmlns="http://www.w3.org/2000/svg" | |
| > | |
| <path | |
| class="loading-path" | |
| d="M8 2.5H30C30 2.5 35.5 2.5 35.5 8V30C35.5 30 35.5 35.5 30 35.5H8C8 35.5 2.5 35.5 2.5 30V8C2.5 8 2.5 2.5 8 2.5Z" | |
| stroke="currentColor" | |
| stroke-width="1" | |
| stroke-linecap="round" | |
| id="shape" | |
| /> | |
| </svg> | |
| <CarbonTools class="text-xs text-purple-700 dark:text-purple-500" /> | |
| </div> | |
| <span> | |
| {toolError ? "Error calling" : toolDone ? "Called" : "Calling"} tool | |
| <span class="font-semibold" | |
| >{availableTools.find((tool) => tool.name === toolFnName)?.displayName ?? | |
| toolFnName}</span | |
| > | |
| </span> | |
| </summary> | |
| {#each tool as toolUpdate} | |
| {#if toolUpdate.subtype === MessageToolUpdateType.Call} | |
| <div class="mt-1 flex items-center gap-2 opacity-80"> | |
| <h3 class="text-sm">Parameters</h3> | |
| <div class="h-px flex-1 bg-gradient-to-r from-gray-500/20"></div> | |
| </div> | |
| <ul class="py-1 text-sm"> | |
| {#each Object.entries(toolUpdate.call.parameters ?? {}) as [k, v]} | |
| {#if v !== null} | |
| <li> | |
| <span class="font-semibold">{k}</span>: | |
| <span>{v}</span> | |
| </li> | |
| {/if} | |
| {/each} | |
| </ul> | |
| {:else if toolUpdate.subtype === MessageToolUpdateType.Error} | |
| <div class="mt-1 flex items-center gap-2 opacity-80"> | |
| <h3 class="text-sm">Error</h3> | |
| <div class="h-px flex-1 bg-gradient-to-r from-gray-500/20"></div> | |
| </div> | |
| <p class="text-sm">{toolUpdate.message}</p> | |
| {:else if isMessageToolResultUpdate(toolUpdate) && toolUpdate.result.status === ToolResultStatus.Success && toolUpdate.result.display} | |
| <div class="mt-1 flex items-center gap-2 opacity-80"> | |
| <h3 class="text-sm">Result</h3> | |
| <div class="h-px flex-1 bg-gradient-to-r from-gray-500/20"></div> | |
| </div> | |
| <ul class="py-1 text-sm"> | |
| {#each toolUpdate.result.outputs as output} | |
| {#each Object.entries(output) as [k, v]} | |
| {#if v !== null} | |
| <li> | |
| <span class="font-semibold">{k}</span>: | |
| <span>{v}</span> | |
| </li> | |
| {/if} | |
| {/each} | |
| {/each} | |
| </ul> | |
| {/if} | |
| {/each} | |
| </details> | |
| {/if} | |
| <style> | |
| details summary::-webkit-details-marker { | |
| display: none; | |
| } | |
| .loading-path { | |
| stroke-dasharray: 61.45; | |
| animation: loading 2s linear infinite; | |
| } | |
| </style> | |