mishig HF staff commited on
Commit
5c5d81b
1 Parent(s): c5c965b

Compare models

Browse files
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/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,73 @@
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 +91,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 +124,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 +207,7 @@
153
  }
154
  } finally {
155
  loading = false;
156
- abortController = undefined;
157
  }
158
  }
159
 
@@ -180,6 +234,45 @@
180
  }
181
  }
182
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
183
  onMount(() => {
184
  const storedHfToken = localStorage.getItem(hfTokenLocalStorageKey);
185
  if (storedHfToken !== null) {
@@ -188,7 +281,9 @@
188
  });
189
 
190
  onDestroy(() => {
191
- abortController?.abort();
 
 
192
  });
193
  </script>
194
 
@@ -198,7 +293,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 +310,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 +325,175 @@
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
 
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 ModelSelectorModal from "./InferencePlaygroundModelSelectorModal.svelte";
25
+ import IconThrashcan from "../Icons/IconThrashcan.svelte";
26
+ import { goto } from "$app/navigation";
27
 
28
  export let models: ModelEntryWithTokenizer[];
29
 
30
  const startMessageUser: ChatCompletionInputMessage = { role: "user", content: "" };
31
  const startMessageSystem: ChatCompletionInputMessage = { role: "system", content: "" };
32
 
33
+ const modelIdsFromQueryParam = $page.url.searchParams.get("modelId")?.split(",");
34
+ const modelsFromQueryParam = modelIdsFromQueryParam?.map(id => models.find(model => model.id === id));
35
 
36
+ let session: Session = {
37
+ conversations: [
38
+ {
39
+ model: models.find(m => FEATURED_MODELS_IDS.includes(m.id)) ?? models[0],
40
+ config: { ...defaultGenerationConfig },
41
+ messages: [{ ...startMessageUser }],
42
+ systemMessage: startMessageSystem,
43
+ streaming: true,
44
+ },
45
+ ],
46
  };
47
 
48
+ if (modelsFromQueryParam?.length) {
49
+ const conversations = modelsFromQueryParam.map(model => {
50
+ return {
51
+ model,
52
+ config: { ...defaultGenerationConfig },
53
+ messages: [{ ...startMessageUser }],
54
+ systemMessage: startMessageSystem,
55
+ streaming: true,
56
+ };
57
+ }) as [Conversation] | [Conversation, Conversation];
58
+ session.conversations = conversations;
59
+ session = session;
60
+ }
61
+
62
  let hfToken = "";
63
  let viewCode = false;
64
  let viewSettings = false;
65
  let showTokenModal = false;
66
  let loading = false;
67
+ let abortControllers: AbortController[] = [];
 
 
68
  let waitForNonStreaming = true;
69
  let storeLocallyHfToken = true;
70
+ let selectCompareModelOpen = false;
71
+
72
+ interface GenerationStatistics {
73
+ latency: number;
74
+ generatedTokensCount: number;
75
+ }
76
+ let generationStats: [GenerationStatistics] | [GenerationStatistics, GenerationStatistics] = [
77
+ { latency: 0, generatedTokensCount: 0 },
78
+ ]; // todo: support two models from the starts from the url daw
79
 
80
  const hfTokenLocalStorageKey = "hf_token";
81
 
82
+ $: systemPromptSupported = session.conversations.some(conversation => isSystemPromptSupported(conversation.model));
83
+ $: compareActive = session.conversations.length === 2;
84
 
85
+ function addMessage(conversationIdx: number) {
86
+ const conversation = session.conversations[conversationIdx];
87
  conversation.messages = [
88
  ...conversation.messages,
89
  {
 
91
  content: "",
92
  },
93
  ];
94
+ session = session;
95
  }
96
 
97
+ function deleteMessage(conversationIdx: number, idx: number) {
98
+ session.conversations[conversationIdx].messages.splice(idx, 1)[0];
99
+ session = session;
100
  }
101
 
102
  function reset() {
103
+ session.conversations.map(conversation => {
104
+ conversation.systemMessage.content = "";
105
+ conversation.messages = [{ ...startMessageUser }];
106
+ });
107
+ session = session;
108
  }
109
 
110
  function abort() {
111
+ if (abortControllers.length) {
112
+ for (const abortController of abortControllers) {
113
+ abortController.abort();
114
+ }
115
+ abortControllers = [];
116
+ }
117
  loading = false;
118
  waitForNonStreaming = false;
119
  }
 
124
  showTokenModal = true;
125
  }
126
 
127
+ async function runInference(conversation: Conversation, conversationIdx: number) {
128
+ const startTime = performance.now();
129
+ const hf = createHfInference(hfToken);
130
+
131
+ if (conversation.streaming) {
132
+ const streamingMessage = { role: "assistant", content: "" };
133
+ conversation.messages = [...conversation.messages, streamingMessage];
134
+ const abortController = new AbortController();
135
+ abortControllers.push(abortController);
136
+
137
+ await handleStreamingResponse(
138
+ hf,
139
+ conversation,
140
+ content => {
141
+ if (streamingMessage) {
142
+ streamingMessage.content = content;
143
+ session = session;
144
+ generationStats[conversationIdx].generatedTokensCount += 1;
145
+ }
146
+ },
147
+ abortController
148
+ );
149
+ } else {
150
+ waitForNonStreaming = true;
151
+ const { message: newMessage, completion_tokens: newTokensCount } = await handleNonStreamingResponse(
152
+ hf,
153
+ conversation
154
+ );
155
+ // check if the user did not abort the request
156
+ if (waitForNonStreaming) {
157
+ conversation.messages = [...conversation.messages, newMessage];
158
+ generationStats[conversationIdx].generatedTokensCount += newTokensCount;
159
+ }
160
+ }
161
+
162
+ const endTime = performance.now();
163
+ generationStats[conversationIdx].latency = Math.round(endTime - startTime);
164
+ }
165
+
166
  async function submit() {
167
  if (!hfToken) {
168
  showTokenModal = true;
169
  return;
170
  }
171
 
172
+ for (const [idx, conversation] of session.conversations.entries()) {
173
+ if (conversation.messages.at(-1)?.role === "assistant") {
174
+ let prefix = "";
175
+ if (session.conversations.length === 2) {
176
+ prefix = `Error on ${idx === 0 ? "left" : "right"} conversation. `;
177
+ }
178
+ return alert(`${prefix}Messages must alternate between user/assistant roles.`);
179
+ }
180
  }
181
 
182
  (document.activeElement as HTMLElement).blur();
183
  loading = true;
184
 
185
  try {
186
+ const promises = session.conversations.map((conversation, idx) => runInference(conversation, idx));
187
+ await Promise.all(promises);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
188
  } catch (error) {
189
+ for (const conversation of session.conversations) {
190
+ if (conversation.messages.at(-1)?.role === "assistant" && !conversation.messages.at(-1)?.content?.trim()) {
191
+ conversation.messages.pop();
192
+ conversation.messages = [...conversation.messages];
193
+ }
194
+ session = session;
195
  }
196
  if (error instanceof Error) {
197
  if (error.message.includes("token seems invalid")) {
 
207
  }
208
  } finally {
209
  loading = false;
210
+ abortControllers = [];
211
  }
212
  }
213
 
 
234
  }
235
  }
236
 
237
+ function addCompareModel(modelId: ModelEntryWithTokenizer["id"]) {
238
+ const model = models.find(m => m.id === modelId);
239
+ if (!model || session.conversations.length === 2) {
240
+ return;
241
+ }
242
+ const newConversation = { ...JSON.parse(JSON.stringify(session.conversations[0])), model };
243
+ session.conversations = [...session.conversations, newConversation];
244
+ generationStats = [generationStats[0], { latency: 0, generatedTokensCount: 0 }];
245
+
246
+ // update query param
247
+ const url = new URL($page.url);
248
+ const queryParamValue = `${session.conversations[0].model.id},${modelId}`;
249
+ url.searchParams.set("modelId", queryParamValue);
250
+
251
+ const parentOrigin = "https://huggingface.co";
252
+ window.parent.postMessage({ queryString: `modelId=${queryParamValue}` }, parentOrigin);
253
+ goto(url.toString(), { replaceState: true });
254
+ }
255
+
256
+ function removeCompareModal(conversationIdx: number) {
257
+ session.conversations.splice(conversationIdx, 1)[0];
258
+ session = session;
259
+ generationStats.splice(conversationIdx, 1)[0];
260
+ generationStats = generationStats;
261
+
262
+ // update query param
263
+ const url = new URL($page.url);
264
+ const queryParamValue = url.searchParams.get("modelId");
265
+ if (queryParamValue) {
266
+ const modelIds = queryParamValue.split(",") as [string, string];
267
+ const newQueryParamValue = conversationIdx === 1 ? modelIds[0] : modelIds[1];
268
+ url.searchParams.set("modelId", newQueryParamValue);
269
+
270
+ const parentOrigin = "https://huggingface.co";
271
+ window.parent.postMessage({ queryString: `modelId=${newQueryParamValue}` }, parentOrigin);
272
+ goto(url.toString(), { replaceState: true });
273
+ }
274
+ }
275
+
276
  onMount(() => {
277
  const storedHfToken = localStorage.getItem(hfTokenLocalStorageKey);
278
  if (storedHfToken !== null) {
 
281
  });
282
 
283
  onDestroy(() => {
284
+ for (const abortController of abortControllers) {
285
+ abortController.abort();
286
+ }
287
  });
288
  </script>
289
 
 
293
 
294
  <!-- svelte-ignore a11y-no-static-element-interactions -->
295
  <div
296
+ 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
297
+ ? 'md:grid-cols-[clamp(220px,20%,350px),minmax(0,1fr)]'
298
+ : 'md:grid-cols-[clamp(220px,20%,350px),minmax(0,1fr),clamp(270px,25%,300px)]'}"
299
  >
300
  <div class="flex flex-col overflow-y-auto py-3 pr-3 max-md:pl-3">
301
  <div
 
310
  placeholder={systemPromptSupported
311
  ? "Enter a custom prompt"
312
  : "System prompt is not supported with the chosen model."}
313
+ value={systemPromptSupported ? session.conversations[0].systemMessage.content : ""}
314
+ on:input={e => {
315
+ for (const conversation of session.conversations) {
316
+ conversation.systemMessage.content = e.currentTarget.value;
317
+ }
318
+ session = session;
319
+ }}
320
  class="absolute inset-x-0 bottom-0 h-full resize-none bg-transparent px-3 pt-10 text-sm outline-none"
321
  ></textarea>
322
  </div>
 
325
  <div
326
  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"
327
  >
328
+ {#each session.conversations as conversation, conversationIdx}
329
+ <div>
330
+ {#if compareActive}
331
+ <PlaygroundConversationHeader
332
+ {models}
333
+ {conversationIdx}
334
+ bind:conversation
335
+ on:close={() => removeCompareModal(conversationIdx)}
336
+ />
337
+ {/if}
338
+ <PlaygroundConversation
339
+ {loading}
340
+ {conversation}
341
+ {viewCode}
342
+ {hfToken}
343
+ on:addMessage={() => addMessage(conversationIdx)}
344
+ on:deleteMessage={e => deleteMessage(conversationIdx, e.detail)}
345
+ />
346
+ </div>
347
+ {/each}
348
  </div>
349
  <div
350
+ class="fixed inset-x-0 bottom-0 flex h-20 items-center justify-center gap-2 overflow-hidden whitespace-nowrap px-3 md:absolute"
351
  >
352
+ <div class="flex flex-1 justify-start gap-x-2">
353
+ {#if !compareActive}
354
+ <button
355
+ type="button"
356
+ on:click={() => (viewSettings = !viewSettings)}
357
+ 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"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
358
  >
359
+ <IconThrashcan classNames="text-black dark:text-white" />
360
+ {!viewSettings ? "Settings" : "Hide Settings"}
361
+ </button>
362
  {/if}
363
+ <button
364
+ type="button"
365
+ on:click={reset}
366
+ 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"
367
+ >
368
+ <IconDelete />
369
+ </button>
370
+ </div>
371
+ <div class="flex flex-1 flex-shrink-0 items-center justify-center gap-x-8 text-center text-sm text-gray-500">
372
+ {#each generationStats as { latency, generatedTokensCount }}
373
+ <span class="max-xl:hidden">{generatedTokensCount} tokens · Latency {latency}ms</span>
374
+ {/each}
375
+ </div>
376
+ <div class="flex flex-1 justify-end gap-x-2">
377
+ <button
378
+ type="button"
379
+ on:click={() => (viewCode = !viewCode)}
380
+ 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"
381
+ >
382
+ <IconCode />
383
+ {!viewCode ? "View Code" : "Hide Code"}</button
384
+ >
385
+ <button
386
+ on:click={() => {
387
+ viewCode = false;
388
+ loading ? abort() : submit();
389
+ }}
390
+ type="button"
391
+ 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
392
+ ? 'bg-red-900 hover:bg-red-800 dark:bg-red-600 dark:hover:bg-red-700'
393
+ : 'bg-black hover:bg-gray-900 dark:bg-blue-600 dark:hover:bg-blue-700'}"
394
+ >
395
+ {#if loading}
396
+ <div class="flex flex-none items-center gap-[3px]">
397
+ <span class="mr-2">
398
+ {#if session.conversations[0].streaming || session.conversations[1]?.streaming}
399
+ Stop
400
+ {:else}
401
+ Cancel
402
+ {/if}
403
+ </span>
404
+ <div
405
+ class="h-1 w-1 flex-none animate-bounce rounded-full bg-gray-500 dark:bg-gray-100"
406
+ style="animation-delay: 0.25s;"
407
+ />
408
+ <div
409
+ class="h-1 w-1 flex-none animate-bounce rounded-full bg-gray-500 dark:bg-gray-100"
410
+ style="animation-delay: 0.5s;"
411
+ />
412
+ <div
413
+ class="h-1 w-1 flex-none animate-bounce rounded-full bg-gray-500 dark:bg-gray-100"
414
+ style="animation-delay: 0.75s;"
415
+ />
416
+ </div>
417
+ {:else}
418
+ Run <span
419
+ class="inline-flex gap-0.5 rounded border border-white/20 bg-white/10 px-0.5 text-xs text-white/70"
420
+ >⌘<span class="translate-y-px">↵</span></span
421
+ >
422
+ {/if}
423
+ </button>
424
+ </div>
425
  </div>
426
  </div>
427
+ {#if !compareActive}
428
+ <div class="flex flex-col p-3 {viewSettings ? 'max-md:fixed' : 'max-md:hidden'} max-md:inset-x-0 max-md:bottom-20">
429
+ <div
430
+ 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"
431
+ >
432
+ <div class="flex flex-col gap-2">
433
+ <ModelSelector {models} bind:conversation={session.conversations[0]} />
434
+ <div class="self-end text-xs">
435
+ <a
436
+ href="https://huggingface.co/{session.conversations[0].model.id}"
437
+ target="_blank"
438
+ class="flex items-center gap-0.5 text-gray-500 hover:text-gray-700 dark:hover:text-gray-400"
 
 
439
  >
440
+ <svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 32 32"
441
+ ><path fill="currentColor" d="M10 6v2h12.59L6 24.59L7.41 26L24 9.41V22h2V6H10z" /></svg
442
+ >
443
+ Model page
444
+ </a>
445
+ </div>
446
  </div>
 
447
 
 
 
448
  <button
449
+ class="group relative -mt-4 flex h-[26px] w-full items-center justify-center gap-2 rounded-lg bg-black px-5 text-sm text-white hover:bg-gray-900 focus:outline-none focus:ring-4 focus:ring-gray-300 dark:border-gray-700 dark:bg-blue-600 dark:hover:bg-blue-700 dark:focus:ring-gray-700"
450
+ on:click={() => (selectCompareModelOpen = true)}
 
 
 
 
 
 
 
 
 
 
451
  >
452
+ Compare with...
453
+ {#if selectCompareModelOpen}
454
+ <ModelSelectorModal
455
+ {models}
456
+ conversation={session.conversations[0]}
457
+ on:modelSelected={e => addCompareModel(e.detail)}
458
+ on:close={() => (selectCompareModelOpen = false)}
459
+ />
460
+ {/if}
461
+ </button>
462
+
463
+ <GenerationConfig bind:conversation={session.conversations[0]} />
464
+ {#if hfToken}
465
+ <button
466
+ on:click={resetToken}
467
+ 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"
468
+ ><svg xmlns="http://www.w3.org/2000/svg" class="text-xs" width="1em" height="1em" viewBox="0 0 32 32"
469
+ ><path
470
+ fill="currentColor"
471
+ 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"
472
+ /><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
473
+ fill="currentColor"
474
+ 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"
475
+ /></svg
476
+ >
477
+ Reset token</button
478
  >
479
+ {/if}
480
+ <div class="mt-auto hidden">
481
+ <div class="mb-3 flex items-center justify-between gap-2">
482
+ <label for="default-range" class="block text-sm font-medium text-gray-900 dark:text-white">API Quota</label>
483
+ <span
484
+ 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"
485
+ >Free</span
486
+ >
487
 
488
+ <div class="ml-auto w-12 text-right text-sm">76%</div>
489
+ </div>
490
+ <div class="h-2 w-full rounded-full bg-gray-200 dark:bg-gray-700">
491
+ <div class="h-2 rounded-full bg-black dark:bg-gray-400" style="width: 75%"></div>
492
+ </div>
493
  </div>
494
  </div>
495
  </div>
496
+ {/if}
497
  </div>
498
 
499
  <a
src/lib/components/InferencePlayground/InferencePlaygroundConversationHeader.svelte ADDED
@@ -0,0 +1,73 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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 { goto } from "$app/navigation";
11
+
12
+ export let models: ModelEntryWithTokenizer[];
13
+ export let conversation: Conversation;
14
+ export let conversationIdx: number;
15
+
16
+ const dispatch = createEventDispatcher<{ close: string }>();
17
+
18
+ let modelSelectorOpen = false;
19
+
20
+ function changeModel(newModelId: ModelEntryWithTokenizer["id"]) {
21
+ const model = models.find(m => m.id === newModelId);
22
+ if (!model) {
23
+ return;
24
+ }
25
+ conversation.model = model;
26
+
27
+ const url = new URL($page.url);
28
+ const queryParamValue = url.searchParams.get("modelId");
29
+ if (queryParamValue) {
30
+ const modelIds = queryParamValue.split(",") as [string, string];
31
+ modelIds[conversationIdx] = newModelId;
32
+
33
+ const newQueryParamValue = modelIds.join(",");
34
+ url.searchParams.set("modelId", newQueryParamValue);
35
+
36
+ const parentOrigin = "https://huggingface.co";
37
+ window.parent.postMessage({ queryString: `modelId=${newQueryParamValue}` }, parentOrigin);
38
+
39
+ goto(url.toString(), { replaceState: true });
40
+ }
41
+ }
42
+ </script>
43
+
44
+ {#if modelSelectorOpen}
45
+ <ModelSelectorModal
46
+ {models}
47
+ {conversation}
48
+ on:modelSelected={e => changeModel(e.detail)}
49
+ on:close={() => (modelSelectorOpen = false)}
50
+ />
51
+ {/if}
52
+
53
+ <div
54
+ class="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-gray-800 dark:bg-gray-800/70 dark:hover:bg-gray-800"
55
+ >
56
+ <div class="size-3.5 rounded bg-black dark:bg-gray-400"></div>
57
+ <button on:click={() => (modelSelectorOpen = true)}>{conversation.model.id}</button>
58
+ <button
59
+ class="ml-auto flex size-6 items-center justify-center rounded bg-gray-50 text-xs hover:bg-gray-100 dark:bg-gray-800 dark:hover:bg-gray-700"
60
+ on:click={() => dispatch("close", conversation.model.id)}
61
+ >
62
+
63
+ </button>
64
+ <button
65
+ class="group relative flex size-6 items-center justify-center rounded bg-gray-50 hover:bg-gray-100 dark:bg-gray-800 dark:hover:bg-gray-700"
66
+ >
67
+ <IconCog />
68
+ <GenerationConfig
69
+ bind:conversation
70
+ classNames="absolute top-7 min-w-[200px] right-3 bg-white dark:bg-gray-900 p-4 rounded-xl border border-gray-200 dark:border-gray-600 hidden group-focus:flex hover:flex"
71
+ />
72
+ </button>
73
+ </div>
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;