Thomas G. Lopes
commited on
Commit
·
9b4caaa
1
Parent(s):
73a8db9
migrate stores to runes
Browse files- src/lib/components/debug-menu.svelte +4 -4
- src/lib/components/inference-playground/conversation-header.svelte +3 -3
- src/lib/components/inference-playground/conversation.svelte +1 -2
- src/lib/components/inference-playground/hf-token-modal.svelte +2 -1
- src/lib/components/inference-playground/model-selector-modal.svelte +3 -6
- src/lib/components/inference-playground/model-selector.svelte +2 -2
- src/lib/components/inference-playground/playground.svelte +81 -97
- src/lib/components/inference-playground/project-select.svelte +8 -8
- src/lib/components/toaster.svelte +2 -2
- src/lib/spells/abort-manager.svelte.ts +34 -0
- src/lib/spells/create-init.svelte.ts +14 -0
- src/lib/spells/textarea-autosize.svelte.ts +2 -2
- src/lib/state/models.svelte.ts +2 -1
- src/lib/{stores/session.ts → state/session.svelte.ts} +63 -96
- src/lib/state/token.svelte.ts +7 -7
- src/lib/stores/models.ts +0 -8
- src/lib/stores/mounted.ts +0 -9
- src/lib/stores/token.ts +0 -31
- src/lib/types.ts +4 -2
- src/lib/utils/model.ts +0 -5
src/lib/components/debug-menu.svelte
CHANGED
@@ -1,9 +1,9 @@
|
|
1 |
<script lang="ts">
|
2 |
import { dev } from "$app/environment";
|
3 |
-
import { session } from "$lib/
|
4 |
import { createPopover } from "@melt-ui/svelte";
|
5 |
import { prompt } from "./prompts.svelte";
|
6 |
-
import { token } from "$lib/
|
7 |
import { compareStr } from "$lib/utils/compare.js";
|
8 |
import type { ToastData } from "./toaster.svelte.js";
|
9 |
import { addToast } from "./toaster.svelte.js";
|
@@ -29,7 +29,7 @@
|
|
29 |
{
|
30 |
label: "Log session to console",
|
31 |
cb: () => {
|
32 |
-
console.log(
|
33 |
},
|
34 |
},
|
35 |
{
|
@@ -41,7 +41,7 @@
|
|
41 |
{
|
42 |
label: "Show token modal",
|
43 |
cb: () => {
|
44 |
-
|
45 |
},
|
46 |
},
|
47 |
{
|
|
|
1 |
<script lang="ts">
|
2 |
import { dev } from "$app/environment";
|
3 |
+
import { session } from "$lib/state/session.svelte.js";
|
4 |
import { createPopover } from "@melt-ui/svelte";
|
5 |
import { prompt } from "./prompts.svelte";
|
6 |
+
import { token } from "$lib/state/token.svelte.js";
|
7 |
import { compareStr } from "$lib/utils/compare.js";
|
8 |
import type { ToastData } from "./toaster.svelte.js";
|
9 |
import { addToast } from "./toaster.svelte.js";
|
|
|
29 |
{
|
30 |
label: "Log session to console",
|
31 |
cb: () => {
|
32 |
+
console.log(session.$);
|
33 |
},
|
34 |
},
|
35 |
{
|
|
|
41 |
{
|
42 |
label: "Show token modal",
|
43 |
cb: () => {
|
44 |
+
token.showModal = true;
|
45 |
},
|
46 |
},
|
47 |
{
|
src/lib/components/inference-playground/conversation-header.svelte
CHANGED
@@ -3,9 +3,9 @@
|
|
3 |
|
4 |
import { createEventDispatcher } from "svelte";
|
5 |
|
6 |
-
import { models } from "$lib/
|
7 |
-
import Avatar from "../avatar.svelte";
|
8 |
import IconCog from "~icons/carbon/settings";
|
|
|
9 |
import GenerationConfig from "./generation-config.svelte";
|
10 |
import ModelSelectorModal from "./model-selector-modal.svelte";
|
11 |
import ProviderSelect from "./provider-select.svelte";
|
@@ -22,7 +22,7 @@
|
|
22 |
let modelSelectorOpen = $state(false);
|
23 |
|
24 |
function changeModel(newModelId: ModelWithTokenizer["id"]) {
|
25 |
-
const model =
|
26 |
if (!model) {
|
27 |
return;
|
28 |
}
|
|
|
3 |
|
4 |
import { createEventDispatcher } from "svelte";
|
5 |
|
6 |
+
import { models } from "$lib/state/models.svelte.js";
|
|
|
7 |
import IconCog from "~icons/carbon/settings";
|
8 |
+
import Avatar from "../avatar.svelte";
|
9 |
import GenerationConfig from "./generation-config.svelte";
|
10 |
import ModelSelectorModal from "./model-selector-modal.svelte";
|
11 |
import ProviderSelect from "./provider-select.svelte";
|
|
|
22 |
let modelSelectorOpen = $state(false);
|
23 |
|
24 |
function changeModel(newModelId: ModelWithTokenizer["id"]) {
|
25 |
+
const model = models.all.find(m => m.id === newModelId);
|
26 |
if (!model) {
|
27 |
return;
|
28 |
}
|
src/lib/components/inference-playground/conversation.svelte
CHANGED
@@ -60,8 +60,7 @@ https://svelte.dev/e/bind_invalid_expression -->
|
|
60 |
}
|
61 |
|
62 |
function deleteMessage(idx: number) {
|
63 |
-
conversation.messages.
|
64 |
-
conversation = conversation;
|
65 |
}
|
66 |
</script>
|
67 |
|
|
|
60 |
}
|
61 |
|
62 |
function deleteMessage(idx: number) {
|
63 |
+
conversation.messages = conversation.messages.slice(0, idx);
|
|
|
64 |
}
|
65 |
</script>
|
66 |
|
src/lib/components/inference-playground/hf-token-modal.svelte
CHANGED
@@ -6,6 +6,7 @@
|
|
6 |
import { createEventDispatcher, onDestroy, onMount } from "svelte";
|
7 |
|
8 |
import IconCross from "~icons/carbon/close";
|
|
|
9 |
|
10 |
interface Props {
|
11 |
storeLocallyHfToken?: boolean;
|
@@ -28,7 +29,6 @@
|
|
28 |
|
29 |
onMount(() => {
|
30 |
document.getElementById("app")?.setAttribute("inert", "true");
|
31 |
-
modalEl?.focus();
|
32 |
});
|
33 |
|
34 |
onDestroy(() => {
|
@@ -85,6 +85,7 @@
|
|
85 |
>Hugging Face Token</label
|
86 |
>
|
87 |
<input
|
|
|
88 |
required
|
89 |
placeholder="Enter HF Token"
|
90 |
type="text"
|
|
|
6 |
import { createEventDispatcher, onDestroy, onMount } from "svelte";
|
7 |
|
8 |
import IconCross from "~icons/carbon/close";
|
9 |
+
import { autofocus } from "$lib/actions/autofocus.js";
|
10 |
|
11 |
interface Props {
|
12 |
storeLocallyHfToken?: boolean;
|
|
|
29 |
|
30 |
onMount(() => {
|
31 |
document.getElementById("app")?.setAttribute("inert", "true");
|
|
|
32 |
});
|
33 |
|
34 |
onDestroy(() => {
|
|
|
85 |
>Hugging Face Token</label
|
86 |
>
|
87 |
<input
|
88 |
+
use:autofocus
|
89 |
required
|
90 |
placeholder="Enter HF Token"
|
91 |
type="text"
|
src/lib/components/inference-playground/model-selector-modal.svelte
CHANGED
@@ -3,8 +3,7 @@
|
|
3 |
|
4 |
import { createEventDispatcher, onMount, tick } from "svelte";
|
5 |
|
6 |
-
import { models } from "$lib/
|
7 |
-
import { getTrending } from "$lib/utils/model.js";
|
8 |
import fuzzysearch from "$lib/utils/search.js";
|
9 |
import IconSearch from "~icons/carbon/search";
|
10 |
import IconStar from "~icons/carbon/star";
|
@@ -23,10 +22,8 @@
|
|
23 |
|
24 |
const dispatch = createEventDispatcher<{ modelSelected: string; close: void }>();
|
25 |
|
26 |
-
let
|
27 |
-
|
28 |
-
let featuredModels = $derived(fuzzysearch({ needle: query, haystack: trendingModels, property: "id" }));
|
29 |
-
let otherModels = $derived(fuzzysearch({ needle: query, haystack: $models, property: "id" }));
|
30 |
|
31 |
onMount(() => {
|
32 |
if (featuredModels.findIndex(model => model.id === conversation.model.id) !== -1) {
|
|
|
3 |
|
4 |
import { createEventDispatcher, onMount, tick } from "svelte";
|
5 |
|
6 |
+
import { models } from "$lib/state/models.svelte.js";
|
|
|
7 |
import fuzzysearch from "$lib/utils/search.js";
|
8 |
import IconSearch from "~icons/carbon/search";
|
9 |
import IconStar from "~icons/carbon/star";
|
|
|
22 |
|
23 |
const dispatch = createEventDispatcher<{ modelSelected: string; close: void }>();
|
24 |
|
25 |
+
let featuredModels = $derived(fuzzysearch({ needle: query, haystack: models.trending, property: "id" }));
|
26 |
+
let otherModels = $derived(fuzzysearch({ needle: query, haystack: models.all, property: "id" }));
|
|
|
|
|
27 |
|
28 |
onMount(() => {
|
29 |
if (featuredModels.findIndex(model => model.id === conversation.model.id) !== -1) {
|
src/lib/components/inference-playground/model-selector.svelte
CHANGED
@@ -18,7 +18,7 @@
|
|
18 |
|
19 |
// Model
|
20 |
function changeModel(modelId: ModelWithTokenizer["id"]) {
|
21 |
-
const model = models
|
22 |
if (!model) {
|
23 |
return;
|
24 |
}
|
@@ -34,7 +34,7 @@
|
|
34 |
|
35 |
<div class="flex flex-col gap-2">
|
36 |
<label for={id} class="flex items-baseline gap-2 text-sm font-medium text-gray-900 dark:text-white">
|
37 |
-
Models<span class="text-xs font-normal text-gray-400">{models
|
38 |
</label>
|
39 |
|
40 |
<button
|
|
|
18 |
|
19 |
// Model
|
20 |
function changeModel(modelId: ModelWithTokenizer["id"]) {
|
21 |
+
const model = models.all.find(m => m.id === modelId);
|
22 |
if (!model) {
|
23 |
return;
|
24 |
}
|
|
|
34 |
|
35 |
<div class="flex flex-col gap-2">
|
36 |
<label for={id} class="flex items-baseline gap-2 text-sm font-medium text-gray-900 dark:text-white">
|
37 |
+
Models<span class="text-xs font-normal text-gray-400">{models.all.length}</span>
|
38 |
</label>
|
39 |
|
40 |
<button
|
src/lib/components/inference-playground/playground.svelte
CHANGED
@@ -1,35 +1,35 @@
|
|
1 |
<script lang="ts">
|
2 |
-
import type {
|
3 |
|
4 |
import { handleNonStreamingResponse, handleStreamingResponse, isSystemPromptSupported } from "./utils.js";
|
5 |
|
6 |
-
import {
|
7 |
-
import {
|
8 |
-
import {
|
|
|
9 |
import { isMac } from "$lib/utils/platform.js";
|
10 |
import { HfInference } from "@huggingface/inference";
|
11 |
-
import { onDestroy } from "svelte";
|
12 |
import IconExternal from "~icons/carbon/arrow-up-right";
|
13 |
import IconCode from "~icons/carbon/code";
|
14 |
import IconCompare from "~icons/carbon/compare";
|
15 |
import IconInfo from "~icons/carbon/information";
|
16 |
import { default as IconDelete, default as IconThrashcan } from "~icons/carbon/trash-can";
|
17 |
-
import
|
18 |
import PlaygroundConversationHeader from "./conversation-header.svelte";
|
|
|
19 |
import GenerationConfig from "./generation-config.svelte";
|
20 |
import HFTokenModal from "./hf-token-modal.svelte";
|
21 |
-
import ModelSelector from "./model-selector.svelte";
|
22 |
import ModelSelectorModal from "./model-selector-modal.svelte";
|
|
|
23 |
import ProjectSelect from "./project-select.svelte";
|
24 |
-
import { addToast } from "../toaster.svelte.js";
|
25 |
|
26 |
const startMessageUser: ConversationMessage = { role: "user", content: "" };
|
27 |
|
28 |
let viewCode = $state(false);
|
29 |
let viewSettings = $state(false);
|
30 |
let loading = $state(false);
|
31 |
-
|
32 |
-
|
33 |
let selectCompareModelOpen = $state(false);
|
34 |
|
35 |
interface GenerationStatistics {
|
@@ -37,74 +37,61 @@
|
|
37 |
generatedTokensCount: number;
|
38 |
}
|
39 |
let generationStats = $state(
|
40 |
-
|
41 |
| [GenerationStatistics]
|
42 |
| [GenerationStatistics, GenerationStatistics]
|
43 |
);
|
44 |
|
45 |
let systemPromptSupported = $derived(
|
46 |
-
|
47 |
);
|
48 |
-
let compareActive = $derived(
|
49 |
|
50 |
function reset() {
|
51 |
-
|
52 |
-
|
53 |
-
|
|
|
|
|
|
|
54 |
});
|
55 |
-
$session = $session;
|
56 |
}
|
57 |
|
58 |
-
function
|
59 |
-
|
60 |
-
|
61 |
-
abortController.abort();
|
62 |
-
}
|
63 |
-
abortControllers = [];
|
64 |
-
}
|
65 |
-
loading = false;
|
66 |
-
waitForNonStreaming = false;
|
67 |
-
}
|
68 |
|
69 |
-
async function runInference(conversation: Conversation, conversationIdx: number) {
|
70 |
const startTime = performance.now();
|
71 |
-
const hf = new HfInference(
|
72 |
|
73 |
if (conversation.streaming) {
|
74 |
-
let
|
75 |
-
|
76 |
-
const abortController = new AbortController();
|
77 |
-
abortControllers.push(abortController);
|
78 |
|
79 |
await handleStreamingResponse(
|
80 |
hf,
|
81 |
conversation,
|
82 |
content => {
|
83 |
-
if (streamingMessage)
|
84 |
-
|
85 |
-
|
86 |
-
|
87 |
-
|
88 |
-
}
|
89 |
-
$session = $session;
|
90 |
-
const c = generationStats[conversationIdx];
|
91 |
-
if (c) c.generatedTokensCount += 1;
|
92 |
}
|
|
|
|
|
|
|
93 |
},
|
94 |
-
|
95 |
);
|
96 |
} else {
|
97 |
-
waitForNonStreaming = true;
|
98 |
const { message: newMessage, completion_tokens: newTokensCount } = await handleNonStreamingResponse(
|
99 |
hf,
|
100 |
conversation
|
101 |
);
|
102 |
-
|
103 |
-
|
104 |
-
|
105 |
-
const c = generationStats[conversationIdx];
|
106 |
-
if (c) c.generatedTokensCount += newTokensCount;
|
107 |
-
}
|
108 |
}
|
109 |
|
110 |
const endTime = performance.now();
|
@@ -113,39 +100,39 @@
|
|
113 |
}
|
114 |
|
115 |
async function submit() {
|
116 |
-
if (
|
117 |
-
|
118 |
return;
|
119 |
}
|
120 |
|
121 |
-
for (const [idx, conversation] of
|
122 |
-
if (conversation.messages.at(-1)?.role
|
123 |
-
|
124 |
-
|
125 |
-
|
126 |
-
}
|
127 |
-
return addToast({
|
128 |
-
title: "Failed to run inference",
|
129 |
-
description: `${prefix}Messages must alternate between user/assistant roles.`,
|
130 |
-
variant: "error",
|
131 |
-
});
|
132 |
}
|
|
|
|
|
|
|
|
|
|
|
133 |
}
|
134 |
|
135 |
(document.activeElement as HTMLElement).blur();
|
136 |
loading = true;
|
137 |
|
138 |
try {
|
139 |
-
const promises =
|
140 |
await Promise.all(promises);
|
141 |
} catch (error) {
|
142 |
-
for (const conversation of
|
143 |
if (conversation.messages.at(-1)?.role === "assistant" && !conversation.messages.at(-1)?.content?.trim()) {
|
144 |
conversation.messages.pop();
|
145 |
conversation.messages = [...conversation.messages];
|
146 |
}
|
147 |
-
|
148 |
}
|
|
|
149 |
if (error instanceof Error) {
|
150 |
if (error.message.includes("token seems invalid")) {
|
151 |
token.reset();
|
@@ -158,7 +145,7 @@
|
|
158 |
}
|
159 |
} finally {
|
160 |
loading = false;
|
161 |
-
|
162 |
}
|
163 |
}
|
164 |
|
@@ -174,7 +161,7 @@
|
|
174 |
const submittedHfToken = (formData.get("hf-token") as string).trim() ?? "";
|
175 |
const RE_HF_TOKEN = /\bhf_[a-zA-Z0-9]{34}\b/;
|
176 |
if (RE_HF_TOKEN.test(submittedHfToken)) {
|
177 |
-
token.
|
178 |
submit();
|
179 |
} else {
|
180 |
alert("Please provide a valid HF token.");
|
@@ -182,33 +169,27 @@
|
|
182 |
}
|
183 |
|
184 |
function addCompareModel(modelId: ModelWithTokenizer["id"]) {
|
185 |
-
const model =
|
186 |
-
if (!model ||
|
187 |
return;
|
188 |
}
|
189 |
-
const newConversation = { ...JSON.parse(JSON.stringify(
|
190 |
-
|
191 |
generationStats = [generationStats[0], { latency: 0, generatedTokensCount: 0 }];
|
192 |
}
|
193 |
|
194 |
function removeCompareModal(conversationIdx: number) {
|
195 |
-
|
196 |
-
|
197 |
generationStats.splice(conversationIdx, 1)[0];
|
198 |
generationStats = generationStats;
|
199 |
}
|
200 |
-
|
201 |
-
onDestroy(() => {
|
202 |
-
for (const abortController of abortControllers) {
|
203 |
-
abortController.abort();
|
204 |
-
}
|
205 |
-
});
|
206 |
</script>
|
207 |
|
208 |
-
{#if
|
209 |
<HFTokenModal
|
210 |
-
bind:storeLocallyHfToken={
|
211 |
-
on:close={() => (
|
212 |
on:submit={handleTokenSubmit}
|
213 |
/>
|
214 |
{/if}
|
@@ -235,12 +216,12 @@
|
|
235 |
placeholder={systemPromptSupported
|
236 |
? "Enter a custom prompt"
|
237 |
: "System prompt is not supported with the chosen model."}
|
238 |
-
value={systemPromptSupported ?
|
239 |
oninput={e => {
|
240 |
-
for (const conversation of
|
241 |
conversation.systemMessage.content = e.currentTarget.value;
|
242 |
}
|
243 |
-
|
244 |
}}
|
245 |
class="absolute inset-x-0 bottom-0 h-full resize-none bg-transparent px-3 pt-10 text-sm outline-hidden"
|
246 |
></textarea>
|
@@ -250,18 +231,21 @@
|
|
250 |
<div
|
251 |
class="flex h-[calc(100dvh-5rem-120px)] divide-x divide-gray-200 overflow-x-auto overflow-y-hidden *:w-full max-sm:w-dvw md:h-[calc(100dvh-5rem)] md:pt-3 dark:divide-gray-800"
|
252 |
>
|
253 |
-
{#each
|
254 |
<div class="max-sm:min-w-full">
|
255 |
{#if compareActive}
|
256 |
<PlaygroundConversationHeader
|
257 |
{conversationIdx}
|
258 |
-
bind:conversation={
|
259 |
on:close={() => removeCompareModal(conversationIdx)}
|
260 |
/>
|
261 |
{/if}
|
262 |
<PlaygroundConversation
|
263 |
{loading}
|
264 |
-
bind:conversation={
|
|
|
|
|
|
|
265 |
{viewCode}
|
266 |
{compareActive}
|
267 |
on:closeCode={() => (viewCode = false)}
|
@@ -302,7 +286,7 @@
|
|
302 |
<button
|
303 |
onclick={() => {
|
304 |
viewCode = false;
|
305 |
-
loading ?
|
306 |
}}
|
307 |
type="button"
|
308 |
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:ring-4 focus:ring-gray-300 focus:outline-hidden dark:border-gray-700 dark:focus:ring-gray-700 {loading
|
@@ -312,7 +296,7 @@
|
|
312 |
{#if loading}
|
313 |
<div class="flex flex-none items-center gap-[3px]">
|
314 |
<span class="mr-2">
|
315 |
-
{#if
|
316 |
Stop
|
317 |
{:else}
|
318 |
Cancel
|
@@ -347,7 +331,7 @@
|
|
347 |
class="flex flex-1 flex-col gap-6 overflow-y-hidden rounded-xl border border-gray-200/80 bg-white bg-linear-to-b from-white via-white p-3 shadow-xs dark:border-white/5 dark:bg-gray-900 dark:from-gray-800/40 dark:via-gray-800/40"
|
348 |
>
|
349 |
<div class="flex flex-col gap-2">
|
350 |
-
<ModelSelector bind:conversation={
|
351 |
<div class="flex items-center gap-2 self-end px-2 text-xs whitespace-nowrap">
|
352 |
<button
|
353 |
class="flex items-center gap-0.5 text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-300"
|
@@ -357,8 +341,8 @@
|
|
357 |
Compare
|
358 |
</button>
|
359 |
<a
|
360 |
-
href="https://huggingface.co/{
|
361 |
-
.conversations[0]
|
362 |
target="_blank"
|
363 |
class="flex items-center gap-0.5 text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-300"
|
364 |
>
|
@@ -368,8 +352,8 @@
|
|
368 |
</div>
|
369 |
</div>
|
370 |
|
371 |
-
<GenerationConfig bind:conversation={
|
372 |
-
{#if
|
373 |
<button
|
374 |
onclick={token.reset}
|
375 |
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"
|
@@ -427,7 +411,7 @@
|
|
427 |
|
428 |
{#if selectCompareModelOpen}
|
429 |
<ModelSelectorModal
|
430 |
-
conversation={
|
431 |
on:modelSelected={e => addCompareModel(e.detail)}
|
432 |
on:close={() => (selectCompareModelOpen = false)}
|
433 |
/>
|
|
|
1 |
<script lang="ts">
|
2 |
+
import type { ConversationMessage, ModelWithTokenizer } from "$lib/types.js";
|
3 |
|
4 |
import { handleNonStreamingResponse, handleStreamingResponse, isSystemPromptSupported } from "./utils.js";
|
5 |
|
6 |
+
import { AbortManager } from "$lib/spells/abort-manager.svelte.js";
|
7 |
+
import { models } from "$lib/state/models.svelte.js";
|
8 |
+
import { session } from "$lib/state/session.svelte.js";
|
9 |
+
import { token } from "$lib/state/token.svelte.js";
|
10 |
import { isMac } from "$lib/utils/platform.js";
|
11 |
import { HfInference } from "@huggingface/inference";
|
|
|
12 |
import IconExternal from "~icons/carbon/arrow-up-right";
|
13 |
import IconCode from "~icons/carbon/code";
|
14 |
import IconCompare from "~icons/carbon/compare";
|
15 |
import IconInfo from "~icons/carbon/information";
|
16 |
import { default as IconDelete, default as IconThrashcan } from "~icons/carbon/trash-can";
|
17 |
+
import { addToast } from "../toaster.svelte.js";
|
18 |
import PlaygroundConversationHeader from "./conversation-header.svelte";
|
19 |
+
import PlaygroundConversation from "./conversation.svelte";
|
20 |
import GenerationConfig from "./generation-config.svelte";
|
21 |
import HFTokenModal from "./hf-token-modal.svelte";
|
|
|
22 |
import ModelSelectorModal from "./model-selector-modal.svelte";
|
23 |
+
import ModelSelector from "./model-selector.svelte";
|
24 |
import ProjectSelect from "./project-select.svelte";
|
|
|
25 |
|
26 |
const startMessageUser: ConversationMessage = { role: "user", content: "" };
|
27 |
|
28 |
let viewCode = $state(false);
|
29 |
let viewSettings = $state(false);
|
30 |
let loading = $state(false);
|
31 |
+
|
32 |
+
const abortManager = new AbortManager();
|
33 |
let selectCompareModelOpen = $state(false);
|
34 |
|
35 |
interface GenerationStatistics {
|
|
|
37 |
generatedTokensCount: number;
|
38 |
}
|
39 |
let generationStats = $state(
|
40 |
+
session.project.conversations.map(_ => ({ latency: 0, generatedTokensCount: 0 })) as
|
41 |
| [GenerationStatistics]
|
42 |
| [GenerationStatistics, GenerationStatistics]
|
43 |
);
|
44 |
|
45 |
let systemPromptSupported = $derived(
|
46 |
+
session.project.conversations.some(conversation => isSystemPromptSupported(conversation.model))
|
47 |
);
|
48 |
+
let compareActive = $derived(session.project.conversations.length === 2);
|
49 |
|
50 |
function reset() {
|
51 |
+
session.project.conversations = session.project.conversations.map(conversation => {
|
52 |
+
return {
|
53 |
+
...conversation,
|
54 |
+
systemMessage: { role: "system", content: "" },
|
55 |
+
messages: [{ ...startMessageUser }],
|
56 |
+
};
|
57 |
});
|
|
|
58 |
}
|
59 |
|
60 |
+
async function runInference(conversationIdx: number) {
|
61 |
+
const conversation = session.project.conversations[conversationIdx];
|
62 |
+
if (!conversation) return;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
63 |
|
|
|
64 |
const startTime = performance.now();
|
65 |
+
const hf = new HfInference(token.value);
|
66 |
|
67 |
if (conversation.streaming) {
|
68 |
+
let addedMessage = false;
|
69 |
+
let streamingMessage = $state({ role: "assistant", content: "" });
|
|
|
|
|
70 |
|
71 |
await handleStreamingResponse(
|
72 |
hf,
|
73 |
conversation,
|
74 |
content => {
|
75 |
+
if (!streamingMessage) return;
|
76 |
+
streamingMessage.content = content;
|
77 |
+
if (!addedMessage) {
|
78 |
+
conversation.messages = [...conversation.messages, streamingMessage];
|
79 |
+
addedMessage = true;
|
|
|
|
|
|
|
|
|
80 |
}
|
81 |
+
// session.project.conversations[conversationIdx] = conversation;
|
82 |
+
const c = generationStats[conversationIdx];
|
83 |
+
if (c) c.generatedTokensCount += 1;
|
84 |
},
|
85 |
+
abortManager.createController()
|
86 |
);
|
87 |
} else {
|
|
|
88 |
const { message: newMessage, completion_tokens: newTokensCount } = await handleNonStreamingResponse(
|
89 |
hf,
|
90 |
conversation
|
91 |
);
|
92 |
+
conversation.messages = [...conversation.messages, newMessage];
|
93 |
+
const c = generationStats[conversationIdx];
|
94 |
+
if (c) c.generatedTokensCount += newTokensCount;
|
|
|
|
|
|
|
95 |
}
|
96 |
|
97 |
const endTime = performance.now();
|
|
|
100 |
}
|
101 |
|
102 |
async function submit() {
|
103 |
+
if (!token.value) {
|
104 |
+
token.showModal = true;
|
105 |
return;
|
106 |
}
|
107 |
|
108 |
+
for (const [idx, conversation] of session.project.conversations.entries()) {
|
109 |
+
if (conversation.messages.at(-1)?.role !== "assistant") continue;
|
110 |
+
let prefix = "";
|
111 |
+
if (session.project.conversations.length === 2) {
|
112 |
+
prefix = `Error on ${idx === 0 ? "left" : "right"} conversation. `;
|
|
|
|
|
|
|
|
|
|
|
|
|
113 |
}
|
114 |
+
return addToast({
|
115 |
+
title: "Failed to run inference",
|
116 |
+
description: `${prefix}Messages must alternate between user/assistant roles.`,
|
117 |
+
variant: "error",
|
118 |
+
});
|
119 |
}
|
120 |
|
121 |
(document.activeElement as HTMLElement).blur();
|
122 |
loading = true;
|
123 |
|
124 |
try {
|
125 |
+
const promises = session.project.conversations.map((_, idx) => runInference(idx));
|
126 |
await Promise.all(promises);
|
127 |
} catch (error) {
|
128 |
+
for (const conversation of session.project.conversations) {
|
129 |
if (conversation.messages.at(-1)?.role === "assistant" && !conversation.messages.at(-1)?.content?.trim()) {
|
130 |
conversation.messages.pop();
|
131 |
conversation.messages = [...conversation.messages];
|
132 |
}
|
133 |
+
session.$ = session.$;
|
134 |
}
|
135 |
+
|
136 |
if (error instanceof Error) {
|
137 |
if (error.message.includes("token seems invalid")) {
|
138 |
token.reset();
|
|
|
145 |
}
|
146 |
} finally {
|
147 |
loading = false;
|
148 |
+
abortManager.clear();
|
149 |
}
|
150 |
}
|
151 |
|
|
|
161 |
const submittedHfToken = (formData.get("hf-token") as string).trim() ?? "";
|
162 |
const RE_HF_TOKEN = /\bhf_[a-zA-Z0-9]{34}\b/;
|
163 |
if (RE_HF_TOKEN.test(submittedHfToken)) {
|
164 |
+
token.value = submittedHfToken;
|
165 |
submit();
|
166 |
} else {
|
167 |
alert("Please provide a valid HF token.");
|
|
|
169 |
}
|
170 |
|
171 |
function addCompareModel(modelId: ModelWithTokenizer["id"]) {
|
172 |
+
const model = models.all.find(m => m.id === modelId);
|
173 |
+
if (!model || session.project.conversations.length === 2) {
|
174 |
return;
|
175 |
}
|
176 |
+
const newConversation = { ...JSON.parse(JSON.stringify(session.project.conversations[0])), model };
|
177 |
+
session.project.conversations = [...session.project.conversations, newConversation];
|
178 |
generationStats = [generationStats[0], { latency: 0, generatedTokensCount: 0 }];
|
179 |
}
|
180 |
|
181 |
function removeCompareModal(conversationIdx: number) {
|
182 |
+
session.project.conversations.splice(conversationIdx, 1)[0];
|
183 |
+
session.$ = session.$;
|
184 |
generationStats.splice(conversationIdx, 1)[0];
|
185 |
generationStats = generationStats;
|
186 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
187 |
</script>
|
188 |
|
189 |
+
{#if token.showModal}
|
190 |
<HFTokenModal
|
191 |
+
bind:storeLocallyHfToken={token.writeToLocalStorage}
|
192 |
+
on:close={() => (token.showModal = false)}
|
193 |
on:submit={handleTokenSubmit}
|
194 |
/>
|
195 |
{/if}
|
|
|
216 |
placeholder={systemPromptSupported
|
217 |
? "Enter a custom prompt"
|
218 |
: "System prompt is not supported with the chosen model."}
|
219 |
+
value={systemPromptSupported ? session.project.conversations[0]?.systemMessage.content : ""}
|
220 |
oninput={e => {
|
221 |
+
for (const conversation of session.project.conversations) {
|
222 |
conversation.systemMessage.content = e.currentTarget.value;
|
223 |
}
|
224 |
+
session.$ = session.$;
|
225 |
}}
|
226 |
class="absolute inset-x-0 bottom-0 h-full resize-none bg-transparent px-3 pt-10 text-sm outline-hidden"
|
227 |
></textarea>
|
|
|
231 |
<div
|
232 |
class="flex h-[calc(100dvh-5rem-120px)] divide-x divide-gray-200 overflow-x-auto overflow-y-hidden *:w-full max-sm:w-dvw md:h-[calc(100dvh-5rem)] md:pt-3 dark:divide-gray-800"
|
233 |
>
|
234 |
+
{#each session.project.conversations as _conversation, conversationIdx}
|
235 |
<div class="max-sm:min-w-full">
|
236 |
{#if compareActive}
|
237 |
<PlaygroundConversationHeader
|
238 |
{conversationIdx}
|
239 |
+
bind:conversation={session.project.conversations[conversationIdx]!}
|
240 |
on:close={() => removeCompareModal(conversationIdx)}
|
241 |
/>
|
242 |
{/if}
|
243 |
<PlaygroundConversation
|
244 |
{loading}
|
245 |
+
bind:conversation={
|
246 |
+
() => session.project.conversations[conversationIdx]!,
|
247 |
+
v => (session.project.conversations[conversationIdx] = v)
|
248 |
+
}
|
249 |
{viewCode}
|
250 |
{compareActive}
|
251 |
on:closeCode={() => (viewCode = false)}
|
|
|
286 |
<button
|
287 |
onclick={() => {
|
288 |
viewCode = false;
|
289 |
+
loading ? abortManager.abortAll() : submit();
|
290 |
}}
|
291 |
type="button"
|
292 |
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:ring-4 focus:ring-gray-300 focus:outline-hidden dark:border-gray-700 dark:focus:ring-gray-700 {loading
|
|
|
296 |
{#if loading}
|
297 |
<div class="flex flex-none items-center gap-[3px]">
|
298 |
<span class="mr-2">
|
299 |
+
{#if session.project.conversations[0]?.streaming || session.project.conversations[1]?.streaming}
|
300 |
Stop
|
301 |
{:else}
|
302 |
Cancel
|
|
|
331 |
class="flex flex-1 flex-col gap-6 overflow-y-hidden rounded-xl border border-gray-200/80 bg-white bg-linear-to-b from-white via-white p-3 shadow-xs dark:border-white/5 dark:bg-gray-900 dark:from-gray-800/40 dark:via-gray-800/40"
|
332 |
>
|
333 |
<div class="flex flex-col gap-2">
|
334 |
+
<ModelSelector bind:conversation={session.project.conversations[0]!} />
|
335 |
<div class="flex items-center gap-2 self-end px-2 text-xs whitespace-nowrap">
|
336 |
<button
|
337 |
class="flex items-center gap-0.5 text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-300"
|
|
|
341 |
Compare
|
342 |
</button>
|
343 |
<a
|
344 |
+
href="https://huggingface.co/{session.project.conversations[0]?.model.id}?inference_provider={session
|
345 |
+
.project.conversations[0]?.provider}"
|
346 |
target="_blank"
|
347 |
class="flex items-center gap-0.5 text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-300"
|
348 |
>
|
|
|
352 |
</div>
|
353 |
</div>
|
354 |
|
355 |
+
<GenerationConfig bind:conversation={session.project.conversations[0]!} />
|
356 |
+
{#if token.value}
|
357 |
<button
|
358 |
onclick={token.reset}
|
359 |
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"
|
|
|
411 |
|
412 |
{#if selectCompareModelOpen}
|
413 |
<ModelSelectorModal
|
414 |
+
conversation={session.project.conversations[0]!}
|
415 |
on:modelSelected={e => addCompareModel(e.detail)}
|
416 |
on:close={() => (selectCompareModelOpen = false)}
|
417 |
/>
|
src/lib/components/inference-playground/project-select.svelte
CHANGED
@@ -1,5 +1,5 @@
|
|
1 |
<script lang="ts">
|
2 |
-
import {
|
3 |
import { cn } from "$lib/utils/cn.js";
|
4 |
import { Select } from "melt/builders";
|
5 |
import IconCaret from "~icons/carbon/chevron-down";
|
@@ -15,18 +15,18 @@
|
|
15 |
|
16 |
let { class: classNames = "" }: Props = $props();
|
17 |
|
18 |
-
const isDefault = $derived(
|
19 |
|
20 |
const select = new Select({
|
21 |
-
value: () =>
|
22 |
onValueChange(v) {
|
23 |
-
if (v)
|
24 |
},
|
25 |
sameWidth: true,
|
26 |
});
|
27 |
|
28 |
async function saveProject() {
|
29 |
-
session.saveProject((await prompt("Set project name")) || "Project #" + (
|
30 |
}
|
31 |
</script>
|
32 |
|
@@ -39,7 +39,7 @@
|
|
39 |
)}
|
40 |
>
|
41 |
<div class="flex items-center gap-1 text-sm">
|
42 |
-
{
|
43 |
</div>
|
44 |
<div
|
45 |
class="absolute right-2 grid size-4 flex-none place-items-center rounded-sm bg-gray-100 text-xs dark:bg-gray-600"
|
@@ -52,14 +52,14 @@
|
|
52 |
<IconSave />
|
53 |
</button>
|
54 |
{:else}
|
55 |
-
<button class="btn size-[32px] p-0" onclick={() => (
|
56 |
<IconCross />
|
57 |
</button>
|
58 |
{/if}
|
59 |
</div>
|
60 |
|
61 |
<div {...select.content} class="rounded-lg border bg-gray-100 dark:border-gray-700 dark:bg-gray-800">
|
62 |
-
{#each
|
63 |
{@const option = select.getOption(id)}
|
64 |
<div {...option} class="group block w-full p-1 text-sm dark:text-white">
|
65 |
<div
|
|
|
1 |
<script lang="ts">
|
2 |
+
import { session } from "$lib/state/session.svelte.js";
|
3 |
import { cn } from "$lib/utils/cn.js";
|
4 |
import { Select } from "melt/builders";
|
5 |
import IconCaret from "~icons/carbon/chevron-down";
|
|
|
15 |
|
16 |
let { class: classNames = "" }: Props = $props();
|
17 |
|
18 |
+
const isDefault = $derived(session.$.activeProjectId === "default");
|
19 |
|
20 |
const select = new Select({
|
21 |
+
value: () => session.$.activeProjectId,
|
22 |
onValueChange(v) {
|
23 |
+
if (v) session.$.activeProjectId = v;
|
24 |
},
|
25 |
sameWidth: true,
|
26 |
});
|
27 |
|
28 |
async function saveProject() {
|
29 |
+
session.saveProject((await prompt("Set project name")) || "Project #" + (session.$.projects.length + 1));
|
30 |
}
|
31 |
</script>
|
32 |
|
|
|
39 |
)}
|
40 |
>
|
41 |
<div class="flex items-center gap-1 text-sm">
|
42 |
+
{session.project.name}
|
43 |
</div>
|
44 |
<div
|
45 |
class="absolute right-2 grid size-4 flex-none place-items-center rounded-sm bg-gray-100 text-xs dark:bg-gray-600"
|
|
|
52 |
<IconSave />
|
53 |
</button>
|
54 |
{:else}
|
55 |
+
<button class="btn size-[32px] p-0" onclick={() => (session.$.activeProjectId = "default")}>
|
56 |
<IconCross />
|
57 |
</button>
|
58 |
{/if}
|
59 |
</div>
|
60 |
|
61 |
<div {...select.content} class="rounded-lg border bg-gray-100 dark:border-gray-700 dark:bg-gray-800">
|
62 |
+
{#each session.$.projects as { name, id } (id)}
|
63 |
{@const option = select.getOption(id)}
|
64 |
<div {...option} class="group block w-full p-1 text-sm dark:text-white">
|
65 |
<div
|
src/lib/components/toaster.svelte
CHANGED
@@ -93,8 +93,8 @@
|
|
93 |
transition: all 350ms ease;
|
94 |
}
|
95 |
|
96 |
-
:global(
|
97 |
-
box-shadow: 0 -2px 10px rgba(0, 0, 0, 0.
|
98 |
}
|
99 |
|
100 |
[data-melt-toaster-toast-content]:nth-last-child(n + 4) {
|
|
|
93 |
transition: all 350ms ease;
|
94 |
}
|
95 |
|
96 |
+
:global(.dark [data-melt-toaster-toast-content]) {
|
97 |
+
box-shadow: 0 -2px 10px rgba(0, 0, 0, 0.25);
|
98 |
}
|
99 |
|
100 |
[data-melt-toaster-toast-content]:nth-last-child(n + 4) {
|
src/lib/spells/abort-manager.svelte.ts
ADDED
@@ -0,0 +1,34 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import { onDestroy } from "svelte";
|
2 |
+
|
3 |
+
/**
|
4 |
+
* Manages abort controllers, and aborts them when the component unmounts.
|
5 |
+
*/
|
6 |
+
export class AbortManager {
|
7 |
+
private controllers: AbortController[] = [];
|
8 |
+
|
9 |
+
constructor() {
|
10 |
+
onDestroy(() => this.abortAll());
|
11 |
+
}
|
12 |
+
|
13 |
+
/**
|
14 |
+
* Creates a new abort controller and adds it to the manager.
|
15 |
+
*/
|
16 |
+
public createController(): AbortController {
|
17 |
+
const controller = new AbortController();
|
18 |
+
this.controllers.push(controller);
|
19 |
+
return controller;
|
20 |
+
}
|
21 |
+
|
22 |
+
/**
|
23 |
+
* Aborts all controllers and clears the manager.
|
24 |
+
*/
|
25 |
+
public abortAll(): void {
|
26 |
+
this.controllers.forEach(controller => controller.abort());
|
27 |
+
this.controllers = [];
|
28 |
+
}
|
29 |
+
|
30 |
+
/** Clears the manager without aborting the controllers. */
|
31 |
+
public clear(): void {
|
32 |
+
this.controllers = [];
|
33 |
+
}
|
34 |
+
}
|
src/lib/spells/create-init.svelte.ts
ADDED
@@ -0,0 +1,14 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
export function createInit(cb: () => void) {
|
2 |
+
let called = $state(false);
|
3 |
+
|
4 |
+
return {
|
5 |
+
fn: () => {
|
6 |
+
if (called) return;
|
7 |
+
called = true;
|
8 |
+
cb();
|
9 |
+
},
|
10 |
+
get called() {
|
11 |
+
return called;
|
12 |
+
},
|
13 |
+
};
|
14 |
+
}
|
src/lib/spells/textarea-autosize.svelte.ts
CHANGED
@@ -53,7 +53,7 @@ export class TextareaAutosize {
|
|
53 |
);
|
54 |
}
|
55 |
|
56 |
-
triggerResize() {
|
57 |
if (!this.element) return;
|
58 |
|
59 |
let height = "";
|
@@ -63,5 +63,5 @@ export class TextareaAutosize {
|
|
63 |
height = `${this.textareaScrollHeight}px`;
|
64 |
|
65 |
this.element.style[this.styleProp] = height;
|
66 |
-
}
|
67 |
}
|
|
|
53 |
);
|
54 |
}
|
55 |
|
56 |
+
triggerResize = () => {
|
57 |
if (!this.element) return;
|
58 |
|
59 |
let height = "";
|
|
|
63 |
height = `${this.textareaScrollHeight}px`;
|
64 |
|
65 |
this.element.style[this.styleProp] = height;
|
66 |
+
};
|
67 |
}
|
src/lib/state/models.svelte.ts
CHANGED
@@ -2,7 +2,8 @@ import { page } from "$app/state";
|
|
2 |
import type { ModelWithTokenizer } from "$lib/types.js";
|
3 |
|
4 |
class Models {
|
5 |
-
|
|
|
6 |
}
|
7 |
|
8 |
export const models = new Models();
|
|
|
2 |
import type { ModelWithTokenizer } from "$lib/types.js";
|
3 |
|
4 |
class Models {
|
5 |
+
all = $derived(page.data.models as ModelWithTokenizer[]);
|
6 |
+
trending = $derived(this.all.toSorted((a, b) => b.trendingScore - a.trendingScore).slice(0, 5));
|
7 |
}
|
8 |
|
9 |
export const models = new Models();
|
src/lib/{stores/session.ts → state/session.svelte.ts}
RENAMED
@@ -1,5 +1,5 @@
|
|
1 |
import { defaultGenerationConfig } from "$lib/components/inference-playground/generation-config-settings.js";
|
2 |
-
import {
|
3 |
import {
|
4 |
PipelineTag,
|
5 |
type Conversation,
|
@@ -10,9 +10,8 @@ import {
|
|
10 |
type Session,
|
11 |
} from "$lib/types.js";
|
12 |
import { safeParse } from "$lib/utils/json.js";
|
13 |
-
import { getTrending } from "$lib/utils/model.js";
|
14 |
-
import { get, writable } from "svelte/store";
|
15 |
import typia from "typia";
|
|
|
16 |
|
17 |
const LOCAL_STORAGE_KEY = "hf_inference_playground_session";
|
18 |
|
@@ -38,9 +37,7 @@ const emptyModel: ModelWithTokenizer = {
|
|
38 |
};
|
39 |
|
40 |
function getDefaults() {
|
41 |
-
const
|
42 |
-
const featured = getTrending($models);
|
43 |
-
const defaultModel = featured[0] ?? $models[0] ?? emptyModel;
|
44 |
|
45 |
const defaultConversation: Conversation = {
|
46 |
model: defaultModel,
|
@@ -59,8 +56,11 @@ function getDefaults() {
|
|
59 |
return { defaultProject, defaultConversation };
|
60 |
}
|
61 |
|
62 |
-
|
63 |
-
|
|
|
|
|
|
|
64 |
const { defaultConversation, defaultProject } = getDefaults();
|
65 |
|
66 |
// Get saved session from localStorage if available
|
@@ -86,12 +86,11 @@ function createSessionStore() {
|
|
86 |
// is the maximum between the two.
|
87 |
const dp = savedSession.projects.find(p => p.id === "default");
|
88 |
if (typia.is<DefaultProject>(dp)) {
|
89 |
-
const $models = get(models);
|
90 |
// Parse URL query parameters
|
91 |
const searchParams = new URLSearchParams(window.location.search);
|
92 |
const searchProviders = searchParams.getAll("provider");
|
93 |
const searchModelIds = searchParams.getAll("modelId");
|
94 |
-
const modelsFromSearch = searchModelIds.map(id =>
|
95 |
if (modelsFromSearch.length > 0) savedSession.activeProjectId = "default";
|
96 |
|
97 |
const max = Math.max(dp.conversations.length, modelsFromSearch.length, searchProviders.length);
|
@@ -105,110 +104,78 @@ function createSessionStore() {
|
|
105 |
}
|
106 |
}
|
107 |
|
108 |
-
|
109 |
});
|
110 |
|
111 |
-
|
112 |
-
|
113 |
-
|
114 |
-
|
115 |
-
|
116 |
-
|
117 |
-
|
118 |
-
|
119 |
-
|
120 |
-
|
121 |
-
|
122 |
-
return s;
|
123 |
});
|
124 |
-
};
|
125 |
-
|
126 |
-
const set: typeof store.set = (...args) => {
|
127 |
-
update(_ => args[0]);
|
128 |
-
};
|
129 |
-
|
130 |
-
// Add a method to clear localStorage
|
131 |
-
function clearSavedSession() {
|
132 |
-
localStorage.removeItem(LOCAL_STORAGE_KEY);
|
133 |
}
|
134 |
|
135 |
-
|
136 |
-
|
137 |
-
|
138 |
-
function saveProject(name: string) {
|
139 |
-
update(s => {
|
140 |
-
const defaultProject = s.projects.find(p => p.id === "default");
|
141 |
-
if (!defaultProject) return s;
|
142 |
-
|
143 |
-
const project: Project = {
|
144 |
-
...defaultProject,
|
145 |
-
name,
|
146 |
-
id: crypto.randomUUID(),
|
147 |
-
};
|
148 |
-
|
149 |
-
defaultProject.conversations = [getDefaults().defaultConversation];
|
150 |
-
|
151 |
-
return { ...s, projects: [...s.projects, project], activeProjectId: project.id };
|
152 |
-
});
|
153 |
}
|
154 |
|
155 |
-
|
156 |
-
|
157 |
-
if (id === "default") return;
|
158 |
-
|
159 |
-
update(s => {
|
160 |
-
const projects = s.projects.filter(p => p.id !== id);
|
161 |
-
if (projects.length === 0) {
|
162 |
-
const { defaultProject } = getDefaults();
|
163 |
-
const newSession = { ...s, projects: [defaultProject], activeProjectId: defaultProject.id };
|
164 |
-
return typia.is<Session>(newSession) ? newSession : s;
|
165 |
-
}
|
166 |
-
|
167 |
-
const currProject = projects.find(p => p.id === s.activeProjectId);
|
168 |
-
const newSession = { ...s, projects, activeProjectId: currProject?.id ?? projects[0]?.id };
|
169 |
-
return typia.is<Session>(newSession) ? newSession : s;
|
170 |
-
});
|
171 |
}
|
172 |
|
173 |
-
|
174 |
-
|
175 |
-
const projects = s.projects.map(p => (p.id === id ? { ...p, ...data } : p));
|
176 |
-
const newSession = { ...s, projects };
|
177 |
-
return typia.is<Session>(newSession) ? newSession : s;
|
178 |
-
});
|
179 |
}
|
180 |
|
181 |
-
|
182 |
-
|
|
|
183 |
|
184 |
-
|
|
|
|
|
|
|
|
|
185 |
|
186 |
-
|
187 |
-
return s.projects.find(p => p.id === s.activeProjectId) ?? s.projects[0];
|
188 |
-
}
|
189 |
|
190 |
-
|
191 |
-
|
192 |
-
return session.subscribe(s => {
|
193 |
-
set(getActiveProject(s));
|
194 |
-
});
|
195 |
-
});
|
196 |
|
197 |
-
|
198 |
-
|
199 |
-
|
200 |
-
|
201 |
-
|
202 |
-
|
203 |
-
|
204 |
-
|
|
|
|
|
|
|
|
|
205 |
};
|
206 |
|
207 |
-
|
208 |
-
|
|
|
209 |
};
|
210 |
|
211 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
212 |
}
|
213 |
|
214 |
-
export const
|
|
|
1 |
import { defaultGenerationConfig } from "$lib/components/inference-playground/generation-config-settings.js";
|
2 |
+
import { createInit } from "$lib/spells/create-init.svelte.js";
|
3 |
import {
|
4 |
PipelineTag,
|
5 |
type Conversation,
|
|
|
10 |
type Session,
|
11 |
} from "$lib/types.js";
|
12 |
import { safeParse } from "$lib/utils/json.js";
|
|
|
|
|
13 |
import typia from "typia";
|
14 |
+
import { models } from "./models.svelte";
|
15 |
|
16 |
const LOCAL_STORAGE_KEY = "hf_inference_playground_session";
|
17 |
|
|
|
37 |
};
|
38 |
|
39 |
function getDefaults() {
|
40 |
+
const defaultModel = models.trending[0] ?? models.all[0] ?? emptyModel;
|
|
|
|
|
41 |
|
42 |
const defaultConversation: Conversation = {
|
43 |
model: defaultModel,
|
|
|
56 |
return { defaultProject, defaultConversation };
|
57 |
}
|
58 |
|
59 |
+
class SessionState {
|
60 |
+
#value = $state<Session>({} as Session);
|
61 |
+
// Call this only when the value is read, otherwise some values may have not
|
62 |
+
// been loaded yet (page.data, for example)
|
63 |
+
#init = createInit(() => {
|
64 |
const { defaultConversation, defaultProject } = getDefaults();
|
65 |
|
66 |
// Get saved session from localStorage if available
|
|
|
86 |
// is the maximum between the two.
|
87 |
const dp = savedSession.projects.find(p => p.id === "default");
|
88 |
if (typia.is<DefaultProject>(dp)) {
|
|
|
89 |
// Parse URL query parameters
|
90 |
const searchParams = new URLSearchParams(window.location.search);
|
91 |
const searchProviders = searchParams.getAll("provider");
|
92 |
const searchModelIds = searchParams.getAll("modelId");
|
93 |
+
const modelsFromSearch = searchModelIds.map(id => models.all.find(model => model.id === id)).filter(Boolean);
|
94 |
if (modelsFromSearch.length > 0) savedSession.activeProjectId = "default";
|
95 |
|
96 |
const max = Math.max(dp.conversations.length, modelsFromSearch.length, searchProviders.length);
|
|
|
104 |
}
|
105 |
}
|
106 |
|
107 |
+
this.$ = savedSession;
|
108 |
});
|
109 |
|
110 |
+
constructor() {
|
111 |
+
$effect.root(() => {
|
112 |
+
$effect(() => {
|
113 |
+
if (!this.#init.called) return;
|
114 |
+
const v = $state.snapshot(this.#value);
|
115 |
+
try {
|
116 |
+
localStorage.setItem(LOCAL_STORAGE_KEY, JSON.stringify(v));
|
117 |
+
} catch (e) {
|
118 |
+
console.error("Failed to save session to localStorage:", e);
|
119 |
+
}
|
120 |
+
});
|
|
|
121 |
});
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
122 |
}
|
123 |
|
124 |
+
get $() {
|
125 |
+
this.#init.fn();
|
126 |
+
return this.#value;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
127 |
}
|
128 |
|
129 |
+
set $(v: Session) {
|
130 |
+
this.#value = v;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
131 |
}
|
132 |
|
133 |
+
#setAnySession(s: unknown) {
|
134 |
+
if (typia.is<Session>(s)) this.$ = s;
|
|
|
|
|
|
|
|
|
135 |
}
|
136 |
|
137 |
+
saveProject = (name: string) => {
|
138 |
+
const defaultProject = this.$.projects.find(p => p.id === "default");
|
139 |
+
if (!defaultProject) return;
|
140 |
|
141 |
+
const project: Project = {
|
142 |
+
...defaultProject,
|
143 |
+
name,
|
144 |
+
id: crypto.randomUUID(),
|
145 |
+
};
|
146 |
|
147 |
+
defaultProject.conversations = [getDefaults().defaultConversation];
|
|
|
|
|
148 |
|
149 |
+
this.$ = { ...this.$, projects: [...this.$.projects, project], activeProjectId: project.id };
|
150 |
+
};
|
|
|
|
|
|
|
|
|
151 |
|
152 |
+
deleteProject = (id: string) => {
|
153 |
+
// Can't delete default project!
|
154 |
+
if (id === "default") return;
|
155 |
+
|
156 |
+
const projects = this.$.projects.filter(p => p.id !== id);
|
157 |
+
if (projects.length === 0) {
|
158 |
+
const { defaultProject } = getDefaults();
|
159 |
+
this.#setAnySession({ ...this.$, projects: [defaultProject], activeProjectId: defaultProject.id });
|
160 |
+
}
|
161 |
+
|
162 |
+
const currProject = projects.find(p => p.id === this.$.activeProjectId);
|
163 |
+
this.#setAnySession({ ...this.$, projects, activeProjectId: currProject?.id ?? projects[0]?.id });
|
164 |
};
|
165 |
|
166 |
+
updateProject = (id: string, data: Partial<Project>) => {
|
167 |
+
const projects = this.$.projects.map(p => (p.id === id ? { ...p, ...data } : p));
|
168 |
+
this.#setAnySession({ ...this.$, projects });
|
169 |
};
|
170 |
|
171 |
+
get project() {
|
172 |
+
return this.$.projects.find(p => p.id === this.$.activeProjectId) ?? this.$.projects[0];
|
173 |
+
}
|
174 |
+
|
175 |
+
set project(np: Project) {
|
176 |
+
const projects = this.$.projects.map(p => (p.id === np.id ? np : p));
|
177 |
+
this.#setAnySession({ ...this.$, projects });
|
178 |
+
}
|
179 |
}
|
180 |
|
181 |
+
export const session = new SessionState();
|
src/lib/state/token.svelte.ts
CHANGED
@@ -10,10 +10,8 @@ class Token {
|
|
10 |
|
11 |
constructor() {
|
12 |
const storedHfToken = localStorage.getItem(key);
|
13 |
-
|
14 |
-
|
15 |
-
this.value = typia.is<string>(parsed) ? parsed : "";
|
16 |
-
}
|
17 |
}
|
18 |
|
19 |
get value() {
|
@@ -21,15 +19,17 @@ class Token {
|
|
21 |
}
|
22 |
|
23 |
set value(token: string) {
|
24 |
-
if (this.writeToLocalStorage)
|
|
|
|
|
25 |
this.#value = token;
|
26 |
this.showModal = !token.length;
|
27 |
}
|
28 |
|
29 |
-
reset() {
|
30 |
this.value = "";
|
31 |
localStorage.removeItem(key);
|
32 |
-
}
|
33 |
}
|
34 |
|
35 |
export const token = new Token();
|
|
|
10 |
|
11 |
constructor() {
|
12 |
const storedHfToken = localStorage.getItem(key);
|
13 |
+
const parsed = safeParse(storedHfToken ?? "");
|
14 |
+
this.value = typia.is<string>(parsed) ? parsed : "";
|
|
|
|
|
15 |
}
|
16 |
|
17 |
get value() {
|
|
|
19 |
}
|
20 |
|
21 |
set value(token: string) {
|
22 |
+
if (this.writeToLocalStorage) {
|
23 |
+
localStorage.setItem(key, JSON.stringify(token));
|
24 |
+
}
|
25 |
this.#value = token;
|
26 |
this.showModal = !token.length;
|
27 |
}
|
28 |
|
29 |
+
reset = () => {
|
30 |
this.value = "";
|
31 |
localStorage.removeItem(key);
|
32 |
+
};
|
33 |
}
|
34 |
|
35 |
export const token = new Token();
|
src/lib/stores/models.ts
DELETED
@@ -1,8 +0,0 @@
|
|
1 |
-
import { page } from "$app/stores";
|
2 |
-
import type { ModelWithTokenizer } from "$lib/types.js";
|
3 |
-
import { readable } from "svelte/store";
|
4 |
-
|
5 |
-
export const models = readable<ModelWithTokenizer[]>(undefined, set => {
|
6 |
-
const unsub = page.subscribe($p => set($p.data?.models));
|
7 |
-
return unsub;
|
8 |
-
});
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
src/lib/stores/mounted.ts
DELETED
@@ -1,9 +0,0 @@
|
|
1 |
-
import { onMount } from "svelte";
|
2 |
-
import { readonly, writable } from "svelte/store";
|
3 |
-
|
4 |
-
export function isMounted() {
|
5 |
-
const store = writable(false);
|
6 |
-
onMount(() => store.set(true));
|
7 |
-
|
8 |
-
return readonly(store);
|
9 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
src/lib/stores/token.ts
DELETED
@@ -1,31 +0,0 @@
|
|
1 |
-
import { writable } from "svelte/store";
|
2 |
-
|
3 |
-
const key = "hf_token";
|
4 |
-
|
5 |
-
function createTokenStore() {
|
6 |
-
const store = writable({ value: "", writeToLocalStorage: true, showModal: false });
|
7 |
-
|
8 |
-
function setValue(token: string) {
|
9 |
-
store.update(s => {
|
10 |
-
if (s.writeToLocalStorage) localStorage.setItem(key, JSON.stringify(token));
|
11 |
-
return { ...s, value: token, showModal: !token.length };
|
12 |
-
});
|
13 |
-
}
|
14 |
-
|
15 |
-
const storedHfToken = localStorage.getItem(key);
|
16 |
-
if (storedHfToken !== null) {
|
17 |
-
setValue(JSON.parse(storedHfToken));
|
18 |
-
}
|
19 |
-
|
20 |
-
return {
|
21 |
-
...store,
|
22 |
-
setValue,
|
23 |
-
reset() {
|
24 |
-
setValue("");
|
25 |
-
localStorage.removeItem(key);
|
26 |
-
store.update(s => ({ ...s, showModal: true }));
|
27 |
-
},
|
28 |
-
};
|
29 |
-
}
|
30 |
-
|
31 |
-
export const token = createTokenStore();
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
src/lib/types.ts
CHANGED
@@ -1,7 +1,9 @@
|
|
1 |
import type { GenerationConfig } from "$lib/components/inference-playground/generation-config-settings.js";
|
2 |
import type { ChatCompletionInputMessage } from "@huggingface/tasks";
|
3 |
|
4 |
-
export type ConversationMessage =
|
|
|
|
|
5 |
|
6 |
export type Conversation = {
|
7 |
model: ModelWithTokenizer;
|
@@ -13,7 +15,7 @@ export type Conversation = {
|
|
13 |
};
|
14 |
|
15 |
export type Project = {
|
16 |
-
conversations:
|
17 |
id: string;
|
18 |
name: string;
|
19 |
};
|
|
|
1 |
import type { GenerationConfig } from "$lib/components/inference-playground/generation-config-settings.js";
|
2 |
import type { ChatCompletionInputMessage } from "@huggingface/tasks";
|
3 |
|
4 |
+
export type ConversationMessage = Pick<ChatCompletionInputMessage, "name" | "role" | "tool_calls"> & {
|
5 |
+
content?: string;
|
6 |
+
};
|
7 |
|
8 |
export type Conversation = {
|
9 |
model: ModelWithTokenizer;
|
|
|
15 |
};
|
16 |
|
17 |
export type Project = {
|
18 |
+
conversations: Conversation[];
|
19 |
id: string;
|
20 |
name: string;
|
21 |
};
|
src/lib/utils/model.ts
DELETED
@@ -1,5 +0,0 @@
|
|
1 |
-
import type { ModelWithTokenizer } from "$lib/types.js";
|
2 |
-
|
3 |
-
export function getTrending(models: ModelWithTokenizer[], limit = 5) {
|
4 |
-
return models.toSorted((a, b) => b.trendingScore - a.trendingScore).slice(0, limit);
|
5 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|