mishig HF staff commited on
Commit
226d43f
·
unverified ·
2 Parent(s): c5c965b 859f79d

[Feature] support models comparison (#48)

Browse files
src/lib/components/Avatar.svelte ADDED
@@ -0,0 +1,26 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <script lang="ts">
2
+ export let orgName: string;
3
+ export let size: "sm" | "md" = "md";
4
+
5
+ const sizeClass = size === "sm" ? "size-3" : "size-4";
6
+
7
+ async function getAvatarUrl(orgName: string) {
8
+ const url = `https://huggingface.co/api/organizations/${orgName}/avatar`;
9
+ const res = await fetch(url);
10
+ if (!res.ok) {
11
+ console.error(`Error getting avatar url for org: ${orgName}`, res.status, res.statusText);
12
+ return;
13
+ }
14
+ const json = await res.json();
15
+ const { avatarUrl } = json;
16
+ return avatarUrl;
17
+ }
18
+ </script>
19
+
20
+ {#await getAvatarUrl(orgName)}
21
+ <div class="{sizeClass} flex-none rounded bg-gray-200"></div>
22
+ {:then avatarUrl}
23
+ <img class="{sizeClass} flex-none rounded bg-gray-200 object-cover" src={avatarUrl} alt="{orgName} avatar" />
24
+ {:catch}
25
+ <div class="{sizeClass} flex-none rounded bg-gray-200"></div>
26
+ {/await}
src/lib/components/Icons/IconCog.svelte ADDED
@@ -0,0 +1,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <script lang="ts">
2
+ export let classNames = "";
3
+ </script>
4
+
5
+ <svg class={classNames} xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 32 32"
6
+ ><path
7
+ fill="currentColor"
8
+ d="M27 16.76v-1.53l1.92-1.68A2 2 0 0 0 29.3 11l-2.36-4a2 2 0 0 0-1.73-1a2 2 0 0 0-.64.1l-2.43.82a11.35 11.35 0 0 0-1.31-.75l-.51-2.52a2 2 0 0 0-2-1.61h-4.68a2 2 0 0 0-2 1.61l-.51 2.52a11.48 11.48 0 0 0-1.32.75l-2.38-.86A2 2 0 0 0 6.79 6a2 2 0 0 0-1.73 1L2.7 11a2 2 0 0 0 .41 2.51L5 15.24v1.53l-1.89 1.68A2 2 0 0 0 2.7 21l2.36 4a2 2 0 0 0 1.73 1a2 2 0 0 0 .64-.1l2.43-.82a11.35 11.35 0 0 0 1.31.75l.51 2.52a2 2 0 0 0 2 1.61h4.72a2 2 0 0 0 2-1.61l.51-2.52a11.48 11.48 0 0 0 1.32-.75l2.42.82a2 2 0 0 0 .64.1a2 2 0 0 0 1.73-1l2.28-4a2 2 0 0 0-.41-2.51ZM25.21 24l-3.43-1.16a8.86 8.86 0 0 1-2.71 1.57L18.36 28h-4.72l-.71-3.55a9.36 9.36 0 0 1-2.7-1.57L6.79 24l-2.36-4l2.72-2.4a8.9 8.9 0 0 1 0-3.13L4.43 12l2.36-4l3.43 1.16a8.86 8.86 0 0 1 2.71-1.57L13.64 4h4.72l.71 3.55a9.36 9.36 0 0 1 2.7 1.57L25.21 8l2.36 4l-2.72 2.4a8.9 8.9 0 0 1 0 3.13L27.57 20Z"
9
+ /><path
10
+ fill="currentColor"
11
+ d="M16 22a6 6 0 1 1 6-6a5.94 5.94 0 0 1-6 6Zm0-10a3.91 3.91 0 0 0-4 4a3.91 3.91 0 0 0 4 4a3.91 3.91 0 0 0 4-4a3.91 3.91 0 0 0-4-4Z"
12
+ /></svg
13
+ >
src/lib/components/Icons/IconCompare.svelte ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ <script lang="ts">
2
+ export let classNames = "";
3
+ </script>
4
+
5
+ <svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 32 32" class={classNames}
6
+ ><path
7
+ fill="currentColor"
8
+ d="M28 6H18V4a2 2 0 0 0-2-2H4a2 2 0 0 0-2 2v20a2 2 0 0 0 2 2h10v2a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8a2 2 0 0 0-2-2ZM4 15h6.17l-2.58 2.59L9 19l5-5l-5-5l-1.41 1.41L10.17 13H4V4h12v20H4Zm12 13v-2a2 2 0 0 0 2-2V8h10v9h-6.17l2.58-2.59L23 13l-5 5l5 5l1.41-1.41L21.83 19H28v9Z"
9
+ /></svg
10
+ >
src/lib/components/Icons/IconThrashcan.svelte ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <script lang="ts">
2
+ export let classNames = "";
3
+ </script>
4
+
5
+ <svg
6
+ class={classNames}
7
+ style=""
8
+ xmlns="http://www.w3.org/2000/svg"
9
+ xmlns:xlink="http://www.w3.org/1999/xlink"
10
+ aria-hidden="true"
11
+ focusable="false"
12
+ role="img"
13
+ width="1em"
14
+ height="1em"
15
+ preserveAspectRatio="xMidYMid meet"
16
+ viewBox="0 0 24 24"
17
+ ><path
18
+ fill="currentColor"
19
+ d="M2.131 13.63a10 10 0 0 1 .001-3.26c1.101.026 2.092-.502 2.477-1.431c.385-.93.058-2.003-.74-2.763a10 10 0 0 1 2.306-2.307c.76.798 1.834 1.125 2.763.74c.93-.385 1.458-1.376 1.431-2.477a10 10 0 0 1 3.261 0c-.026 1.102.502 2.092 1.431 2.477c.93.385 2.003.058 2.763-.74a10 10 0 0 1 2.307 2.306c-.798.76-1.125 1.834-.74 2.764s1.376 1.458 2.477 1.43a10 10 0 0 1 0 3.262c-1.102-.027-2.092.501-2.477 1.43c-.385.93-.058 2.004.74 2.764a10 10 0 0 1-2.306 2.306c-.76-.798-1.834-1.125-2.764-.74s-1.458 1.376-1.43 2.478a10 10 0 0 1-3.262-.001c.027-1.101-.502-2.092-1.43-2.477c-.93-.385-2.004-.058-2.764.74a10 10 0 0 1-2.306-2.306c.798-.76 1.125-1.834.74-2.763c-.385-.93-1.376-1.458-2.478-1.431M12 15a3 3 0 1 0 0-6a3 3 0 0 0 0 6"
20
+ /></svg
21
+ >
src/lib/components/InferencePlayground/InferencePlayground.svelte CHANGED
@@ -1,5 +1,5 @@
1
  <script lang="ts">
2
- import type { Conversation, ModelEntryWithTokenizer } from "./types";
3
  import type { ChatCompletionInputMessage } from "@huggingface/tasks";
4
 
5
  import { page } from "$app/stores";
@@ -17,42 +17,74 @@
17
  import HFTokenModal from "./InferencePlaygroundHFTokenModal.svelte";
18
  import ModelSelector from "./InferencePlaygroundModelSelector.svelte";
19
  import PlaygroundConversation from "./InferencePlaygroundConversation.svelte";
 
20
  import IconDelete from "../Icons/IconDelete.svelte";
21
  import IconCode from "../Icons/IconCode.svelte";
22
  import IconInfo from "../Icons/IconInfo.svelte";
 
 
 
 
23
 
24
  export let models: ModelEntryWithTokenizer[];
25
 
26
  const startMessageUser: ChatCompletionInputMessage = { role: "user", content: "" };
27
  const startMessageSystem: ChatCompletionInputMessage = { role: "system", content: "" };
28
 
29
- const modelIdFromQueryParam = $page.url.searchParams.get("modelId");
30
- const modelFromQueryParam = models.find(model => model.id === modelIdFromQueryParam);
31
 
32
- let conversation: Conversation = {
33
- model: modelFromQueryParam ?? models.find(m => FEATURED_MODELS_IDS.includes(m.id)) ?? models[0],
34
- config: defaultGenerationConfig,
35
- messages: [{ ...startMessageUser }],
36
- systemMessage: startMessageSystem,
37
- streaming: true,
 
 
 
 
38
  };
39
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
40
  let hfToken = "";
41
  let viewCode = false;
42
  let viewSettings = false;
43
  let showTokenModal = false;
44
  let loading = false;
45
- let latency = 0;
46
- let generatedTokensCount = 0;
47
- let abortController: AbortController | undefined = undefined;
48
  let waitForNonStreaming = true;
49
  let storeLocallyHfToken = true;
 
 
 
 
 
 
 
 
 
50
 
51
  const hfTokenLocalStorageKey = "hf_token";
52
 
53
- $: systemPromptSupported = isSystemPromptSupported(conversation.model);
 
54
 
55
- function addMessage() {
 
56
  conversation.messages = [
57
  ...conversation.messages,
58
  {
@@ -60,21 +92,29 @@
60
  content: "",
61
  },
62
  ];
 
63
  }
64
 
65
- function deleteMessage(idx: number) {
66
- conversation.messages.splice(idx, 1)[0];
67
- conversation = conversation;
68
  }
69
 
70
  function reset() {
71
- conversation.systemMessage.content = "";
72
- conversation.messages = [{ ...startMessageUser }];
 
 
 
73
  }
74
 
75
  function abort() {
76
- abortController?.abort();
77
- abortController = undefined;
 
 
 
 
78
  loading = false;
79
  waitForNonStreaming = false;
80
  }
@@ -85,59 +125,74 @@
85
  showTokenModal = true;
86
  }
87
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
88
  async function submit() {
89
  if (!hfToken) {
90
  showTokenModal = true;
91
  return;
92
  }
93
 
94
- if (conversation.messages.at(-1)?.role === "assistant") {
95
- return alert("Messages must alternate between user/assistant roles.");
 
 
 
 
 
 
96
  }
97
 
98
  (document.activeElement as HTMLElement).blur();
99
  loading = true;
100
 
101
  try {
102
- const startTime = performance.now();
103
- const hf = createHfInference(hfToken);
104
-
105
- if (conversation.streaming) {
106
- const streamingMessage = { role: "assistant", content: "" };
107
- conversation.messages = [...conversation.messages, streamingMessage];
108
- abortController = new AbortController();
109
-
110
- await handleStreamingResponse(
111
- hf,
112
- conversation,
113
- content => {
114
- if (streamingMessage) {
115
- streamingMessage.content = content;
116
- conversation.messages = [...conversation.messages];
117
- generatedTokensCount += 1;
118
- }
119
- },
120
- abortController
121
- );
122
- } else {
123
- waitForNonStreaming = true;
124
- const { message: newMessage, completion_tokens: newTokensCount } = await handleNonStreamingResponse(
125
- hf,
126
- conversation
127
- );
128
- // check if the user did not abort the request
129
- if (waitForNonStreaming) {
130
- conversation.messages = [...conversation.messages, newMessage];
131
- generatedTokensCount += newTokensCount;
132
- }
133
- }
134
-
135
- const endTime = performance.now();
136
- latency = Math.round(endTime - startTime);
137
  } catch (error) {
138
- if (conversation.messages.at(-1)?.role === "assistant" && !conversation.messages.at(-1)?.content?.trim()) {
139
- conversation.messages.pop();
140
- conversation.messages = [...conversation.messages];
 
 
 
141
  }
142
  if (error instanceof Error) {
143
  if (error.message.includes("token seems invalid")) {
@@ -153,7 +208,7 @@
153
  }
154
  } finally {
155
  loading = false;
156
- abortController = undefined;
157
  }
158
  }
159
 
@@ -180,6 +235,45 @@
180
  }
181
  }
182
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
183
  onMount(() => {
184
  const storedHfToken = localStorage.getItem(hfTokenLocalStorageKey);
185
  if (storedHfToken !== null) {
@@ -188,7 +282,9 @@
188
  });
189
 
190
  onDestroy(() => {
191
- abortController?.abort();
 
 
192
  });
193
  </script>
194
 
@@ -198,7 +294,9 @@
198
 
199
  <!-- svelte-ignore a11y-no-static-element-interactions -->
200
  <div
201
- class="w-dvh grid h-dvh divide-gray-200 overflow-hidden bg-gray-100/50 max-md:grid-rows-[120px,1fr] max-md:divide-y md:grid-cols-[clamp(220px,20%,350px),minmax(0,1fr),clamp(270px,25%,300px)] dark:divide-gray-800 dark:bg-gray-900 dark:text-gray-300 dark:[color-scheme:dark]"
 
 
202
  >
203
  <div class="flex flex-col overflow-y-auto py-3 pr-3 max-md:pl-3">
204
  <div
@@ -213,8 +311,13 @@
213
  placeholder={systemPromptSupported
214
  ? "Enter a custom prompt"
215
  : "System prompt is not supported with the chosen model."}
216
- value={systemPromptSupported ? conversation.systemMessage.content : ""}
217
- on:input={e => (conversation.systemMessage.content = e.currentTarget.value)}
 
 
 
 
 
218
  class="absolute inset-x-0 bottom-0 h-full resize-none bg-transparent px-3 pt-10 text-sm outline-none"
219
  ></textarea>
220
  </div>
@@ -223,153 +326,168 @@
223
  <div
224
  class="flex h-[calc(100dvh-5rem-120px)] divide-x divide-gray-200 *:w-full md:h-[calc(100dvh-5rem)] md:pt-3 dark:divide-gray-800"
225
  >
226
- <PlaygroundConversation
227
- {loading}
228
- {conversation}
229
- {viewCode}
230
- {hfToken}
231
- on:addMessage={addMessage}
232
- on:deleteMessage={e => deleteMessage(e.detail)}
233
- />
 
 
 
 
 
 
 
 
 
 
 
 
 
234
  </div>
235
  <div
236
- class="fixed inset-x-0 bottom-0 flex h-20 items-center gap-2 overflow-hidden whitespace-nowrap px-3 md:absolute"
237
  >
238
- <button
239
- type="button"
240
- on:click={() => (viewSettings = !viewSettings)}
241
- class="flex h-[39px] items-center gap-1 rounded-lg border border-gray-200 bg-white px-3 py-2.5 text-sm font-medium text-gray-900 hover:bg-gray-100 hover:text-blue-700 focus:z-10 focus:outline-none focus:ring-4 focus:ring-gray-100 md:hidden dark:border-gray-600 dark:bg-gray-800 dark:text-gray-400 dark:hover:bg-gray-700 dark:hover:text-white dark:focus:ring-gray-700"
242
- >
243
- <svg
244
- class="text-black dark:text-white"
245
- style=""
246
- xmlns="http://www.w3.org/2000/svg"
247
- xmlns:xlink="http://www.w3.org/1999/xlink"
248
- aria-hidden="true"
249
- focusable="false"
250
- role="img"
251
- width="1em"
252
- height="1em"
253
- preserveAspectRatio="xMidYMid meet"
254
- viewBox="0 0 24 24"
255
- ><path
256
- fill="currentColor"
257
- d="M2.131 13.63a10 10 0 0 1 .001-3.26c1.101.026 2.092-.502 2.477-1.431c.385-.93.058-2.003-.74-2.763a10 10 0 0 1 2.306-2.307c.76.798 1.834 1.125 2.763.74c.93-.385 1.458-1.376 1.431-2.477a10 10 0 0 1 3.261 0c-.026 1.102.502 2.092 1.431 2.477c.93.385 2.003.058 2.763-.74a10 10 0 0 1 2.307 2.306c-.798.76-1.125 1.834-.74 2.764s1.376 1.458 2.477 1.43a10 10 0 0 1 0 3.262c-1.102-.027-2.092.501-2.477 1.43c-.385.93-.058 2.004.74 2.764a10 10 0 0 1-2.306 2.306c-.76-.798-1.834-1.125-2.764-.74s-1.458 1.376-1.43 2.478a10 10 0 0 1-3.262-.001c.027-1.101-.502-2.092-1.43-2.477c-.93-.385-2.004-.058-2.764.74a10 10 0 0 1-2.306-2.306c.798-.76 1.125-1.834.74-2.763c-.385-.93-1.376-1.458-2.478-1.431M12 15a3 3 0 1 0 0-6a3 3 0 0 0 0 6"
258
- /></svg
259
- >
260
- {!viewSettings ? "Settings" : "Hide Settings"}
261
- </button>
262
- <button
263
- type="button"
264
- on:click={reset}
265
- class="flex size-[39px] flex-none items-center justify-center rounded-lg border border-gray-200 bg-white text-sm font-medium text-gray-900 hover:bg-gray-100 hover:text-blue-700 focus:z-10 focus:outline-none focus:ring-4 focus:ring-gray-100 dark:border-gray-600 dark:bg-gray-800 dark:text-gray-400 dark:hover:bg-gray-700 dark:hover:text-white dark:focus:ring-gray-700"
266
- >
267
- <IconDelete />
268
- </button>
269
- <div class="flex-1 items-center justify-center text-center text-sm text-gray-500">
270
- <span class="max-xl:hidden">{generatedTokensCount} tokens · Latency {latency}ms</span>
271
- </div>
272
- <button
273
- type="button"
274
- on:click={() => (viewCode = !viewCode)}
275
- class="flex h-[39px] items-center gap-2 rounded-lg border border-gray-200 bg-white px-3 py-2.5 text-sm font-medium text-gray-900 hover:bg-gray-100 hover:text-blue-700 focus:z-10 focus:outline-none focus:ring-4 focus:ring-gray-100 dark:border-gray-600 dark:bg-gray-800 dark:text-gray-400 dark:hover:bg-gray-700 dark:hover:text-white dark:focus:ring-gray-700"
276
- >
277
- <IconCode />
278
- {!viewCode ? "View Code" : "Hide Code"}</button
279
- >
280
- <button
281
- on:click={() => {
282
- viewCode = false;
283
- loading ? abort() : submit();
284
- }}
285
- type="button"
286
- class="flex h-[39px] w-24 items-center justify-center gap-2 rounded-lg px-5 py-2.5 text-sm font-medium text-white focus:outline-none focus:ring-4 focus:ring-gray-300 dark:border-gray-700 dark:focus:ring-gray-700 {loading
287
- ? 'bg-red-900 hover:bg-red-800 dark:bg-red-600 dark:hover:bg-red-700'
288
- : 'bg-black hover:bg-gray-900 dark:bg-blue-600 dark:hover:bg-blue-700'}"
289
- >
290
- {#if loading}
291
- <div class="flex flex-none items-center gap-[3px]">
292
- <span class="mr-2">
293
- {#if conversation.streaming}
294
- Stop
295
- {:else}
296
- Cancel
297
- {/if}
298
- </span>
299
- <div
300
- class="h-1 w-1 flex-none animate-bounce rounded-full bg-gray-500 dark:bg-gray-100"
301
- style="animation-delay: 0.25s;"
302
- />
303
- <div
304
- class="h-1 w-1 flex-none animate-bounce rounded-full bg-gray-500 dark:bg-gray-100"
305
- style="animation-delay: 0.5s;"
306
- />
307
- <div
308
- class="h-1 w-1 flex-none animate-bounce rounded-full bg-gray-500 dark:bg-gray-100"
309
- style="animation-delay: 0.75s;"
310
- />
311
- </div>
312
- {:else}
313
- Run <span class="inline-flex gap-0.5 rounded border border-white/20 bg-white/10 px-0.5 text-xs text-white/70"
314
- >⌘<span class="translate-y-px">↵</span></span
315
  >
 
 
 
316
  {/if}
317
- </button>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
318
  </div>
319
  </div>
320
- <div class="flex flex-col p-3 {viewSettings ? 'max-md:fixed' : 'max-md:hidden'} max-md:inset-x-0 max-md:bottom-20">
321
- <div
322
- class="flex flex-1 flex-col gap-6 overflow-y-hidden rounded-xl border border-gray-200/80 bg-white bg-gradient-to-b from-white via-white p-3 shadow-sm dark:border-white/5 dark:bg-gray-900 dark:from-gray-800/40 dark:via-gray-800/40"
323
- >
324
- <div class="flex flex-col gap-2">
325
- <ModelSelector {models} bind:conversation />
326
- <div class="self-end text-xs">
327
- <a
328
- href="https://huggingface.co/{conversation.model.id}"
329
- target="_blank"
330
- class="flex items-center gap-0.5 text-gray-500 hover:text-gray-700 dark:hover:text-gray-400"
331
- >
332
- <svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 32 32"
333
- ><path fill="currentColor" d="M10 6v2h12.59L6 24.59L7.41 26L24 9.41V22h2V6H10z" /></svg
334
  >
335
- Model page
336
- </a>
 
 
 
 
 
 
 
 
 
 
 
 
337
  </div>
338
- </div>
339
 
340
- <GenerationConfig bind:conversation />
341
- {#if hfToken}
342
- <button
343
- on:click={resetToken}
344
- class="mt-auto flex items-center gap-1 self-end text-sm text-gray-500 underline decoration-gray-300 hover:text-gray-800 dark:text-gray-400 dark:decoration-gray-600 dark:hover:text-gray-200"
345
- ><svg xmlns="http://www.w3.org/2000/svg" class="text-xs" width="1em" height="1em" viewBox="0 0 32 32"
346
- ><path
347
- fill="currentColor"
348
- d="M23.216 4H26V2h-7v6h2V5.096A11.96 11.96 0 0 1 28 16c0 6.617-5.383 12-12 12v2c7.72 0 14-6.28 14-14c0-5.009-2.632-9.512-6.784-12"
349
- /><path fill="currentColor" d="M16 20a1.5 1.5 0 1 0 0 3a1.5 1.5 0 0 0 0-3M15 9h2v9h-2z" /><path
350
- fill="currentColor"
351
- d="M16 4V2C8.28 2 2 8.28 2 16c0 4.977 2.607 9.494 6.784 12H6v2h7v-6h-2v2.903A11.97 11.97 0 0 1 4 16C4 9.383 9.383 4 16 4"
352
- /></svg
353
- >
354
- Reset token</button
355
- >
356
- {/if}
357
- <div class="mt-auto hidden">
358
- <div class="mb-3 flex items-center justify-between gap-2">
359
- <label for="default-range" class="block text-sm font-medium text-gray-900 dark:text-white">API Quota</label>
360
- <span
361
- class="rounded bg-gray-100 px-1.5 py-0.5 text-xs font-medium text-gray-800 dark:bg-gray-700 dark:text-gray-300"
362
- >Free</span
363
  >
 
 
 
 
 
 
 
 
364
 
365
- <div class="ml-auto w-12 text-right text-sm">76%</div>
366
- </div>
367
- <div class="h-2 w-full rounded-full bg-gray-200 dark:bg-gray-700">
368
- <div class="h-2 rounded-full bg-black dark:bg-gray-400" style="width: 75%"></div>
 
369
  </div>
370
  </div>
371
  </div>
372
- </div>
373
  </div>
374
 
375
  <a
@@ -380,3 +498,11 @@
380
  <IconInfo classNames="text-xs" />
381
  Give feedback
382
  </a>
 
 
 
 
 
 
 
 
 
1
  <script lang="ts">
2
+ import type { Conversation, ModelEntryWithTokenizer, Session } from "./types";
3
  import type { ChatCompletionInputMessage } from "@huggingface/tasks";
4
 
5
  import { page } from "$app/stores";
 
17
  import HFTokenModal from "./InferencePlaygroundHFTokenModal.svelte";
18
  import ModelSelector from "./InferencePlaygroundModelSelector.svelte";
19
  import PlaygroundConversation from "./InferencePlaygroundConversation.svelte";
20
+ import PlaygroundConversationHeader from "./InferencePlaygroundConversationHeader.svelte";
21
  import IconDelete from "../Icons/IconDelete.svelte";
22
  import IconCode from "../Icons/IconCode.svelte";
23
  import IconInfo from "../Icons/IconInfo.svelte";
24
+ import IconCompare from "../Icons/IconCompare.svelte";
25
+ import ModelSelectorModal from "./InferencePlaygroundModelSelectorModal.svelte";
26
+ import IconThrashcan from "../Icons/IconThrashcan.svelte";
27
+ import { goto } from "$app/navigation";
28
 
29
  export let models: ModelEntryWithTokenizer[];
30
 
31
  const startMessageUser: ChatCompletionInputMessage = { role: "user", content: "" };
32
  const startMessageSystem: ChatCompletionInputMessage = { role: "system", content: "" };
33
 
34
+ const modelIdsFromQueryParam = $page.url.searchParams.get("modelId")?.split(",");
35
+ const modelsFromQueryParam = modelIdsFromQueryParam?.map(id => models.find(model => model.id === id));
36
 
37
+ let session: Session = {
38
+ conversations: [
39
+ {
40
+ model: models.find(m => FEATURED_MODELS_IDS.includes(m.id)) ?? models[0],
41
+ config: { ...defaultGenerationConfig },
42
+ messages: [{ ...startMessageUser }],
43
+ systemMessage: startMessageSystem,
44
+ streaming: true,
45
+ },
46
+ ],
47
  };
48
 
49
+ if (modelsFromQueryParam?.length) {
50
+ const conversations = modelsFromQueryParam.map(model => {
51
+ return {
52
+ model,
53
+ config: { ...defaultGenerationConfig },
54
+ messages: [{ ...startMessageUser }],
55
+ systemMessage: startMessageSystem,
56
+ streaming: true,
57
+ };
58
+ }) as [Conversation] | [Conversation, Conversation];
59
+ session.conversations = conversations;
60
+ session = session;
61
+ }
62
+
63
  let hfToken = "";
64
  let viewCode = false;
65
  let viewSettings = false;
66
  let showTokenModal = false;
67
  let loading = false;
68
+ let abortControllers: AbortController[] = [];
 
 
69
  let waitForNonStreaming = true;
70
  let storeLocallyHfToken = true;
71
+ let selectCompareModelOpen = false;
72
+
73
+ interface GenerationStatistics {
74
+ latency: number;
75
+ generatedTokensCount: number;
76
+ }
77
+ let generationStats: [GenerationStatistics] | [GenerationStatistics, GenerationStatistics] = [
78
+ { latency: 0, generatedTokensCount: 0 },
79
+ ]; // todo: support two models from the starts from the url daw
80
 
81
  const hfTokenLocalStorageKey = "hf_token";
82
 
83
+ $: systemPromptSupported = session.conversations.some(conversation => isSystemPromptSupported(conversation.model));
84
+ $: compareActive = session.conversations.length === 2;
85
 
86
+ function addMessage(conversationIdx: number) {
87
+ const conversation = session.conversations[conversationIdx];
88
  conversation.messages = [
89
  ...conversation.messages,
90
  {
 
92
  content: "",
93
  },
94
  ];
95
+ session = session;
96
  }
97
 
98
+ function deleteMessage(conversationIdx: number, idx: number) {
99
+ session.conversations[conversationIdx].messages.splice(idx, 1)[0];
100
+ session = session;
101
  }
102
 
103
  function reset() {
104
+ session.conversations.map(conversation => {
105
+ conversation.systemMessage.content = "";
106
+ conversation.messages = [{ ...startMessageUser }];
107
+ });
108
+ session = session;
109
  }
110
 
111
  function abort() {
112
+ if (abortControllers.length) {
113
+ for (const abortController of abortControllers) {
114
+ abortController.abort();
115
+ }
116
+ abortControllers = [];
117
+ }
118
  loading = false;
119
  waitForNonStreaming = false;
120
  }
 
125
  showTokenModal = true;
126
  }
127
 
128
+ async function runInference(conversation: Conversation, conversationIdx: number) {
129
+ const startTime = performance.now();
130
+ const hf = createHfInference(hfToken);
131
+
132
+ if (conversation.streaming) {
133
+ const streamingMessage = { role: "assistant", content: "" };
134
+ conversation.messages = [...conversation.messages, streamingMessage];
135
+ const abortController = new AbortController();
136
+ abortControllers.push(abortController);
137
+
138
+ await handleStreamingResponse(
139
+ hf,
140
+ conversation,
141
+ content => {
142
+ if (streamingMessage) {
143
+ streamingMessage.content = content;
144
+ session = session;
145
+ generationStats[conversationIdx].generatedTokensCount += 1;
146
+ }
147
+ },
148
+ abortController
149
+ );
150
+ } else {
151
+ waitForNonStreaming = true;
152
+ const { message: newMessage, completion_tokens: newTokensCount } = await handleNonStreamingResponse(
153
+ hf,
154
+ conversation
155
+ );
156
+ // check if the user did not abort the request
157
+ if (waitForNonStreaming) {
158
+ conversation.messages = [...conversation.messages, newMessage];
159
+ generationStats[conversationIdx].generatedTokensCount += newTokensCount;
160
+ }
161
+ }
162
+
163
+ const endTime = performance.now();
164
+ generationStats[conversationIdx].latency = Math.round(endTime - startTime);
165
+ }
166
+
167
  async function submit() {
168
  if (!hfToken) {
169
  showTokenModal = true;
170
  return;
171
  }
172
 
173
+ for (const [idx, conversation] of session.conversations.entries()) {
174
+ if (conversation.messages.at(-1)?.role === "assistant") {
175
+ let prefix = "";
176
+ if (session.conversations.length === 2) {
177
+ prefix = `Error on ${idx === 0 ? "left" : "right"} conversation. `;
178
+ }
179
+ return alert(`${prefix}Messages must alternate between user/assistant roles.`);
180
+ }
181
  }
182
 
183
  (document.activeElement as HTMLElement).blur();
184
  loading = true;
185
 
186
  try {
187
+ const promises = session.conversations.map((conversation, idx) => runInference(conversation, idx));
188
+ await Promise.all(promises);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
189
  } catch (error) {
190
+ for (const conversation of session.conversations) {
191
+ if (conversation.messages.at(-1)?.role === "assistant" && !conversation.messages.at(-1)?.content?.trim()) {
192
+ conversation.messages.pop();
193
+ conversation.messages = [...conversation.messages];
194
+ }
195
+ session = session;
196
  }
197
  if (error instanceof Error) {
198
  if (error.message.includes("token seems invalid")) {
 
208
  }
209
  } finally {
210
  loading = false;
211
+ abortControllers = [];
212
  }
213
  }
214
 
 
235
  }
236
  }
237
 
238
+ function addCompareModel(modelId: ModelEntryWithTokenizer["id"]) {
239
+ const model = models.find(m => m.id === modelId);
240
+ if (!model || session.conversations.length === 2) {
241
+ return;
242
+ }
243
+ const newConversation = { ...JSON.parse(JSON.stringify(session.conversations[0])), model };
244
+ session.conversations = [...session.conversations, newConversation];
245
+ generationStats = [generationStats[0], { latency: 0, generatedTokensCount: 0 }];
246
+
247
+ // update query param
248
+ const url = new URL($page.url);
249
+ const queryParamValue = `${session.conversations[0].model.id},${modelId}`;
250
+ url.searchParams.set("modelId", queryParamValue);
251
+
252
+ const parentOrigin = "https://huggingface.co";
253
+ window.parent.postMessage({ queryString: `modelId=${queryParamValue}` }, parentOrigin);
254
+ goto(url.toString(), { replaceState: true });
255
+ }
256
+
257
+ function removeCompareModal(conversationIdx: number) {
258
+ session.conversations.splice(conversationIdx, 1)[0];
259
+ session = session;
260
+ generationStats.splice(conversationIdx, 1)[0];
261
+ generationStats = generationStats;
262
+
263
+ // update query param
264
+ const url = new URL($page.url);
265
+ const queryParamValue = url.searchParams.get("modelId");
266
+ if (queryParamValue) {
267
+ const modelIds = queryParamValue.split(",") as [string, string];
268
+ const newQueryParamValue = conversationIdx === 1 ? modelIds[0] : modelIds[1];
269
+ url.searchParams.set("modelId", newQueryParamValue);
270
+
271
+ const parentOrigin = "https://huggingface.co";
272
+ window.parent.postMessage({ queryString: `modelId=${newQueryParamValue}` }, parentOrigin);
273
+ goto(url.toString(), { replaceState: true });
274
+ }
275
+ }
276
+
277
  onMount(() => {
278
  const storedHfToken = localStorage.getItem(hfTokenLocalStorageKey);
279
  if (storedHfToken !== null) {
 
282
  });
283
 
284
  onDestroy(() => {
285
+ for (const abortController of abortControllers) {
286
+ abortController.abort();
287
+ }
288
  });
289
  </script>
290
 
 
294
 
295
  <!-- svelte-ignore a11y-no-static-element-interactions -->
296
  <div
297
+ class="w-dvh grid h-dvh divide-gray-200 overflow-hidden bg-gray-100/50 max-md:grid-rows-[120px,1fr] max-md:divide-y dark:divide-gray-800 dark:bg-gray-900 dark:text-gray-300 dark:[color-scheme:dark] {compareActive
298
+ ? 'md:grid-cols-[clamp(220px,20%,350px),minmax(0,1fr)]'
299
+ : 'md:grid-cols-[clamp(220px,20%,350px),minmax(0,1fr),clamp(270px,25%,300px)]'}"
300
  >
301
  <div class="flex flex-col overflow-y-auto py-3 pr-3 max-md:pl-3">
302
  <div
 
311
  placeholder={systemPromptSupported
312
  ? "Enter a custom prompt"
313
  : "System prompt is not supported with the chosen model."}
314
+ value={systemPromptSupported ? session.conversations[0].systemMessage.content : ""}
315
+ on:input={e => {
316
+ for (const conversation of session.conversations) {
317
+ conversation.systemMessage.content = e.currentTarget.value;
318
+ }
319
+ session = session;
320
+ }}
321
  class="absolute inset-x-0 bottom-0 h-full resize-none bg-transparent px-3 pt-10 text-sm outline-none"
322
  ></textarea>
323
  </div>
 
326
  <div
327
  class="flex h-[calc(100dvh-5rem-120px)] divide-x divide-gray-200 *:w-full md:h-[calc(100dvh-5rem)] md:pt-3 dark:divide-gray-800"
328
  >
329
+ {#each session.conversations as conversation, conversationIdx}
330
+ <div>
331
+ {#if compareActive}
332
+ <PlaygroundConversationHeader
333
+ {models}
334
+ {conversationIdx}
335
+ bind:conversation
336
+ on:close={() => removeCompareModal(conversationIdx)}
337
+ />
338
+ {/if}
339
+ <PlaygroundConversation
340
+ {loading}
341
+ {conversation}
342
+ {viewCode}
343
+ {hfToken}
344
+ {compareActive}
345
+ on:addMessage={() => addMessage(conversationIdx)}
346
+ on:deleteMessage={e => deleteMessage(conversationIdx, e.detail)}
347
+ />
348
+ </div>
349
+ {/each}
350
  </div>
351
  <div
352
+ class="fixed inset-x-0 bottom-0 flex h-20 items-center justify-center gap-2 overflow-hidden whitespace-nowrap px-3 md:absolute"
353
  >
354
+ <div class="flex flex-1 justify-start gap-x-2">
355
+ {#if !compareActive}
356
+ <button
357
+ type="button"
358
+ on:click={() => (viewSettings = !viewSettings)}
359
+ class="flex h-[39px] items-center gap-1 rounded-lg border border-gray-200 bg-white px-3 py-2.5 text-sm font-medium text-gray-900 hover:bg-gray-100 hover:text-blue-700 focus:z-10 focus:outline-none focus:ring-4 focus:ring-gray-100 md:hidden dark:border-gray-600 dark:bg-gray-800 dark:text-gray-400 dark:hover:bg-gray-700 dark:hover:text-white dark:focus:ring-gray-700"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
360
  >
361
+ <IconThrashcan classNames="text-black dark:text-white" />
362
+ {!viewSettings ? "Settings" : "Hide Settings"}
363
+ </button>
364
  {/if}
365
+ <button
366
+ type="button"
367
+ on:click={reset}
368
+ class="flex size-[39px] flex-none items-center justify-center rounded-lg border border-gray-200 bg-white text-sm font-medium text-gray-900 hover:bg-gray-100 hover:text-blue-700 focus:z-10 focus:outline-none focus:ring-4 focus:ring-gray-100 dark:border-gray-600 dark:bg-gray-800 dark:text-gray-400 dark:hover:bg-gray-700 dark:hover:text-white dark:focus:ring-gray-700"
369
+ >
370
+ <IconDelete />
371
+ </button>
372
+ </div>
373
+ <div class="flex flex-1 flex-shrink-0 items-center justify-center gap-x-8 text-center text-sm text-gray-500">
374
+ {#each generationStats as { latency, generatedTokensCount }}
375
+ <span class="max-xl:hidden">{generatedTokensCount} tokens · Latency {latency}ms</span>
376
+ {/each}
377
+ </div>
378
+ <div class="flex flex-1 justify-end gap-x-2">
379
+ <button
380
+ type="button"
381
+ on:click={() => (viewCode = !viewCode)}
382
+ class="flex h-[39px] items-center gap-2 rounded-lg border border-gray-200 bg-white px-3 py-2.5 text-sm font-medium text-gray-900 hover:bg-gray-100 hover:text-blue-700 focus:z-10 focus:outline-none focus:ring-4 focus:ring-gray-100 dark:border-gray-600 dark:bg-gray-800 dark:text-gray-400 dark:hover:bg-gray-700 dark:hover:text-white dark:focus:ring-gray-700"
383
+ >
384
+ <IconCode />
385
+ {!viewCode ? "View Code" : "Hide Code"}</button
386
+ >
387
+ <button
388
+ on:click={() => {
389
+ viewCode = false;
390
+ loading ? abort() : submit();
391
+ }}
392
+ type="button"
393
+ class="flex h-[39px] w-24 items-center justify-center gap-2 rounded-lg px-5 py-2.5 text-sm font-medium text-white focus:outline-none focus:ring-4 focus:ring-gray-300 dark:border-gray-700 dark:focus:ring-gray-700 {loading
394
+ ? 'bg-red-900 hover:bg-red-800 dark:bg-red-600 dark:hover:bg-red-700'
395
+ : 'bg-black hover:bg-gray-900 dark:bg-blue-600 dark:hover:bg-blue-700'}"
396
+ >
397
+ {#if loading}
398
+ <div class="flex flex-none items-center gap-[3px]">
399
+ <span class="mr-2">
400
+ {#if session.conversations[0].streaming || session.conversations[1]?.streaming}
401
+ Stop
402
+ {:else}
403
+ Cancel
404
+ {/if}
405
+ </span>
406
+ <div
407
+ class="h-1 w-1 flex-none animate-bounce rounded-full bg-gray-500 dark:bg-gray-100"
408
+ style="animation-delay: 0.25s;"
409
+ />
410
+ <div
411
+ class="h-1 w-1 flex-none animate-bounce rounded-full bg-gray-500 dark:bg-gray-100"
412
+ style="animation-delay: 0.5s;"
413
+ />
414
+ <div
415
+ class="h-1 w-1 flex-none animate-bounce rounded-full bg-gray-500 dark:bg-gray-100"
416
+ style="animation-delay: 0.75s;"
417
+ />
418
+ </div>
419
+ {:else}
420
+ Run <span
421
+ class="inline-flex gap-0.5 rounded border border-white/20 bg-white/10 px-0.5 text-xs text-white/70"
422
+ >⌘<span class="translate-y-px">↵</span></span
423
+ >
424
+ {/if}
425
+ </button>
426
+ </div>
427
  </div>
428
  </div>
429
+ {#if !compareActive}
430
+ <div class="flex flex-col p-3 {viewSettings ? 'max-md:fixed' : 'max-md:hidden'} max-md:inset-x-0 max-md:bottom-20">
431
+ <div
432
+ class="flex flex-1 flex-col gap-6 overflow-y-hidden rounded-xl border border-gray-200/80 bg-white bg-gradient-to-b from-white via-white p-3 shadow-sm dark:border-white/5 dark:bg-gray-900 dark:from-gray-800/40 dark:via-gray-800/40"
433
+ >
434
+ <div class="flex flex-col gap-2">
435
+ <ModelSelector {models} bind:conversation={session.conversations[0]} />
436
+ <div class="flex items-center gap-2 self-end whitespace-nowrap px-2 text-xs">
437
+ <button
438
+ class="flex items-center gap-0.5 text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-300"
439
+ on:click={() => (selectCompareModelOpen = true)}
 
 
 
440
  >
441
+ <IconCompare />
442
+ Compare
443
+ </button>
444
+ <a
445
+ href="https://huggingface.co/{session.conversations[0].model.id}"
446
+ target="_blank"
447
+ class="flex items-center gap-0.5 text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-300"
448
+ >
449
+ <svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 32 32"
450
+ ><path fill="currentColor" d="M10 6v2h12.59L6 24.59L7.41 26L24 9.41V22h2V6H10z" /></svg
451
+ >
452
+ Model page
453
+ </a>
454
+ </div>
455
  </div>
 
456
 
457
+ <GenerationConfig bind:conversation={session.conversations[0]} />
458
+ {#if hfToken}
459
+ <button
460
+ on:click={resetToken}
461
+ class="mt-auto flex items-center gap-1 self-end text-sm text-gray-500 underline decoration-gray-300 hover:text-gray-800 dark:text-gray-400 dark:decoration-gray-600 dark:hover:text-gray-200"
462
+ ><svg xmlns="http://www.w3.org/2000/svg" class="text-xs" width="1em" height="1em" viewBox="0 0 32 32"
463
+ ><path
464
+ fill="currentColor"
465
+ d="M23.216 4H26V2h-7v6h2V5.096A11.96 11.96 0 0 1 28 16c0 6.617-5.383 12-12 12v2c7.72 0 14-6.28 14-14c0-5.009-2.632-9.512-6.784-12"
466
+ /><path fill="currentColor" d="M16 20a1.5 1.5 0 1 0 0 3a1.5 1.5 0 0 0 0-3M15 9h2v9h-2z" /><path
467
+ fill="currentColor"
468
+ d="M16 4V2C8.28 2 2 8.28 2 16c0 4.977 2.607 9.494 6.784 12H6v2h7v-6h-2v2.903A11.97 11.97 0 0 1 4 16C4 9.383 9.383 4 16 4"
469
+ /></svg
470
+ >
471
+ Reset token</button
 
 
 
 
 
 
 
 
472
  >
473
+ {/if}
474
+ <div class="mt-auto hidden">
475
+ <div class="mb-3 flex items-center justify-between gap-2">
476
+ <label for="default-range" class="block text-sm font-medium text-gray-900 dark:text-white">API Quota</label>
477
+ <span
478
+ class="rounded bg-gray-100 px-1.5 py-0.5 text-xs font-medium text-gray-800 dark:bg-gray-700 dark:text-gray-300"
479
+ >Free</span
480
+ >
481
 
482
+ <div class="ml-auto w-12 text-right text-sm">76%</div>
483
+ </div>
484
+ <div class="h-2 w-full rounded-full bg-gray-200 dark:bg-gray-700">
485
+ <div class="h-2 rounded-full bg-black dark:bg-gray-400" style="width: 75%"></div>
486
+ </div>
487
  </div>
488
  </div>
489
  </div>
490
+ {/if}
491
  </div>
492
 
493
  <a
 
498
  <IconInfo classNames="text-xs" />
499
  Give feedback
500
  </a>
501
+ {#if selectCompareModelOpen}
502
+ <ModelSelectorModal
503
+ {models}
504
+ conversation={session.conversations[0]}
505
+ on:modelSelected={e => addCompareModel(e.detail)}
506
+ on:close={() => (selectCompareModelOpen = false)}
507
+ />
508
+ {/if}
src/lib/components/InferencePlayground/InferencePlaygroundConversation.svelte CHANGED
@@ -11,6 +11,7 @@
11
  export let loading: boolean;
12
  export let viewCode: boolean;
13
  export let hfToken: string;
 
14
 
15
  let shouldScrollToBottom = true;
16
  let isProgrammaticScroll = true;
@@ -63,7 +64,9 @@
63
  </script>
64
 
65
  <div
66
- class="flex max-h-[calc(100dvh-5.8rem)] flex-col overflow-y-auto overflow-x-hidden @container"
 
 
67
  class:animate-pulse={loading && !conversation.streaming}
68
  bind:this={messageContainer}
69
  on:scroll={() => {
 
11
  export let loading: boolean;
12
  export let viewCode: boolean;
13
  export let hfToken: string;
14
+ export let compareActive: boolean;
15
 
16
  let shouldScrollToBottom = true;
17
  let isProgrammaticScroll = true;
 
64
  </script>
65
 
66
  <div
67
+ class="flex flex-col overflow-y-auto overflow-x-hidden @container {compareActive
68
+ ? 'max-h-[calc(100dvh-5.8rem-2.5rem-75px)] md:max-h-[calc(100dvh-5.8rem-2.5rem)]'
69
+ : 'max-h-[calc(100dvh-5.8rem-2.5rem-75px)] md:max-h-[calc(100dvh-5.8rem)]'}"
70
  class:animate-pulse={loading && !conversation.streaming}
71
  bind:this={messageContainer}
72
  on:scroll={() => {
src/lib/components/InferencePlayground/InferencePlaygroundConversationHeader.svelte ADDED
@@ -0,0 +1,80 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <script lang="ts">
2
+ import type { Conversation, ModelEntryWithTokenizer } from "$lib/components/InferencePlayground/types";
3
+
4
+ import { createEventDispatcher } from "svelte";
5
+
6
+ import { page } from "$app/stores";
7
+ import IconCog from "../Icons/IconCog.svelte";
8
+ import GenerationConfig from "./InferencePlaygroundGenerationConfig.svelte";
9
+ import ModelSelectorModal from "./InferencePlaygroundModelSelectorModal.svelte";
10
+ import Avatar from "../Avatar.svelte";
11
+ import { goto } from "$app/navigation";
12
+
13
+ export let models: ModelEntryWithTokenizer[];
14
+ export let conversation: Conversation;
15
+ export let conversationIdx: number;
16
+
17
+ const dispatch = createEventDispatcher<{ close: string }>();
18
+
19
+ let modelSelectorOpen = false;
20
+
21
+ function changeModel(newModelId: ModelEntryWithTokenizer["id"]) {
22
+ const model = models.find(m => m.id === newModelId);
23
+ if (!model) {
24
+ return;
25
+ }
26
+ conversation.model = model;
27
+
28
+ const url = new URL($page.url);
29
+ const queryParamValue = url.searchParams.get("modelId");
30
+ if (queryParamValue) {
31
+ const modelIds = queryParamValue.split(",") as [string, string];
32
+ modelIds[conversationIdx] = newModelId;
33
+
34
+ const newQueryParamValue = modelIds.join(",");
35
+ url.searchParams.set("modelId", newQueryParamValue);
36
+
37
+ const parentOrigin = "https://huggingface.co";
38
+ window.parent.postMessage({ queryString: `modelId=${newQueryParamValue}` }, parentOrigin);
39
+
40
+ goto(url.toString(), { replaceState: true });
41
+ }
42
+ }
43
+
44
+ $: [nameSpace] = conversation.model.id.split("/");
45
+ </script>
46
+
47
+ {#if modelSelectorOpen}
48
+ <ModelSelectorModal
49
+ {models}
50
+ {conversation}
51
+ on:modelSelected={e => changeModel(e.detail)}
52
+ on:close={() => (modelSelectorOpen = false)}
53
+ />
54
+ {/if}
55
+
56
+ <div
57
+ class="{conversationIdx === 0
58
+ ? 'mr-4'
59
+ : 'mx-4'} flex h-11 flex-none items-center gap-2 whitespace-nowrap rounded-lg border border-gray-200/80 bg-white pl-3 pr-2 text-sm leading-none shadow-sm *:flex-none dark:border-white/5 dark:bg-gray-800/70 dark:hover:bg-gray-800"
60
+ >
61
+ <Avatar orgName={nameSpace} size="md" />
62
+ <button class="!flex-1 self-stretch text-left hover:underline" on:click={() => (modelSelectorOpen = true)}
63
+ >{conversation.model.id}</button
64
+ >
65
+ <button
66
+ class="flex size-6 items-center justify-center rounded bg-gray-50 text-xs hover:bg-gray-100 dark:bg-gray-700 dark:hover:bg-gray-600"
67
+ on:click={() => dispatch("close", conversation.model.id)}
68
+ >
69
+
70
+ </button>
71
+ <button
72
+ class="group relative flex size-6 items-center justify-center rounded bg-gray-50 hover:bg-gray-100 dark:bg-gray-700 dark:hover:bg-gray-600"
73
+ >
74
+ <IconCog />
75
+ <GenerationConfig
76
+ bind:conversation
77
+ classNames="absolute top-7 min-w-[250px] z-10 right-3 bg-white dark:bg-gray-900 p-4 rounded-xl border border-gray-200 dark:border-gray-800 hidden group-focus:flex hover:flex"
78
+ />
79
+ </button>
80
+ </div>
src/lib/components/InferencePlayground/InferencePlaygroundModelSelector.svelte CHANGED
@@ -6,24 +6,13 @@
6
 
7
  import IconCaret from "../Icons/IconCaret.svelte";
8
  import ModelSelectorModal from "./InferencePlaygroundModelSelectorModal.svelte";
 
9
 
10
  export let models: ModelEntryWithTokenizer[] = [];
11
  export let conversation: Conversation;
12
 
13
  let showModelPickerModal = false;
14
 
15
- async function getAvatarUrl(orgName: string) {
16
- const url = `https://huggingface.co/api/organizations/${orgName}/avatar`;
17
- const res = await fetch(url);
18
- if (!res.ok) {
19
- console.error(`Error getting avatar url for org: ${orgName}`, res.status, res.statusText);
20
- return;
21
- }
22
- const json = await res.json();
23
- const { avatarUrl } = json;
24
- return avatarUrl;
25
- }
26
-
27
  function changeModel(modelId: ModelEntryWithTokenizer["id"]) {
28
  const model = models.find(m => m.id === modelId);
29
  if (!model) {
@@ -63,9 +52,7 @@
63
  >
64
  <div class="flex flex-col items-start">
65
  <div class="flex items-center gap-1 text-sm text-gray-500 dark:text-gray-300">
66
- {#await getAvatarUrl(nameSpace) then avatarUrl}
67
- <img class="size-3 flex-none rounded bg-gray-200 object-cover" src={avatarUrl} alt="{nameSpace} avatar" />
68
- {/await}
69
  {nameSpace}
70
  </div>
71
  <div>{modelName}</div>
 
6
 
7
  import IconCaret from "../Icons/IconCaret.svelte";
8
  import ModelSelectorModal from "./InferencePlaygroundModelSelectorModal.svelte";
9
+ import Avatar from "../Avatar.svelte";
10
 
11
  export let models: ModelEntryWithTokenizer[] = [];
12
  export let conversation: Conversation;
13
 
14
  let showModelPickerModal = false;
15
 
 
 
 
 
 
 
 
 
 
 
 
 
16
  function changeModel(modelId: ModelEntryWithTokenizer["id"]) {
17
  const model = models.find(m => m.id === modelId);
18
  if (!model) {
 
52
  >
53
  <div class="flex flex-col items-start">
54
  <div class="flex items-center gap-1 text-sm text-gray-500 dark:text-gray-300">
55
+ <Avatar orgName={nameSpace} size="sm" />
 
 
56
  {nameSpace}
57
  </div>
58
  <div>{modelName}</div>
src/lib/components/InferencePlayground/InferencePlaygroundModelSelectorModal.svelte CHANGED
@@ -103,7 +103,7 @@
103
  bind:this={backdropEl}
104
  on:click|stopPropagation={handleBackdropClick}
105
  >
106
- <div class="flex w-full max-w-[600px] items-start justify-center overflow-hidden whitespace-nowrap p-10">
107
  <div
108
  class="flex h-full w-full flex-col overflow-hidden rounded-lg border bg-white text-gray-900 shadow-md dark:border-gray-800 dark:bg-gray-900 dark:text-gray-300"
109
  bind:this={containerEl}
 
103
  bind:this={backdropEl}
104
  on:click|stopPropagation={handleBackdropClick}
105
  >
106
+ <div class="flex w-full max-w-[600px] items-start justify-center overflow-hidden whitespace-nowrap p-10 text-left">
107
  <div
108
  class="flex h-full w-full flex-col overflow-hidden rounded-lg border bg-white text-gray-900 shadow-md dark:border-gray-800 dark:bg-gray-900 dark:text-gray-300"
109
  bind:this={containerEl}
src/lib/components/InferencePlayground/types.ts CHANGED
@@ -10,6 +10,10 @@ export type Conversation = {
10
  streaming: boolean;
11
  };
12
 
 
 
 
 
13
  interface TokenizerConfig {
14
  chat_template?: string;
15
  model_max_length?: number;
 
10
  streaming: boolean;
11
  };
12
 
13
+ export type Session = {
14
+ conversations: [Conversation] | [Conversation, Conversation];
15
+ };
16
+
17
  interface TokenizerConfig {
18
  chat_template?: string;
19
  model_max_length?: number;