nsarrazin HF Staff victor HF Staff Mishig commited on
Commit
2e21e16
·
unverified ·
1 Parent(s): 59077fd

Add a loading bar for tools (#1188)

Browse files

* Add a loading bar for tools

* move css to right file

* lighter purple

and no rounding on the loading bar

* Update src/routes/+layout.server.ts

Co-authored-by: Mishig <[email protected]>

* Clear animation when done (#1191)

* Clear animation when done

* make the bar go to the end

---------

Co-authored-by: Nathan Sarrazin <[email protected]>

---------

Co-authored-by: Victor Mustar <[email protected]>
Co-authored-by: Mishig <[email protected]>

src/lib/components/chat/ChatMessage.svelte CHANGED
@@ -16,30 +16,22 @@
16
  import CarbonPen from "~icons/carbon/pen";
17
  import CarbonChevronLeft from "~icons/carbon/chevron-left";
18
  import CarbonChevronRight from "~icons/carbon/chevron-right";
19
- import CarbonTools from "~icons/carbon/tools";
20
  import { PUBLIC_SEP_TOKEN } from "$lib/constants/publicSepToken";
21
  import type { Model } from "$lib/types/Model";
22
  import UploadedFile from "./UploadedFile.svelte";
23
 
24
  import OpenWebSearchResults from "../OpenWebSearchResults.svelte";
25
  import {
26
- MessageToolUpdateType,
27
  MessageWebSearchUpdateType,
28
  type MessageToolUpdate,
29
  type MessageWebSearchSourcesUpdate,
30
  type MessageWebSearchUpdate,
31
  } from "$lib/types/MessageUpdate";
32
- import {
33
- isMessageToolCallUpdate,
34
- isMessageToolResultUpdate,
35
- isMessageToolErrorUpdate,
36
- } from "$lib/utils/messageUpdates";
37
- import type { ToolFront } from "$lib/types/Tool";
38
  import { base } from "$app/paths";
39
  import { useConvTreeStore } from "$lib/stores/convTree";
40
  import { isReducedMotion } from "$lib/utils/isReduceMotion";
41
  import Modal from "../Modal.svelte";
42
- import { toolHasName } from "$lib/utils/tools";
43
 
44
  function sanitizeMd(md: string) {
45
  let ret = md
@@ -221,8 +213,6 @@
221
  $: if (message.children?.length === 0) $convTreeStore.leaf = message.id;
222
 
223
  $: modalImageToShow = null as MessageFile | null;
224
-
225
- const availableTools: ToolFront[] = $page.data.tools;
226
  </script>
227
 
228
  {#if modalImageToShow}
@@ -301,72 +291,7 @@
301
  {#if toolUpdates}
302
  {#each Object.values(toolUpdates) as tool}
303
  {#if tool.length}
304
- {@const toolName = tool.find(isMessageToolCallUpdate)?.call.name}
305
- {@const toolError = tool.some(isMessageToolErrorUpdate)}
306
- {@const toolDone = tool.some(isMessageToolResultUpdate)}
307
- {#if toolName && toolName !== "websearch"}
308
- <details
309
- 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
310
- 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"
311
- >
312
- <summary
313
- class="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"
314
- >
315
- <div
316
- class="relative grid size-[22px] place-items-center rounded bg-purple-600/10 dark:bg-purple-600/20"
317
- >
318
- <svg
319
- class="absolute inset-0 text-purple-500/40 transition-opacity"
320
- class:invisible={toolDone || toolError}
321
- width="22"
322
- height="22"
323
- viewBox="0 0 38 38"
324
- fill="none"
325
- xmlns="http://www.w3.org/2000/svg"
326
- >
327
- <path
328
- class="loading-path"
329
- 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"
330
- stroke="currentColor"
331
- stroke-width="1"
332
- stroke-linecap="round"
333
- id="shape"
334
- />
335
- </svg>
336
- <CarbonTools class="text-xs text-purple-700 dark:text-purple-500" />
337
- </div>
338
-
339
- <span>
340
- {toolError ? "Error calling" : toolDone ? "Called" : "Calling"} tool
341
- <span class="font-semibold"
342
- >{availableTools.find((el) => toolHasName(toolName, el))?.displayName}</span
343
- >
344
- </span>
345
- </summary>
346
- {#each tool as toolUpdate}
347
- {#if toolUpdate.subtype === MessageToolUpdateType.Call}
348
- <div class="mt-1 flex items-center gap-2 opacity-80">
349
- <h3 class="text-sm">Parameters</h3>
350
- <div class="h-px flex-1 bg-gradient-to-r from-gray-500/20" />
351
- </div>
352
- <ul class="py-1 text-sm">
353
- {#each Object.entries(toolUpdate.call.parameters ?? {}) as [k, v]}
354
- <li>
355
- <span class="font-semibold">{k}</span>:
356
- <span>{v}</span>
357
- </li>
358
- {/each}
359
- </ul>
360
- {:else if toolUpdate.subtype === MessageToolUpdateType.Error}
361
- <div class="mt-1 flex items-center gap-2 opacity-80">
362
- <h3 class="text-sm">Error</h3>
363
- <div class="h-px flex-1 bg-gradient-to-r from-gray-500/20" />
364
- </div>
365
- <p class="text-sm">{toolUpdate.message}</p>
366
- {/if}
367
- {/each}
368
- </details>
369
- {/if}
370
  {/if}
371
  {/each}
372
  {/if}
@@ -617,15 +542,6 @@
617
  {/if}
618
 
619
  <style>
620
- details summary::-webkit-details-marker {
621
- display: none;
622
- }
623
-
624
- .loading-path {
625
- stroke-dasharray: 61.45;
626
- animation: loading 2s linear infinite;
627
- }
628
-
629
  @keyframes loading {
630
  to {
631
  stroke-dashoffset: 122.9;
 
16
  import CarbonPen from "~icons/carbon/pen";
17
  import CarbonChevronLeft from "~icons/carbon/chevron-left";
18
  import CarbonChevronRight from "~icons/carbon/chevron-right";
 
19
  import { PUBLIC_SEP_TOKEN } from "$lib/constants/publicSepToken";
20
  import type { Model } from "$lib/types/Model";
21
  import UploadedFile from "./UploadedFile.svelte";
22
 
23
  import OpenWebSearchResults from "../OpenWebSearchResults.svelte";
24
  import {
 
25
  MessageWebSearchUpdateType,
26
  type MessageToolUpdate,
27
  type MessageWebSearchSourcesUpdate,
28
  type MessageWebSearchUpdate,
29
  } from "$lib/types/MessageUpdate";
 
 
 
 
 
 
30
  import { base } from "$app/paths";
31
  import { useConvTreeStore } from "$lib/stores/convTree";
32
  import { isReducedMotion } from "$lib/utils/isReduceMotion";
33
  import Modal from "../Modal.svelte";
34
+ import ToolUpdate from "./ToolUpdate.svelte";
35
 
36
  function sanitizeMd(md: string) {
37
  let ret = md
 
213
  $: if (message.children?.length === 0) $convTreeStore.leaf = message.id;
214
 
215
  $: modalImageToShow = null as MessageFile | null;
 
 
216
  </script>
217
 
218
  {#if modalImageToShow}
 
291
  {#if toolUpdates}
292
  {#each Object.values(toolUpdates) as tool}
293
  {#if tool.length}
294
+ <ToolUpdate {tool} {loading} />
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
295
  {/if}
296
  {/each}
297
  {/if}
 
542
  {/if}
543
 
544
  <style>
 
 
 
 
 
 
 
 
 
545
  @keyframes loading {
546
  to {
547
  stroke-dashoffset: 122.9;
src/lib/components/chat/ToolUpdate.svelte ADDED
@@ -0,0 +1,141 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <script lang="ts">
2
+ import { MessageToolUpdateType, type MessageToolUpdate } from "$lib/types/MessageUpdate";
3
+ import {
4
+ isMessageToolCallUpdate,
5
+ isMessageToolErrorUpdate,
6
+ isMessageToolResultUpdate,
7
+ } from "$lib/utils/messageUpdates";
8
+
9
+ import CarbonTools from "~icons/carbon/tools";
10
+ import { toolHasName } from "$lib/utils/tools";
11
+ import type { ToolFront } from "$lib/types/Tool";
12
+ import { page } from "$app/stores";
13
+ import { onMount } from "svelte";
14
+
15
+ export let tool: MessageToolUpdate[];
16
+ export let loading: boolean = false;
17
+
18
+ const toolName = tool.find(isMessageToolCallUpdate)?.call.name;
19
+ const toolError = tool.some(isMessageToolErrorUpdate);
20
+ $: toolDone = tool.some(isMessageToolResultUpdate);
21
+
22
+ const availableTools: ToolFront[] = $page.data.tools;
23
+
24
+ let loadingBarEl: HTMLDivElement;
25
+ let animation: Animation | undefined = undefined;
26
+
27
+ let isShowingLoadingBar = false;
28
+ onMount(() => {
29
+ if (!toolDone && loading) {
30
+ loadingBarEl.classList.remove("hidden");
31
+ isShowingLoadingBar = true;
32
+ animation = loadingBarEl.animate([{ width: "0%" }, { width: "calc(100%+1rem)" }], {
33
+ duration: availableTools.find((tool) => tool.name === toolName)?.timeToUseMS,
34
+ fill: "forwards",
35
+ });
36
+ }
37
+ return () => animation?.cancel();
38
+ });
39
+
40
+ // go to 100% quickly if loading is done
41
+ $: (!loading || toolDone) &&
42
+ isShowingLoadingBar &&
43
+ (() => {
44
+ isShowingLoadingBar = false;
45
+
46
+ loadingBarEl.classList.remove("hidden");
47
+
48
+ animation?.cancel();
49
+ animation = loadingBarEl.animate(
50
+ [{ width: loadingBarEl.style.width }, { width: "calc(100%+1rem)" }],
51
+ {
52
+ duration: 300,
53
+ fill: "forwards",
54
+ }
55
+ );
56
+
57
+ setTimeout(() => {
58
+ loadingBarEl.classList.add("hidden");
59
+ }, 300);
60
+ })();
61
+ </script>
62
+
63
+ {#if toolName && toolName !== "websearch"}
64
+ <details
65
+ 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
66
+ 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"
67
+ >
68
+ <summary
69
+ 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"
70
+ >
71
+ <div
72
+ bind:this={loadingBarEl}
73
+ class="absolute -m-1 hidden h-full w-[calc(100%+1rem)] rounded-lg bg-purple-500/5 transition-all dark:bg-purple-500/10"
74
+ />
75
+
76
+ <div
77
+ class="relative grid size-[22px] place-items-center rounded bg-purple-600/10 dark:bg-purple-600/20"
78
+ >
79
+ <svg
80
+ class="absolute inset-0 text-purple-500/40 transition-opacity"
81
+ class:invisible={toolDone || toolError}
82
+ width="22"
83
+ height="22"
84
+ viewBox="0 0 38 38"
85
+ fill="none"
86
+ xmlns="http://www.w3.org/2000/svg"
87
+ >
88
+ <path
89
+ class="loading-path"
90
+ 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"
91
+ stroke="currentColor"
92
+ stroke-width="1"
93
+ stroke-linecap="round"
94
+ id="shape"
95
+ />
96
+ </svg>
97
+ <CarbonTools class="text-xs text-purple-700 dark:text-purple-500" />
98
+ </div>
99
+
100
+ <span>
101
+ {toolError ? "Error calling" : toolDone ? "Called" : "Calling"} tool
102
+ <span class="font-semibold"
103
+ >{availableTools.find((el) => toolHasName(toolName, el))?.displayName}</span
104
+ >
105
+ </span>
106
+ </summary>
107
+ {#each tool as toolUpdate}
108
+ {#if toolUpdate.subtype === MessageToolUpdateType.Call}
109
+ <div class="mt-1 flex items-center gap-2 opacity-80">
110
+ <h3 class="text-sm">Parameters</h3>
111
+ <div class="h-px flex-1 bg-gradient-to-r from-gray-500/20" />
112
+ </div>
113
+ <ul class="py-1 text-sm">
114
+ {#each Object.entries(toolUpdate.call.parameters ?? {}) as [k, v]}
115
+ <li>
116
+ <span class="font-semibold">{k}</span>:
117
+ <span>{v}</span>
118
+ </li>
119
+ {/each}
120
+ </ul>
121
+ {:else if toolUpdate.subtype === MessageToolUpdateType.Error}
122
+ <div class="mt-1 flex items-center gap-2 opacity-80">
123
+ <h3 class="text-sm">Error</h3>
124
+ <div class="h-px flex-1 bg-gradient-to-r from-gray-500/20" />
125
+ </div>
126
+ <p class="text-sm">{toolUpdate.message}</p>
127
+ {/if}
128
+ {/each}
129
+ </details>
130
+ {/if}
131
+
132
+ <style>
133
+ details summary::-webkit-details-marker {
134
+ display: none;
135
+ }
136
+
137
+ .loading-path {
138
+ stroke-dasharray: 61.45;
139
+ animation: loading 2s linear infinite;
140
+ }
141
+ </style>
src/lib/types/Tool.ts CHANGED
@@ -25,7 +25,7 @@ export interface Tool {
25
  export type ToolFront = Pick<
26
  Tool,
27
  "name" | "displayName" | "description" | "isOnByDefault" | "isLocked"
28
- >;
29
 
30
  export enum ToolResultStatus {
31
  Success = "success",
 
25
  export type ToolFront = Pick<
26
  Tool,
27
  "name" | "displayName" | "description" | "isOnByDefault" | "isLocked"
28
+ > & { timeToUseMS?: number };
29
 
30
  export enum ToolResultStatus {
31
  Success = "success",
src/routes/+layout.server.ts CHANGED
@@ -9,6 +9,7 @@ import { env } from "$env/dynamic/private";
9
  import { ObjectId } from "mongodb";
10
  import type { ConvSidebar } from "$lib/types/ConvSidebar";
11
  import { allTools } from "$lib/server/tools";
 
12
 
13
  export const load: LayoutServerLoad = async ({ locals, depends }) => {
14
  depends(UrlDependency.ConversationList);
@@ -105,6 +106,7 @@ export const load: LayoutServerLoad = async ({ locals, depends }) => {
105
  }
106
  }
107
 
 
108
  return {
109
  conversations: conversations.map((conv) => {
110
  if (settings?.hideEmojiOnSidebar) {
@@ -172,6 +174,9 @@ export const load: LayoutServerLoad = async ({ locals, depends }) => {
172
  description: tool.description,
173
  isOnByDefault: tool.isOnByDefault,
174
  isLocked: tool.isLocked,
 
 
 
175
  })),
176
  assistants: assistants
177
  .filter((el) => userAssistantsSet.has(el._id.toString()))
 
9
  import { ObjectId } from "mongodb";
10
  import type { ConvSidebar } from "$lib/types/ConvSidebar";
11
  import { allTools } from "$lib/server/tools";
12
+ import { MetricsServer } from "$lib/server/metrics";
13
 
14
  export const load: LayoutServerLoad = async ({ locals, depends }) => {
15
  depends(UrlDependency.ConversationList);
 
106
  }
107
  }
108
 
109
+ const toolUseDuration = (await MetricsServer.getMetrics().tool.toolUseDuration.get()).values;
110
  return {
111
  conversations: conversations.map((conv) => {
112
  if (settings?.hideEmojiOnSidebar) {
 
174
  description: tool.description,
175
  isOnByDefault: tool.isOnByDefault,
176
  isLocked: tool.isLocked,
177
+ timeToUseMS:
178
+ toolUseDuration.find((el) => el.labels.tool === tool.name && el.labels.quantile === 0.9)
179
+ ?.value ?? 15_000,
180
  })),
181
  assistants: assistants
182
  .filter((el) => userAssistantsSet.has(el._id.toString()))