mishig HF staff commited on
Commit
1189124
Β·
unverified Β·
1 Parent(s): ce34835

Restructure to have `InferencePlayground.svelte` component (#23)

Browse files

Restructure to have InferencePlayground.svelte component so that it will
be easier to move this into moon-landing

src/lib/components/InferencePlayground/InferencePlayground.svelte ADDED
@@ -0,0 +1,436 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <script lang="ts">
2
+ import {
3
+ createHfInference,
4
+ prepareRequestMessages,
5
+ handleStreamingResponse,
6
+ handleNonStreamingResponse
7
+ } from './inferencePlaygroundUtils';
8
+ import PlaygroundOptions from './InferencePlaygroundGenerationConfig.svelte';
9
+ import PlaygroundTokenModal from './InferencePlaygroundHFTokenModal.svelte';
10
+ import PlaygroundModelSelector from './InferencePlaygroundModelSelector.svelte';
11
+ import Conversation from './InferencePlaygroundConversation.svelte';
12
+ import { onMount } from 'svelte';
13
+ import { type ModelEntry } from '@huggingface/hub';
14
+ import { type ChatCompletionInputMessage } from '@huggingface/tasks';
15
+
16
+ let compatibleModels: ModelEntry[] = [];
17
+
18
+ const startMessages: ChatCompletionInputMessage[] = [{ role: 'user', content: '' }];
19
+
20
+ let conversations: Conversation[] = [
21
+ {
22
+ id: String(Math.random()),
23
+ model: '01-ai/Yi-1.5-34B-Chat',
24
+ config: { temperature: 0.5, maxTokens: 2048, streaming: true, jsonMode: false },
25
+ messages: startMessages
26
+ }
27
+ ];
28
+
29
+ $: if (conversations.length > 1) {
30
+ viewCode = false;
31
+ }
32
+
33
+ let systemMessage: ChatCompletionInputMessage = { role: 'system', content: '' };
34
+ let hfToken: string | null = import.meta.env.VITE_HF_TOKEN;
35
+ let viewCode = false;
36
+ let showTokenModal = false;
37
+ let loading = false;
38
+ let tokens = 0;
39
+ let latency = 0;
40
+ let abortControllers: AbortController[] = [];
41
+ let waitForNonStreaming = true;
42
+
43
+ onMount(() => {
44
+ (async () => {
45
+ // TODO: use hfjs.hub listModels after https://github.com/huggingface/huggingface.js/pull/795
46
+ const res = await fetch(
47
+ 'https://huggingface.co/api/models?pipeline_tag=text-generation&inference=Warm&filter=conversational'
48
+ );
49
+ compatibleModels = (await res.json()) as ModelEntry[];
50
+ compatibleModels.sort((a, b) => a.id.toLowerCase().localeCompare(b.id.toLowerCase()));
51
+ })();
52
+
53
+ return () => {
54
+ for (const abortController of abortControllers) {
55
+ abortController.abort();
56
+ }
57
+ };
58
+ });
59
+
60
+ function addMessage() {
61
+ conversations = conversations.map((conversation) => {
62
+ conversation.messages = [
63
+ ...conversation.messages,
64
+ {
65
+ role: conversation.messages.at(-1)?.role === 'user' ? 'assistant' : 'user',
66
+ content: ''
67
+ }
68
+ ];
69
+ return conversation;
70
+ });
71
+ }
72
+
73
+ function updateMessage(value: string, conversationIdx: number, messageIdx: number) {
74
+ const lastMsgIdx = conversations[0].messages.length - 1;
75
+ const msg = conversations[conversationIdx].messages[messageIdx];
76
+ msg.content = value;
77
+ const { role } = msg;
78
+ if (messageIdx === lastMsgIdx && role === 'user') {
79
+ conversations = conversations.map((conversation) => {
80
+ conversation.messages[messageIdx].content = value;
81
+ return conversation;
82
+ });
83
+ }
84
+ conversations = conversations;
85
+ }
86
+
87
+ function deleteAndGetItem<T>(array: T[], index: number) {
88
+ if (index >= 0 && index < array.length) {
89
+ return array.splice(index, 1)[0];
90
+ }
91
+ return undefined;
92
+ }
93
+
94
+ function deleteMessage(idx: number) {
95
+ conversations = conversations.map((conversation) => {
96
+ const deletedMsg = deleteAndGetItem<ChatCompletionInputMessage>(conversation.messages, idx);
97
+ // delete messages in user/assistant pairs. otherwise, the chat template will be broken
98
+ if (deletedMsg) {
99
+ const { role } = deletedMsg;
100
+ const pairIdx = role === 'user' ? idx : idx - 1;
101
+ deleteAndGetItem<ChatCompletionInputMessage>(conversation.messages, pairIdx);
102
+ }
103
+ return conversation;
104
+ });
105
+ }
106
+
107
+ function deleteConversation(idx: number) {
108
+ deleteAndGetItem(conversations, idx);
109
+ conversations = conversations;
110
+ }
111
+
112
+ function reset() {
113
+ systemMessage.content = '';
114
+ conversations = conversations.map((conversation) => {
115
+ conversation.messages = [...startMessages];
116
+ return conversation;
117
+ });
118
+ }
119
+
120
+ function abort() {
121
+ if (abortControllers.length) {
122
+ for (const abortController of abortControllers) {
123
+ abortController.abort();
124
+ }
125
+ abortControllers = [];
126
+ }
127
+ loading = false;
128
+ waitForNonStreaming = false;
129
+ }
130
+
131
+ async function runInference(conversation: Conversation) {
132
+ const startTime = performance.now();
133
+ const hf = createHfInference(hfToken);
134
+ const requestMessages = prepareRequestMessages(systemMessage, conversation.messages);
135
+
136
+ if (conversation.config.streaming) {
137
+ const streamingMessage = { role: 'assistant', content: '' };
138
+ conversation.messages = [...conversation.messages, streamingMessage];
139
+ const abortController = new AbortController();
140
+ abortControllers.push(abortController);
141
+
142
+ await handleStreamingResponse(
143
+ hf,
144
+ conversation.model,
145
+ requestMessages,
146
+ conversation.config.temperature,
147
+ conversation.config.maxTokens,
148
+ conversation.config.jsonMode,
149
+ (content) => {
150
+ if (streamingMessage) {
151
+ streamingMessage.content = content;
152
+ conversation.messages = [...conversation.messages];
153
+ conversations = conversations;
154
+ }
155
+ },
156
+ abortController
157
+ );
158
+ } else {
159
+ waitForNonStreaming = true;
160
+ const newMessage = await handleNonStreamingResponse(
161
+ hf,
162
+ conversation.model,
163
+ requestMessages,
164
+ conversation.config.temperature,
165
+ conversation.config.maxTokens,
166
+ conversation.config.jsonMode
167
+ );
168
+ // check if the user did not abort the request
169
+ if (waitForNonStreaming) {
170
+ conversation.messages = [...conversation.messages, newMessage];
171
+ conversations = conversations;
172
+ }
173
+ }
174
+
175
+ const endTime = performance.now();
176
+ latency = Math.round(endTime - startTime);
177
+ }
178
+
179
+ async function submit() {
180
+ // // last message has to be from user
181
+ // if (currentConversation.messages?.at(-1)?.role !== 'user') {
182
+ // addMessage();
183
+ // return;
184
+ // }
185
+ if (!hfToken) {
186
+ showTokenModal = true;
187
+ return;
188
+ }
189
+ (document.activeElement as HTMLElement).blur();
190
+ loading = true;
191
+
192
+ try {
193
+ const promises = conversations.map((conversation) => runInference(conversation));
194
+ await Promise.all(promises);
195
+ addMessage();
196
+ } catch (error) {
197
+ if (error.name !== 'AbortError') {
198
+ alert('error: ' + (error as Error).message);
199
+ }
200
+ } finally {
201
+ loading = false;
202
+ abortControllers = [];
203
+ }
204
+ }
205
+
206
+ function onKeydown(event: KeyboardEvent) {
207
+ if (!event.shiftKey && event.key === 'Enter') {
208
+ submit();
209
+ }
210
+ }
211
+ </script>
212
+
213
+ <svelte:window on:keydown={onKeydown} />
214
+
215
+ {#if showTokenModal}
216
+ <PlaygroundTokenModal
217
+ on:close={() => (showTokenModal = false)}
218
+ on:submit={(e) => {
219
+ const formData = new FormData(e.target);
220
+ hfToken = formData.get('hf-token');
221
+ submit();
222
+ showTokenModal = false;
223
+ }}
224
+ />
225
+ {/if}
226
+
227
+ <div
228
+ class="w-dvh grid divide-gray-200 overflow-hidden bg-gray-100/50 max-md:divide-y md:h-dvh dark:[color-scheme:dark]
229
+ {conversations.length === 1
230
+ ? 'md:grid-cols-[clamp(220px,20%,350px),minmax(0,1fr),clamp(270px,25%,300px)]'
231
+ : 'md:grid-cols-[clamp(220px,20%,350px),minmax(0,1fr),0]'}
232
+
233
+ dark:divide-gray-800 dark:bg-gray-900 dark:text-gray-300"
234
+ >
235
+ <div class=" flex flex-col overflow-y-auto py-3 pr-3">
236
+ <div
237
+ class="relative flex flex-1 flex-col gap-6 overflow-y-hidden rounded-r-xl border-x border-y border-gray-200/80 bg-gradient-to-b from-white via-white p-3 shadow-sm dark:border-white/5 dark:from-gray-800/40 dark:via-gray-800/40"
238
+ >
239
+ <div class="pb-2 text-sm font-semibold">SYSTEM</div>
240
+ <textarea
241
+ name=""
242
+ id=""
243
+ placeholder="Enter a custom prompt"
244
+ bind:value={systemMessage.content}
245
+ class="absolute inset-x-0 bottom-0 h-full resize-none bg-transparent px-3 pt-10 text-sm outline-none"
246
+ ></textarea>
247
+ </div>
248
+ </div>
249
+ <div class="relative divide-y divide-gray-200 pt-3 dark:divide-gray-800">
250
+ <div
251
+ class="flex h-[calc(100dvh-5rem)] divide-x divide-gray-200 {conversations.length === 2
252
+ ? '*:w-1/2'
253
+ : conversations.length == 3
254
+ ? '*:w-1/3'
255
+ : '*:w-full'} dark:divide-gray-800"
256
+ >
257
+ {#each conversations as conversation, index}
258
+ <Conversation
259
+ {loading}
260
+ {conversation}
261
+ {index}
262
+ {viewCode}
263
+ sideBySide={conversations.length > 1}
264
+ on:addMessage={addMessage}
265
+ on:messageValueChanged={(e) => {
266
+ const { conversationIdx, messageIdx, value } = e.detail;
267
+ updateMessage(value, conversationIdx, messageIdx);
268
+ }}
269
+ on:deleteMessage={(e) => deleteMessage(e.detail)}
270
+ on:deleteConversation={(e) => deleteConversation(e.detail)}
271
+ />
272
+ {/each}
273
+ </div>
274
+ <div
275
+ class="fixed inset-x-0 bottom-0 flex h-20 items-center gap-2 overflow-hidden whitespace-nowrap px-3 md:absolute"
276
+ >
277
+ <button
278
+ type="button"
279
+ class="flex h-[39px] flex-none 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"
280
+ >
281
+ <div
282
+ class="flex size-5 items-center justify-center rounded border border-black/5 bg-black/5 text-xs"
283
+ >
284
+ <svg
285
+ width="1em"
286
+ height="1em"
287
+ viewBox="0 0 24 25"
288
+ fill="none"
289
+ xmlns="http://www.w3.org/2000/svg"
290
+ >
291
+ <path
292
+ fill-rule="evenodd"
293
+ clip-rule="evenodd"
294
+ d="M5.41 9.41L4 8L12 0L20 8L18.59 9.41L13 3.83L13 17.5H11L11 3.83L5.41 9.41ZM22 17.5V23H2V17.5H0V23C0 23.5304 0.210714 24.0391 0.585786 24.4142C0.960859 24.7893 1.46957 25 2 25H22C22.5304 25 23.0391 24.7893 23.4142 24.4142C23.7893 24.0391 24 23.5304 24 23V17.5H22Z"
295
+ fill="currentColor"
296
+ />
297
+ </svg>
298
+ </div>
299
+
300
+ Share</button
301
+ >
302
+
303
+ <button
304
+ type="button"
305
+ on:click={reset}
306
+ 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"
307
+ ><svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 32 32"
308
+ ><path fill="currentColor" d="M12 12h2v12h-2zm6 0h2v12h-2z" /><path
309
+ fill="currentColor"
310
+ d="M4 6v2h2v20a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2V8h2V6zm4 22V8h16v20zm4-26h8v2h-8z"
311
+ /></svg
312
+ ></button
313
+ >
314
+ <div class="flex-1 items-center justify-center text-center text-sm text-gray-500">
315
+ <span class="max-xl:hidden">0 tokens Β· Latency {latency}ms</span>
316
+ </div>
317
+ <button
318
+ type="button"
319
+ on:click={() => (viewCode = !viewCode)}
320
+ 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"
321
+ >
322
+ <svg
323
+ xmlns="http://www.w3.org/2000/svg"
324
+ width="1em"
325
+ height="1em"
326
+ class="text-base"
327
+ viewBox="0 0 32 32"
328
+ ><path
329
+ fill="currentColor"
330
+ d="m31 16l-7 7l-1.41-1.41L28.17 16l-5.58-5.59L24 9l7 7zM1 16l7-7l1.41 1.41L3.83 16l5.58 5.59L8 23l-7-7zm11.42 9.484L17.64 6l1.932.517L14.352 26z"
331
+ /></svg
332
+ >
333
+ {!viewCode ? 'View Code' : 'Hide Code'}</button
334
+ >
335
+ <button
336
+ on:click={() => {
337
+ viewCode = false;
338
+ loading ? abort() : submit();
339
+ }}
340
+ type="button"
341
+ 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
342
+ ? 'bg-red-900 hover:bg-red-800 dark:bg-red-600 dark:hover:bg-red-700'
343
+ : 'bg-black hover:bg-gray-900 dark:bg-blue-600 dark:hover:bg-blue-700'}"
344
+ >
345
+ {#if loading}
346
+ <div class="flex flex-none items-center gap-[3px]">
347
+ <span class="mr-2">Cancel</span>
348
+ <div
349
+ class="h-1 w-1 flex-none animate-bounce rounded-full bg-gray-500 dark:bg-gray-100"
350
+ style="animation-delay: 0.25s;"
351
+ />
352
+ <div
353
+ class="h-1 w-1 flex-none animate-bounce rounded-full bg-gray-500 dark:bg-gray-100"
354
+ style="animation-delay: 0.5s;"
355
+ />
356
+ <div
357
+ class="h-1 w-1 flex-none animate-bounce rounded-full bg-gray-500 dark:bg-gray-100"
358
+ style="animation-delay: 0.75s;"
359
+ />
360
+ </div>
361
+ {:else}
362
+ Run <span
363
+ class="inline-flex gap-0.5 rounded border border-white/20 bg-white/10 px-0.5 text-xs text-white/70"
364
+ >↡</span
365
+ >
366
+ {/if}
367
+ </button>
368
+ </div>
369
+ </div>
370
+ {#if conversations.length === 1}
371
+ <div class="flex flex-col p-3">
372
+ <div
373
+ class="flex flex-1 flex-col gap-6 overflow-y-hidden rounded-xl border border-gray-200/80 bg-gradient-to-b from-white via-white p-3 shadow-sm dark:border-white/5 dark:from-gray-800/40 dark:via-gray-800/40"
374
+ >
375
+ <PlaygroundModelSelector {compatibleModels} bind:currentModel={conversations[0].model} />
376
+ <div
377
+ 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"
378
+ >
379
+ Compare with...
380
+ <svg
381
+ class="ml-0.5 flex-none opacity-50 group-hover:opacity-100"
382
+ xmlns="http://www.w3.org/2000/svg"
383
+ xmlns:xlink="http://www.w3.org/1999/xlink"
384
+ aria-hidden="true"
385
+ role="img"
386
+ width="1em"
387
+ height="1em"
388
+ preserveAspectRatio="xMidYMid meet"
389
+ viewBox="0 0 24 24"
390
+ ><path
391
+ d="M16.293 9.293L12 13.586L7.707 9.293l-1.414 1.414L12 16.414l5.707-5.707z"
392
+ fill="currentColor"
393
+ ></path></svg
394
+ >
395
+ <select
396
+ class="absolute inset-0 border-none bg-white text-base opacity-0 outline-none"
397
+ on:change|preventDefault={(e) => {
398
+ conversations = [
399
+ ...conversations,
400
+ {
401
+ id: String(Math.random()),
402
+ model: e.target.value,
403
+ config: { temperature: 0.5, maxTokens: 2048, streaming: true, jsonMode: false },
404
+ messages: [...conversations[0].messages]
405
+ }
406
+ ];
407
+ }}
408
+ >
409
+ {#each compatibleModels as model}
410
+ <option value={model.id}>{model.id}</option>
411
+ {/each}
412
+ </select>
413
+ </div>
414
+
415
+ <PlaygroundOptions bind:config={conversations[0].config} />
416
+ <div class="mt-auto">
417
+ <div class="mb-3 flex items-center justify-between gap-2">
418
+ <label
419
+ for="default-range"
420
+ class="block text-sm font-medium text-gray-900 dark:text-white">API Quota</label
421
+ >
422
+ <span
423
+ 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"
424
+ >Free</span
425
+ >
426
+
427
+ <div class="ml-auto w-12 text-right text-sm">76%</div>
428
+ </div>
429
+ <div class="h-2 w-full rounded-full bg-gray-200 dark:bg-gray-700">
430
+ <div class="h-2 rounded-full bg-black dark:bg-gray-400" style="width: 75%"></div>
431
+ </div>
432
+ </div>
433
+ </div>
434
+ </div>
435
+ {/if}
436
+ </div>
src/lib/components/{CodeSnippets.svelte β†’ InferencePlayground/InferencePlaygroundCodeSnippets.svelte} RENAMED
@@ -124,7 +124,7 @@ output = ""
124
 
125
  messages = ${messagesStr}
126
 
127
- for token in inference_client.chat_completion(messages, stream=True, temperature=${conversation.config.temperature}, max_tokens=${conversation.config.maxTokens}):
128
  new_content = token.choices[0].delta.content
129
  print(new_content, end="")
130
  output += new_content`
 
124
 
125
  messages = ${messagesStr}
126
 
127
+ for token in client.chat_completion(messages, stream=True, temperature=${conversation.config.temperature}, max_tokens=${conversation.config.maxTokens}):
128
  new_content = token.choices[0].delta.content
129
  print(new_content, end="")
130
  output += new_content`
src/lib/components/{Conversation.svelte β†’ InferencePlayground/InferencePlaygroundConversation.svelte} RENAMED
@@ -1,8 +1,8 @@
1
  <script lang="ts">
2
  import { createEventDispatcher } from 'svelte';
3
- import CodeSnippets from '$lib/components/CodeSnippets.svelte';
4
- import Message from '$lib/components/Message.svelte';
5
- import PlaygroundOptions from '$lib/components/GenerationConfig.svelte';
6
 
7
  export let loading;
8
  export let conversation;
 
1
  <script lang="ts">
2
  import { createEventDispatcher } from 'svelte';
3
+ import CodeSnippets from './InferencePlaygroundCodeSnippets.svelte';
4
+ import Message from './InferencePlaygroundMessage.svelte';
5
+ import PlaygroundOptions from './InferencePlaygroundGenerationConfig.svelte';
6
 
7
  export let loading;
8
  export let conversation;
src/lib/components/{GenerationConfig.svelte β†’ InferencePlayground/InferencePlaygroundGenerationConfig.svelte} RENAMED
File without changes
src/lib/components/{HFTokenModal.svelte β†’ InferencePlayground/InferencePlaygroundHFTokenModal.svelte} RENAMED
File without changes
src/lib/components/{Message.svelte β†’ InferencePlayground/InferencePlaygroundMessage.svelte} RENAMED
File without changes
src/lib/components/{ModelSelector.svelte β†’ InferencePlayground/InferencePlaygroundModelSelector.svelte} RENAMED
File without changes
src/lib/components/{playgroundUtils.ts β†’ InferencePlayground/inferencePlaygroundUtils.ts} RENAMED
File without changes
src/routes/+page.svelte CHANGED
@@ -1,436 +1,5 @@
1
  <script lang="ts">
2
- import {
3
- createHfInference,
4
- prepareRequestMessages,
5
- handleStreamingResponse,
6
- handleNonStreamingResponse
7
- } from '$lib/components/playgroundUtils';
8
- import PlaygroundOptions from '$lib/components/GenerationConfig.svelte';
9
- import PlaygroundTokenModal from '$lib/components/HFTokenModal.svelte';
10
- import PlaygroundModelSelector from '$lib/components/ModelSelector.svelte';
11
- import Conversation from '$lib/components/Conversation.svelte';
12
- import { onMount } from 'svelte';
13
- import { type ModelEntry } from '@huggingface/hub';
14
- import { type ChatCompletionInputMessage } from '@huggingface/tasks';
15
-
16
- let compatibleModels: ModelEntry[] = [];
17
-
18
- const startMessages: ChatCompletionInputMessage[] = [{ role: 'user', content: '' }];
19
-
20
- let conversations: Conversation[] = [
21
- {
22
- id: String(Math.random()),
23
- model: '01-ai/Yi-1.5-34B-Chat',
24
- config: { temperature: 0.5, maxTokens: 2048, streaming: true, jsonMode: false },
25
- messages: startMessages
26
- }
27
- ];
28
-
29
- $: if (conversations.length > 1) {
30
- viewCode = false;
31
- }
32
-
33
- let systemMessage: ChatCompletionInputMessage = { role: 'system', content: '' };
34
- let hfToken: string | null = import.meta.env.VITE_HF_TOKEN;
35
- let viewCode = false;
36
- let showTokenModal = false;
37
- let loading = false;
38
- let tokens = 0;
39
- let latency = 0;
40
- let abortControllers: AbortController[] = [];
41
- let waitForNonStreaming = true;
42
-
43
- onMount(() => {
44
- (async () => {
45
- // TODO: use hfjs.hub listModels after https://github.com/huggingface/huggingface.js/pull/795
46
- const res = await fetch(
47
- 'https://huggingface.co/api/models?pipeline_tag=text-generation&inference=Warm&filter=conversational'
48
- );
49
- compatibleModels = (await res.json()) as ModelEntry[];
50
- compatibleModels.sort((a, b) => a.id.toLowerCase().localeCompare(b.id.toLowerCase()));
51
- })();
52
-
53
- return () => {
54
- for (const abortController of abortControllers) {
55
- abortController.abort();
56
- }
57
- };
58
- });
59
-
60
- function addMessage() {
61
- conversations = conversations.map((conversation) => {
62
- conversation.messages = [
63
- ...conversation.messages,
64
- {
65
- role: conversation.messages.at(-1)?.role === 'user' ? 'assistant' : 'user',
66
- content: ''
67
- }
68
- ];
69
- return conversation;
70
- });
71
- }
72
-
73
- function updateMessage(value: string, conversationIdx: number, messageIdx: number) {
74
- const lastMsgIdx = conversations[0].messages.length - 1;
75
- const msg = conversations[conversationIdx].messages[messageIdx];
76
- msg.content = value;
77
- const { role } = msg;
78
- if (messageIdx === lastMsgIdx && role === 'user') {
79
- conversations = conversations.map((conversation) => {
80
- conversation.messages[messageIdx].content = value;
81
- return conversation;
82
- });
83
- }
84
- conversations = conversations;
85
- }
86
-
87
- function deleteAndGetItem<T>(array: T[], index: number) {
88
- if (index >= 0 && index < array.length) {
89
- return array.splice(index, 1)[0];
90
- }
91
- return undefined;
92
- }
93
-
94
- function deleteMessage(idx: number) {
95
- conversations = conversations.map((conversation) => {
96
- const deletedMsg = deleteAndGetItem<ChatCompletionInputMessage>(conversation.messages, idx);
97
- // delete messages in user/assistant pairs. otherwise, the chat template will be broken
98
- if (deletedMsg) {
99
- const { role } = deletedMsg;
100
- const pairIdx = role === 'user' ? idx : idx - 1;
101
- deleteAndGetItem<ChatCompletionInputMessage>(conversation.messages, pairIdx);
102
- }
103
- return conversation;
104
- });
105
- }
106
-
107
- function deleteConversation(idx: number) {
108
- deleteAndGetItem(conversations, idx);
109
- conversations = conversations;
110
- }
111
-
112
- function reset() {
113
- systemMessage.content = '';
114
- conversations = conversations.map((conversation) => {
115
- conversation.messages = [...startMessages];
116
- return conversation;
117
- });
118
- }
119
-
120
- function abort() {
121
- if (abortControllers.length) {
122
- for (const abortController of abortControllers) {
123
- abortController.abort();
124
- }
125
- abortControllers = [];
126
- }
127
- loading = false;
128
- waitForNonStreaming = false;
129
- }
130
-
131
- async function runInference(conversation: Conversation) {
132
- const startTime = performance.now();
133
- const hf = createHfInference(hfToken);
134
- const requestMessages = prepareRequestMessages(systemMessage, conversation.messages);
135
-
136
- if (conversation.config.streaming) {
137
- const streamingMessage = { role: 'assistant', content: '' };
138
- conversation.messages = [...conversation.messages, streamingMessage];
139
- const abortController = new AbortController();
140
- abortControllers.push(abortController);
141
-
142
- await handleStreamingResponse(
143
- hf,
144
- conversation.model,
145
- requestMessages,
146
- conversation.config.temperature,
147
- conversation.config.maxTokens,
148
- conversation.config.jsonMode,
149
- (content) => {
150
- if (streamingMessage) {
151
- streamingMessage.content = content;
152
- conversation.messages = [...conversation.messages];
153
- conversations = conversations;
154
- }
155
- },
156
- abortController
157
- );
158
- } else {
159
- waitForNonStreaming = true;
160
- const newMessage = await handleNonStreamingResponse(
161
- hf,
162
- conversation.model,
163
- requestMessages,
164
- conversation.config.temperature,
165
- conversation.config.maxTokens,
166
- conversation.config.jsonMode
167
- );
168
- // check if the user did not abort the request
169
- if (waitForNonStreaming) {
170
- conversation.messages = [...conversation.messages, newMessage];
171
- conversations = conversations;
172
- }
173
- }
174
-
175
- const endTime = performance.now();
176
- latency = Math.round(endTime - startTime);
177
- }
178
-
179
- async function submit() {
180
- // // last message has to be from user
181
- // if (currentConversation.messages?.at(-1)?.role !== 'user') {
182
- // addMessage();
183
- // return;
184
- // }
185
- if (!hfToken) {
186
- showTokenModal = true;
187
- return;
188
- }
189
- (document.activeElement as HTMLElement).blur();
190
- loading = true;
191
-
192
- try {
193
- const promises = conversations.map((conversation) => runInference(conversation));
194
- await Promise.all(promises);
195
- addMessage();
196
- } catch (error) {
197
- if (error.name !== 'AbortError') {
198
- alert('error: ' + (error as Error).message);
199
- }
200
- } finally {
201
- loading = false;
202
- abortControllers = [];
203
- }
204
- }
205
-
206
- function onKeydown(event: KeyboardEvent) {
207
- if (!event.shiftKey && event.key === 'Enter') {
208
- submit();
209
- }
210
- }
211
  </script>
212
 
213
- <svelte:window on:keydown={onKeydown} />
214
-
215
- {#if showTokenModal}
216
- <PlaygroundTokenModal
217
- on:close={() => (showTokenModal = false)}
218
- on:submit={(e) => {
219
- const formData = new FormData(e.target);
220
- hfToken = formData.get('hf-token');
221
- submit();
222
- showTokenModal = false;
223
- }}
224
- />
225
- {/if}
226
-
227
- <div
228
- class="w-dvh grid divide-gray-200 overflow-hidden bg-gray-100/50 max-md:divide-y md:h-dvh dark:[color-scheme:dark]
229
- {conversations.length === 1
230
- ? 'md:grid-cols-[clamp(220px,20%,350px),minmax(0,1fr),clamp(270px,25%,300px)]'
231
- : 'md:grid-cols-[clamp(220px,20%,350px),minmax(0,1fr),0]'}
232
-
233
- dark:divide-gray-800 dark:bg-gray-900 dark:text-gray-300"
234
- >
235
- <div class=" flex flex-col overflow-y-auto py-3 pr-3">
236
- <div
237
- class="relative flex flex-1 flex-col gap-6 overflow-y-hidden rounded-r-xl border-x border-y border-gray-200/80 bg-gradient-to-b from-white via-white p-3 shadow-sm dark:border-white/5 dark:from-gray-800/40 dark:via-gray-800/40"
238
- >
239
- <div class="pb-2 text-sm font-semibold">SYSTEM</div>
240
- <textarea
241
- name=""
242
- id=""
243
- placeholder="Enter a custom prompt"
244
- bind:value={systemMessage.content}
245
- class="absolute inset-x-0 bottom-0 h-full resize-none bg-transparent px-3 pt-10 text-sm outline-none"
246
- ></textarea>
247
- </div>
248
- </div>
249
- <div class="relative divide-y divide-gray-200 pt-3 dark:divide-gray-800">
250
- <div
251
- class="flex h-[calc(100dvh-5rem)] divide-x divide-gray-200 {conversations.length === 2
252
- ? '*:w-1/2'
253
- : conversations.length == 3
254
- ? '*:w-1/3'
255
- : '*:w-full'} dark:divide-gray-800"
256
- >
257
- {#each conversations as conversation, index}
258
- <Conversation
259
- {loading}
260
- {conversation}
261
- {index}
262
- {viewCode}
263
- sideBySide={conversations.length > 1}
264
- on:addMessage={addMessage}
265
- on:messageValueChanged={(e) => {
266
- const { conversationIdx, messageIdx, value } = e.detail;
267
- updateMessage(value, conversationIdx, messageIdx);
268
- }}
269
- on:deleteMessage={(e) => deleteMessage(e.detail)}
270
- on:deleteConversation={(e) => deleteConversation(e.detail)}
271
- />
272
- {/each}
273
- </div>
274
- <div
275
- class="fixed inset-x-0 bottom-0 flex h-20 items-center gap-2 overflow-hidden whitespace-nowrap px-3 md:absolute"
276
- >
277
- <button
278
- type="button"
279
- class="flex h-[39px] flex-none 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"
280
- >
281
- <div
282
- class="flex size-5 items-center justify-center rounded border border-black/5 bg-black/5 text-xs"
283
- >
284
- <svg
285
- width="1em"
286
- height="1em"
287
- viewBox="0 0 24 25"
288
- fill="none"
289
- xmlns="http://www.w3.org/2000/svg"
290
- >
291
- <path
292
- fill-rule="evenodd"
293
- clip-rule="evenodd"
294
- d="M5.41 9.41L4 8L12 0L20 8L18.59 9.41L13 3.83L13 17.5H11L11 3.83L5.41 9.41ZM22 17.5V23H2V17.5H0V23C0 23.5304 0.210714 24.0391 0.585786 24.4142C0.960859 24.7893 1.46957 25 2 25H22C22.5304 25 23.0391 24.7893 23.4142 24.4142C23.7893 24.0391 24 23.5304 24 23V17.5H22Z"
295
- fill="currentColor"
296
- />
297
- </svg>
298
- </div>
299
-
300
- Share</button
301
- >
302
-
303
- <button
304
- type="button"
305
- on:click={reset}
306
- 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"
307
- ><svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 32 32"
308
- ><path fill="currentColor" d="M12 12h2v12h-2zm6 0h2v12h-2z" /><path
309
- fill="currentColor"
310
- d="M4 6v2h2v20a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2V8h2V6zm4 22V8h16v20zm4-26h8v2h-8z"
311
- /></svg
312
- ></button
313
- >
314
- <div class="flex-1 items-center justify-center text-center text-sm text-gray-500">
315
- <span class="max-xl:hidden">0 tokens Β· Latency {latency}ms</span>
316
- </div>
317
- <button
318
- type="button"
319
- on:click={() => (viewCode = !viewCode)}
320
- 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"
321
- >
322
- <svg
323
- xmlns="http://www.w3.org/2000/svg"
324
- width="1em"
325
- height="1em"
326
- class="text-base"
327
- viewBox="0 0 32 32"
328
- ><path
329
- fill="currentColor"
330
- d="m31 16l-7 7l-1.41-1.41L28.17 16l-5.58-5.59L24 9l7 7zM1 16l7-7l1.41 1.41L3.83 16l5.58 5.59L8 23l-7-7zm11.42 9.484L17.64 6l1.932.517L14.352 26z"
331
- /></svg
332
- >
333
- {!viewCode ? 'View Code' : 'Hide Code'}</button
334
- >
335
- <button
336
- on:click={() => {
337
- viewCode = false;
338
- loading ? abort() : submit();
339
- }}
340
- type="button"
341
- 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
342
- ? 'bg-red-900 hover:bg-red-800 dark:bg-red-600 dark:hover:bg-red-700'
343
- : 'bg-black hover:bg-gray-900 dark:bg-blue-600 dark:hover:bg-blue-700'}"
344
- >
345
- {#if loading}
346
- <div class="flex flex-none items-center gap-[3px]">
347
- <span class="mr-2">Cancel</span>
348
- <div
349
- class="h-1 w-1 flex-none animate-bounce rounded-full bg-gray-500 dark:bg-gray-100"
350
- style="animation-delay: 0.25s;"
351
- />
352
- <div
353
- class="h-1 w-1 flex-none animate-bounce rounded-full bg-gray-500 dark:bg-gray-100"
354
- style="animation-delay: 0.5s;"
355
- />
356
- <div
357
- class="h-1 w-1 flex-none animate-bounce rounded-full bg-gray-500 dark:bg-gray-100"
358
- style="animation-delay: 0.75s;"
359
- />
360
- </div>
361
- {:else}
362
- Run <span
363
- class="inline-flex gap-0.5 rounded border border-white/20 bg-white/10 px-0.5 text-xs text-white/70"
364
- >↡</span
365
- >
366
- {/if}
367
- </button>
368
- </div>
369
- </div>
370
- {#if conversations.length === 1}
371
- <div class="flex flex-col p-3">
372
- <div
373
- class="flex flex-1 flex-col gap-6 overflow-y-hidden rounded-xl border border-gray-200/80 bg-gradient-to-b from-white via-white p-3 shadow-sm dark:border-white/5 dark:from-gray-800/40 dark:via-gray-800/40"
374
- >
375
- <PlaygroundModelSelector {compatibleModels} bind:currentModel={conversations[0].model} />
376
- <div
377
- 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"
378
- >
379
- Compare with...
380
- <svg
381
- class="ml-0.5 flex-none opacity-50 group-hover:opacity-100"
382
- xmlns="http://www.w3.org/2000/svg"
383
- xmlns:xlink="http://www.w3.org/1999/xlink"
384
- aria-hidden="true"
385
- role="img"
386
- width="1em"
387
- height="1em"
388
- preserveAspectRatio="xMidYMid meet"
389
- viewBox="0 0 24 24"
390
- ><path
391
- d="M16.293 9.293L12 13.586L7.707 9.293l-1.414 1.414L12 16.414l5.707-5.707z"
392
- fill="currentColor"
393
- ></path></svg
394
- >
395
- <select
396
- class="absolute inset-0 border-none bg-white text-base opacity-0 outline-none"
397
- on:change|preventDefault={(e) => {
398
- conversations = [
399
- ...conversations,
400
- {
401
- id: String(Math.random()),
402
- model: e.target.value,
403
- config: { temperature: 0.5, maxTokens: 2048, streaming: true, jsonMode: false },
404
- messages: [...conversations[0].messages]
405
- }
406
- ];
407
- }}
408
- >
409
- {#each compatibleModels as model}
410
- <option value={model.id}>{model.id}</option>
411
- {/each}
412
- </select>
413
- </div>
414
-
415
- <PlaygroundOptions bind:config={conversations[0].config} />
416
- <div class="mt-auto">
417
- <div class="mb-3 flex items-center justify-between gap-2">
418
- <label
419
- for="default-range"
420
- class="block text-sm font-medium text-gray-900 dark:text-white">API Quota</label
421
- >
422
- <span
423
- 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"
424
- >Free</span
425
- >
426
-
427
- <div class="ml-auto w-12 text-right text-sm">76%</div>
428
- </div>
429
- <div class="h-2 w-full rounded-full bg-gray-200 dark:bg-gray-700">
430
- <div class="h-2 rounded-full bg-black dark:bg-gray-400" style="width: 75%"></div>
431
- </div>
432
- </div>
433
- </div>
434
- </div>
435
- {/if}
436
- </div>
 
1
  <script lang="ts">
2
+ import InferencePlayground from '$lib/components/InferencePlayground/InferencePlayground.svelte';
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3
  </script>
4
 
5
+ <InferencePlayground />