File size: 5,384 Bytes
2e21e16
 
 
 
 
 
 
 
 
09ed06c
d433b55
15baec4
f940e40
2e21e16
a1a6daf
 
 
 
 
 
2e21e16
0c3e3b2
a1a6daf
 
2e21e16
a1a6daf
15baec4
d433b55
2e21e16
a1a6daf
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
15baec4
 
 
 
2e21e16
 
 
 
a1a6daf
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2e21e16
 
0c3e3b2
2e21e16
 
 
 
 
 
 
 
 
 
a1a6daf
2e21e16
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
0c3e3b2
 
2e21e16
 
 
 
 
 
 
a1a6daf
2e21e16
 
 
0c3e3b2
 
 
 
 
 
2e21e16
 
 
 
 
a1a6daf
2e21e16
 
09ed06c
 
 
a1a6daf
09ed06c
 
 
 
 
 
 
 
 
 
 
 
 
2e21e16
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
<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>