enzostvs HF Staff commited on
Commit
5dfba10
·
1 Parent(s): a4c6b8b

global settings for each model

Browse files
src/lib/chat/triggerAiCall.ts CHANGED
@@ -5,7 +5,7 @@ export interface TriggerAiCallContext {
5
  userId: string;
6
  newNodes: Node[];
7
  messages: ChatMessage[];
8
- selectedModels: ChatModel[];
9
  prompt: string;
10
  nodeData: Record<string, unknown> | undefined;
11
  authToken: string;
@@ -150,9 +150,7 @@ export async function triggerAiCall(ctx: TriggerAiCallContext): Promise<void> {
150
  ...nodeData,
151
  messages: newNodes.length === failedNodeIds.size ? messages.slice(0, -1) : messages,
152
  prompt: newNodes.length === failedNodeIds.size ? prompt : '',
153
- selectedModels: selectedModels.map((m) =>
154
- failedModelIds.has(m.id) ? { ...m, isError: true } : m
155
- )
156
  } as Record<string, unknown>,
157
  { replace: true }
158
  );
@@ -176,7 +174,7 @@ export async function triggerAiCall(ctx: TriggerAiCallContext): Promise<void> {
176
  position: { x: 0, y: 0 },
177
  data: {
178
  role: 'user',
179
- selectedModels: selectedModels.filter((m) => !m.isError),
180
  messages: [...messages, assistantMessage]
181
  }
182
  };
 
5
  userId: string;
6
  newNodes: Node[];
7
  messages: ChatMessage[];
8
+ selectedModels: string[];
9
  prompt: string;
10
  nodeData: Record<string, unknown> | undefined;
11
  authToken: string;
 
150
  ...nodeData,
151
  messages: newNodes.length === failedNodeIds.size ? messages.slice(0, -1) : messages,
152
  prompt: newNodes.length === failedNodeIds.size ? prompt : '',
153
+ selectedModels: selectedModels.filter((m) => !failedModelIds.has(m))
 
 
154
  } as Record<string, unknown>,
155
  { replace: true }
156
  );
 
174
  position: { x: 0, y: 0 },
175
  data: {
176
  role: 'user',
177
+ selectedModels: selectedModels.filter((m) => !failedModelIds.has(m)),
178
  messages: [...messages, assistantMessage]
179
  }
180
  };
src/lib/components/chat/User.svelte CHANGED
@@ -35,8 +35,8 @@
35
  const { update: updateEdges } = useEdges();
36
  const { updateNodeData, deleteElements, getEdges, getNodes } = useSvelteFlow();
37
 
38
- let selectedModels = $derived<ChatModel[]>(
39
- (nodeData.current?.data.selectedModels as ChatModel[]) ?? []
40
  );
41
  let messages = $derived((nodeData.current?.data.messages as ChatMessage[]) ?? []);
42
  let isFirstNode = $derived((nodeData.current?.data.isFirstNode as boolean) ?? false);
@@ -52,30 +52,30 @@
52
  MAX_SUGGESTIONS
53
  );
54
 
55
- function addModel(model: ChatModel) {
56
- if (!selectedModels.some((m) => m.id === model.id)) {
57
  updateNodeData(
58
  id,
59
  {
60
  ...nodeData.current?.data,
61
- selectedModels: [...selectedModels, model]
62
  },
63
  { replace: true }
64
  );
65
  }
66
  }
67
- function removeModel(model: ChatModel) {
68
  updateNodeData(
69
  id,
70
  {
71
  ...nodeData.current?.data,
72
- selectedModels: selectedModels.filter((m) => m.id !== model.id)
73
  },
74
  { replace: true }
75
  );
76
  }
77
 
78
- function handleTriggerAction(models: ChatModel[] = selectedModels) {
79
  if (!authState.user) {
80
  signinModalState.open = true;
81
  return;
@@ -89,7 +89,7 @@
89
  {
90
  ...nodeData.current?.data,
91
  messages: newMessages,
92
- selectedModels: models.map((m) => ({ ...m, isError: false }))
93
  },
94
  { replace: true }
95
  );
@@ -178,30 +178,26 @@
178
  <SettingsModel
179
  {model}
180
  onSave={(model) => {
181
- updateNodeData(
182
- id,
183
- {
184
- ...nodeData.current,
185
- isFirstNode: nodeData.current?.data.isFirstNode ?? false,
186
- selectedModels: selectedModels.map((m) => (m.id === model.id ? model : m))
187
- },
188
- { replace: true }
189
- );
190
- selectedModels = selectedModels.map((m) => (m.id === model.id ? model : m));
191
  }}
192
  >
193
- <Button
194
- variant={model.isError ? 'outline-destructive' : 'outline'}
195
- size="sm"
196
- class="group relative font-normal! shadow-none!"
197
- >
198
  <img
199
- src={`https://huggingface.co/api/avatars/${model.owned_by}`}
200
- alt={model.id}
201
  class="size-3.5 rounded"
202
  />
203
- {model.id.split('/').pop() ?? model.id}
204
- {#if !lastMessage || model.isError || selectedModels.length > MAX_MODELS_PER_NODE}
205
  <Button
206
  variant="default"
207
  size="icon-3xs"
@@ -219,7 +215,7 @@
219
  </SettingsModel>
220
  {/each}
221
  {#if !lastMessage && !loading}
222
- <ComboBoxModels onSelect={addModel} excludeIds={selectedModels.map((m) => m.id)} />
223
  {/if}
224
  </div>
225
  </header>
 
35
  const { update: updateEdges } = useEdges();
36
  const { updateNodeData, deleteElements, getEdges, getNodes } = useSvelteFlow();
37
 
38
+ let selectedModels = $derived<string[]>(
39
+ (nodeData.current?.data.selectedModels as string[]) ?? []
40
  );
41
  let messages = $derived((nodeData.current?.data.messages as ChatMessage[]) ?? []);
42
  let isFirstNode = $derived((nodeData.current?.data.isFirstNode as boolean) ?? false);
 
52
  MAX_SUGGESTIONS
53
  );
54
 
55
+ function addModel(modelId: string) {
56
+ if (!selectedModels.includes(modelId)) {
57
  updateNodeData(
58
  id,
59
  {
60
  ...nodeData.current?.data,
61
+ selectedModels: [...selectedModels, modelId]
62
  },
63
  { replace: true }
64
  );
65
  }
66
  }
67
+ function removeModel(modelId: string) {
68
  updateNodeData(
69
  id,
70
  {
71
  ...nodeData.current?.data,
72
+ selectedModels: selectedModels.filter((m) => m !== modelId)
73
  },
74
  { replace: true }
75
  );
76
  }
77
 
78
+ function handleTriggerAction(models: string[] = selectedModels) {
79
  if (!authState.user) {
80
  signinModalState.open = true;
81
  return;
 
89
  {
90
  ...nodeData.current?.data,
91
  messages: newMessages,
92
+ selectedModels: models
93
  },
94
  { replace: true }
95
  );
 
178
  <SettingsModel
179
  {model}
180
  onSave={(model) => {
181
+ // updateNodeData(
182
+ // id,
183
+ // {
184
+ // ...nodeData.current,
185
+ // isFirstNode: nodeData.current?.data.isFirstNode ?? false,
186
+ // selectedModels: selectedModels.map((m) => (m.id === model.id ? model : m))
187
+ // },
188
+ // { replace: true }
189
+ // );
190
+ // selectedModels = selectedModels.map((m) => (m.id === model.id ? model : m));
191
  }}
192
  >
193
+ <Button variant="outline" size="sm" class="group relative font-normal! shadow-none!">
 
 
 
 
194
  <img
195
+ src={`https://huggingface.co/api/avatars/${model.split('/')[0]}`}
196
+ alt={model.split('/')[0]}
197
  class="size-3.5 rounded"
198
  />
199
+ {model.split('/').pop() ?? model}
200
+ {#if !lastMessage || selectedModels.length > MAX_MODELS_PER_NODE}
201
  <Button
202
  variant="default"
203
  size="icon-3xs"
 
215
  </SettingsModel>
216
  {/each}
217
  {#if !lastMessage && !loading}
218
+ <ComboBoxModels onSelect={addModel} excludeIds={selectedModels} />
219
  {/if}
220
  </div>
221
  </header>
src/lib/components/model/ComboBoxModels.svelte CHANGED
@@ -4,11 +4,10 @@
4
  import * as Command from '$lib/components/ui/command/';
5
  import { Button } from '$lib/components/ui/button/';
6
  import { modelsState } from '$lib/state/models.svelte';
7
- import type { ChatModel } from '$lib/helpers/types';
8
  import { MAX_TRENDING_MODELS } from '$lib';
9
 
10
  interface Props {
11
- onSelect?: (model: ChatModel) => void;
12
  excludeIds?: string[];
13
  }
14
 
@@ -64,7 +63,7 @@
64
  {#each trendingModels as model (model.id)}
65
  <Command.Item
66
  onSelect={() => {
67
- onSelect?.(model);
68
  open = false;
69
  }}
70
  >
@@ -82,7 +81,7 @@
82
  {#each otherModels as model (model.id)}
83
  <Command.Item
84
  onSelect={() => {
85
- onSelect?.(model);
86
  open = false;
87
  }}
88
  >
 
4
  import * as Command from '$lib/components/ui/command/';
5
  import { Button } from '$lib/components/ui/button/';
6
  import { modelsState } from '$lib/state/models.svelte';
 
7
  import { MAX_TRENDING_MODELS } from '$lib';
8
 
9
  interface Props {
10
+ onSelect?: (model: string) => void;
11
  excludeIds?: string[];
12
  }
13
 
 
63
  {#each trendingModels as model (model.id)}
64
  <Command.Item
65
  onSelect={() => {
66
+ onSelect?.(model.id);
67
  open = false;
68
  }}
69
  >
 
81
  {#each otherModels as model (model.id)}
82
  <Command.Item
83
  onSelect={() => {
84
+ onSelect?.(model.id);
85
  open = false;
86
  }}
87
  >
src/lib/components/model/SettingsModel.svelte CHANGED
@@ -14,46 +14,56 @@
14
  import Switch from '$lib/components/ui/switch/switch.svelte';
15
  import { fade } from 'svelte/transition';
16
  import Spinner from '../loading/Spinner.svelte';
 
17
 
18
  let {
19
- model,
20
  children,
21
  onSave
22
  }: {
23
- model: ChatModel;
24
  children: Snippet;
25
  onSave: (model: ChatModel) => void;
26
  } = $props();
27
 
28
- // svelte-ignore state_referenced_locally
29
- let temperature = $state<number | undefined>(model.temperature ?? undefined);
30
- // svelte-ignore state_referenced_locally
31
- let max_tokens = $state<number | undefined>(model.max_tokens ?? undefined);
32
- // svelte-ignore state_referenced_locally
33
- let top_p = $state<number | undefined>(model.top_p ?? undefined);
 
 
 
 
 
 
 
 
 
 
34
  let open = $state<boolean>(false);
35
  // svelte-ignore state_referenced_locally
36
- let provider = $state<string>(model.provider ?? 'auto');
37
  let loading = $state<boolean>(false);
38
 
39
  let maxContentLength = $derived(
40
  provider === 'auto'
41
- ? model.providers[0]?.context_length
42
- : model.providers.find((p) => p.provider === provider)?.context_length
43
  );
44
 
45
  async function handleSave() {
46
  if (loading) return;
47
  loading = true;
48
  await new Promise((resolve) => setTimeout(resolve, 1_000));
 
 
49
  loading = false;
50
  }
51
  </script>
52
 
53
- <Dialog.Root
54
- bind:open
55
- onOpenChange={(value) => !value && onSave({ ...model, temperature, max_tokens, top_p, provider })}
56
- >
57
  <Dialog.Trigger>{@render children?.()}</Dialog.Trigger>
58
  <Dialog.Content class="max-w-md! gap-0! p-0!">
59
  <Dialog.Header class="mb-0 gap-1! rounded-none border-b p-5">
@@ -79,44 +89,17 @@
79
  </Button>
80
  </a>
81
  </div>
82
- {/if}
83
- <main class="mt-0 space-y-5 px-3 pb-5">
84
- <div class="rounded-lg border border-border p-3.5">
85
- <h4 class="text-sm leading-none font-medium">Inference provider</h4>
86
- <p class="mt-0.5 text-xs text-muted-foreground">
87
- Choose which Inference Provider to use with this model
88
- </p>
89
- <Select.Root type="single" bind:value={provider}>
90
- <Select.Trigger class="mt-3 flex w-full items-center justify-between gap-2 capitalize">
91
- <div class="flex items-center gap-2">
92
- {#if PROVIDER_SELECTION_MODES.find((m) => m.value === provider)}
93
- {@const mode = PROVIDER_SELECTION_MODES.find((m) => m.value === provider)!}
94
- <div class="flex size-5 items-center justify-center rounded {mode.class}">
95
- <mode.icon class="size-3 {mode.iconClass}" />
96
- </div>
97
- <p>
98
- {mode.label}
99
- {#if mode.description}
100
- <span class="text-xs text-muted-foreground lowercase italic"
101
- >({mode.description})</span
102
- >
103
- {/if}
104
- </p>
105
- {:else}
106
- <img
107
- src={`https://huggingface.co/api/avatars/${provider}`}
108
- alt={provider}
109
- class="size-4 rounded"
110
- />
111
- {provider}
112
- {/if}
113
- </div>
114
- </Select.Trigger>
115
- <Select.Content>
116
- <Select.Group>
117
- <Select.GroupHeading>Selection mode</Select.GroupHeading>
118
- {#each PROVIDER_SELECTION_MODES as mode}
119
- <Select.Item value={mode.value}>
120
  <div class="flex size-5 items-center justify-center rounded {mode.class}">
121
  <mode.icon class="size-3 {mode.iconClass}" />
122
  </div>
@@ -128,168 +111,195 @@
128
  >
129
  {/if}
130
  </p>
131
- </Select.Item>
132
- {/each}
133
- </Select.Group>
134
- <Select.Separator />
135
- <Select.Group>
136
- <Select.GroupHeading>Specific provider</Select.GroupHeading>
137
- {#each model.providers as provider}
138
- <Select.Item value={provider.provider}>
139
- <div class="flex items-center gap-2 capitalize">
140
- <img
141
- src={`https://huggingface.co/api/avatars/${provider.provider}`}
142
- alt={provider.provider}
143
- class="size-4 rounded"
144
- />
145
- {provider.provider}
146
- </div>
147
- {#if formatPricingPerToken(provider.pricing)}
148
- <span class="text-xs text-muted-foreground">
149
- {formatPricingPerToken(provider.pricing)}
150
- </span>
151
- {/if}
152
- </Select.Item>
153
- {/each}
154
- </Select.Group>
155
- </Select.Content>
156
- </Select.Root>
157
- </div>
158
- <div class="grid gap-3 rounded-lg border border-border p-3.5">
159
- <div class="space-y-2">
160
- <div class="flex items-center justify-between gap-2">
161
- <div>
162
- <h4 class="text-sm leading-none font-medium">Temperature</h4>
163
- <p class="mt-0.5 text-xs text-muted-foreground">
164
- Tunes the creativity vs. predictability trade-off.
165
- </p>
166
- </div>
167
- <Switch
168
- checked={temperature !== undefined}
169
- onCheckedChange={(value) => {
170
- if (value) {
171
- temperature = 0.5;
172
- } else {
173
- temperature = undefined;
174
- }
175
- }}
176
- />
177
- </div>
178
- {#if temperature !== undefined}
179
- <div class="flex items-center gap-2" transition:fade={{ duration: 100 }}>
180
- <Slider
181
- type="single"
182
- bind:value={temperature}
183
- min={0}
184
- max={2}
185
- step={0.01}
186
- class="mt-2"
187
- />
188
- <Input
189
- type="number"
190
- min={0}
191
- max={2}
192
- step={0.1}
193
- bind:value={temperature}
194
- class="h-7! w-24!"
195
- />
196
- </div>
197
- {/if}
198
  </div>
199
- <Separator />
200
- <div class="space-y-2">
201
- <div class="flex items-center justify-between gap-2">
202
- <div>
203
- <h4 class="text-sm leading-none font-medium">Max Tokens</h4>
204
- <p class="mt-0.5 text-xs text-muted-foreground">
205
- Sets the absolute limit for generated content length.
206
- </p>
 
 
 
 
 
 
 
 
 
 
 
207
  </div>
208
- <Switch
209
- checked={max_tokens !== undefined}
210
- onCheckedChange={(value) => {
211
- if (value) {
212
- max_tokens = (maxContentLength ?? 32_000) / 2;
213
- } else {
214
- max_tokens = undefined;
215
- }
216
- }}
217
- />
 
 
 
 
 
 
 
 
 
 
218
  </div>
219
- {#if max_tokens !== undefined}
220
- <div class="flex items-center gap-2" transition:fade={{ duration: 100 }}>
221
- <Slider
222
- type="single"
223
- bind:value={max_tokens}
224
- min={0}
225
- max={maxContentLength ?? 32_000}
226
- step={256}
227
- class="mt-2"
228
- />
229
- <Input
230
- type="number"
231
- min={0}
232
- max={maxContentLength ?? 32_000}
233
- step={256}
234
- bind:value={max_tokens}
235
- class="h-7! w-24!"
 
236
  />
237
  </div>
238
- {/if}
239
- </div>
240
- <Separator />
241
- <div class="space-y-2">
242
- <div class="flex items-center justify-between gap-2">
243
- <div>
244
- <h4 class="text-sm leading-none font-medium">Top-P</h4>
245
- <p class="mt-0.5 text-xs text-muted-foreground">
246
- Dynamically filters token selection by probability mass.
247
- </p>
248
- </div>
249
- <Switch
250
- checked={top_p !== undefined}
251
- onCheckedChange={(value) => {
252
- if (value) {
253
- top_p = 0.5;
254
- } else {
255
- top_p = undefined;
256
- }
257
- }}
258
- />
259
  </div>
260
- {#if top_p !== undefined}
261
- <div class="flex items-center gap-2" transition:fade={{ duration: 100 }}>
262
- <Slider type="single" bind:value={top_p} min={0} max={2} step={0.01} class="mt-2" />
263
- <Input
264
- type="number"
265
- min={0}
266
- max={2}
267
- step={0.1}
268
- bind:value={top_p}
269
- class="h-7! w-24!"
 
 
 
 
 
 
 
 
270
  />
271
  </div>
272
- {/if}
 
 
 
 
 
 
 
 
 
 
 
 
 
273
  </div>
274
- </div>
275
- <!-- <header class="border-t border-border">
276
- <h4 class="text-sm leading-none font-medium text-muted-foreground">Provider</h4>
277
- </header> -->
278
- <!-- <main class="w-full">
279
-
280
- </main> -->
281
- <div class="flex items-center justify-end gap-3 px-3">
282
- <Button variant="outline">Reset</Button>
283
- <Button class="flex-1" onclick={handleSave}>
284
- {#if loading}
285
- <Spinner className="text-lg" />
286
- Saving...
287
- {:else}
288
- <Save />
289
- Save settings
290
- {/if}
291
- </Button>
292
- </div>
293
- </main>
294
  </Dialog.Content>
295
  </Dialog.Root>
 
14
  import Switch from '$lib/components/ui/switch/switch.svelte';
15
  import { fade } from 'svelte/transition';
16
  import Spinner from '../loading/Spinner.svelte';
17
+ import { modelsState, saveModelSettings } from '$lib/state/models.svelte';
18
 
19
  let {
20
+ model: modelId,
21
  children,
22
  onSave
23
  }: {
24
+ model: string;
25
  children: Snippet;
26
  onSave: (model: ChatModel) => void;
27
  } = $props();
28
 
29
+ let model = $derived(modelsState.models.find((m) => m.id === modelId) ?? null);
30
+
31
+ let temperature = $state<number | undefined>(undefined);
32
+ let max_tokens = $state<number | undefined>(undefined);
33
+ let top_p = $state<number | undefined>(undefined);
34
+ let provider = $state<string>('auto');
35
+
36
+ $effect(() => {
37
+ if (model) {
38
+ temperature = model.temperature ?? undefined;
39
+ max_tokens = model.max_tokens ?? undefined;
40
+ top_p = model.top_p ?? undefined;
41
+ provider = model.provider ?? 'auto';
42
+ }
43
+ });
44
+
45
  let open = $state<boolean>(false);
46
  // svelte-ignore state_referenced_locally
 
47
  let loading = $state<boolean>(false);
48
 
49
  let maxContentLength = $derived(
50
  provider === 'auto'
51
+ ? model?.providers[0]?.context_length
52
+ : model?.providers.find((p) => p.provider === provider)?.context_length
53
  );
54
 
55
  async function handleSave() {
56
  if (loading) return;
57
  loading = true;
58
  await new Promise((resolve) => setTimeout(resolve, 1_000));
59
+ saveModelSettings(modelId, { temperature, top_p, max_tokens, provider });
60
+ if (model) onSave?.({ ...model, temperature, max_tokens, top_p, provider });
61
  loading = false;
62
  }
63
  </script>
64
 
65
+ <Dialog.Root bind:open>
66
+ <!-- onOpenChange={(value) => !value && onSave({ ...model, temperature, max_tokens, top_p, provider })} -->
 
 
67
  <Dialog.Trigger>{@render children?.()}</Dialog.Trigger>
68
  <Dialog.Content class="max-w-md! gap-0! p-0!">
69
  <Dialog.Header class="mb-0 gap-1! rounded-none border-b p-5">
 
89
  </Button>
90
  </a>
91
  </div>
92
+ <main class="mt-0 space-y-5 px-3 pb-5">
93
+ <div class="rounded-lg border border-border p-3.5">
94
+ <h4 class="text-sm leading-none font-medium">Inference provider</h4>
95
+ <p class="mt-0.5 text-xs text-muted-foreground">
96
+ Choose which Inference Provider to use with this model
97
+ </p>
98
+ <Select.Root type="single" bind:value={provider}>
99
+ <Select.Trigger class="mt-3 flex w-full items-center justify-between gap-2 capitalize">
100
+ <div class="flex items-center gap-2">
101
+ {#if PROVIDER_SELECTION_MODES.find((m) => m.value === provider)}
102
+ {@const mode = PROVIDER_SELECTION_MODES.find((m) => m.value === provider)!}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
103
  <div class="flex size-5 items-center justify-center rounded {mode.class}">
104
  <mode.icon class="size-3 {mode.iconClass}" />
105
  </div>
 
111
  >
112
  {/if}
113
  </p>
114
+ {:else}
115
+ <img
116
+ src={`https://huggingface.co/api/avatars/${provider}`}
117
+ alt={provider}
118
+ class="size-4 rounded"
119
+ />
120
+ {provider}
121
+ {/if}
122
+ </div>
123
+ </Select.Trigger>
124
+ <Select.Content>
125
+ <Select.Group>
126
+ <Select.GroupHeading>Selection mode</Select.GroupHeading>
127
+ {#each PROVIDER_SELECTION_MODES as mode}
128
+ <Select.Item value={mode.value}>
129
+ <div class="flex size-5 items-center justify-center rounded {mode.class}">
130
+ <mode.icon class="size-3 {mode.iconClass}" />
131
+ </div>
132
+ <p>
133
+ {mode.label}
134
+ {#if mode.description}
135
+ <span class="text-xs text-muted-foreground lowercase italic"
136
+ >({mode.description})</span
137
+ >
138
+ {/if}
139
+ </p>
140
+ </Select.Item>
141
+ {/each}
142
+ </Select.Group>
143
+ <Select.Separator />
144
+ <Select.Group>
145
+ <Select.GroupHeading>Specific provider</Select.GroupHeading>
146
+ {#each model.providers as provider}
147
+ <Select.Item value={provider.provider}>
148
+ <div class="flex items-center gap-2 capitalize">
149
+ <img
150
+ src={`https://huggingface.co/api/avatars/${provider.provider}`}
151
+ alt={provider.provider}
152
+ class="size-4 rounded"
153
+ />
154
+ {provider.provider}
155
+ </div>
156
+ {#if formatPricingPerToken(provider.pricing)}
157
+ <span class="text-xs text-muted-foreground">
158
+ {formatPricingPerToken(provider.pricing)}
159
+ </span>
160
+ {/if}
161
+ </Select.Item>
162
+ {/each}
163
+ </Select.Group>
164
+ </Select.Content>
165
+ </Select.Root>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
166
  </div>
167
+ <div class="grid gap-3 rounded-lg border border-border p-3.5">
168
+ <div class="space-y-2">
169
+ <div class="flex items-center justify-between gap-2">
170
+ <div>
171
+ <h4 class="text-sm leading-none font-medium">Temperature</h4>
172
+ <p class="mt-0.5 text-xs text-muted-foreground">
173
+ Tunes the creativity vs. predictability trade-off.
174
+ </p>
175
+ </div>
176
+ <Switch
177
+ checked={temperature !== undefined}
178
+ onCheckedChange={(value) => {
179
+ if (value) {
180
+ temperature = 0.5;
181
+ } else {
182
+ temperature = undefined;
183
+ }
184
+ }}
185
+ />
186
  </div>
187
+ {#if temperature !== undefined}
188
+ <div class="flex items-center gap-2" transition:fade={{ duration: 100 }}>
189
+ <Slider
190
+ type="single"
191
+ bind:value={temperature}
192
+ min={0}
193
+ max={2}
194
+ step={0.01}
195
+ class="mt-2"
196
+ />
197
+ <Input
198
+ type="number"
199
+ min={0}
200
+ max={2}
201
+ step={0.1}
202
+ bind:value={temperature}
203
+ class="h-7! w-24!"
204
+ />
205
+ </div>
206
+ {/if}
207
  </div>
208
+ <Separator />
209
+ <div class="space-y-2">
210
+ <div class="flex items-center justify-between gap-2">
211
+ <div>
212
+ <h4 class="text-sm leading-none font-medium">Max Tokens</h4>
213
+ <p class="mt-0.5 text-xs text-muted-foreground">
214
+ Sets the absolute limit for generated content length.
215
+ </p>
216
+ </div>
217
+ <Switch
218
+ checked={max_tokens !== undefined}
219
+ onCheckedChange={(value) => {
220
+ if (value) {
221
+ max_tokens = (maxContentLength ?? 32_000) / 2;
222
+ } else {
223
+ max_tokens = undefined;
224
+ }
225
+ }}
226
  />
227
  </div>
228
+ {#if max_tokens !== undefined}
229
+ <div class="flex items-center gap-2" transition:fade={{ duration: 100 }}>
230
+ <Slider
231
+ type="single"
232
+ bind:value={max_tokens}
233
+ min={0}
234
+ max={maxContentLength ?? 32_000}
235
+ step={256}
236
+ class="mt-2"
237
+ />
238
+ <Input
239
+ type="number"
240
+ min={0}
241
+ max={maxContentLength ?? 32_000}
242
+ step={256}
243
+ bind:value={max_tokens}
244
+ class="h-7! w-24!"
245
+ />
246
+ </div>
247
+ {/if}
 
248
  </div>
249
+ <Separator />
250
+ <div class="space-y-2">
251
+ <div class="flex items-center justify-between gap-2">
252
+ <div>
253
+ <h4 class="text-sm leading-none font-medium">Top-P</h4>
254
+ <p class="mt-0.5 text-xs text-muted-foreground">
255
+ Dynamically filters token selection by probability mass.
256
+ </p>
257
+ </div>
258
+ <Switch
259
+ checked={top_p !== undefined}
260
+ onCheckedChange={(value) => {
261
+ if (value) {
262
+ top_p = 0.5;
263
+ } else {
264
+ top_p = undefined;
265
+ }
266
+ }}
267
  />
268
  </div>
269
+ {#if top_p !== undefined}
270
+ <div class="flex items-center gap-2" transition:fade={{ duration: 100 }}>
271
+ <Slider type="single" bind:value={top_p} min={0} max={2} step={0.01} class="mt-2" />
272
+ <Input
273
+ type="number"
274
+ min={0}
275
+ max={2}
276
+ step={0.1}
277
+ bind:value={top_p}
278
+ class="h-7! w-24!"
279
+ />
280
+ </div>
281
+ {/if}
282
+ </div>
283
  </div>
284
+ <!-- <header class="border-t border-border">
285
+ <h4 class="text-sm leading-none font-medium text-muted-foreground">Provider</h4>
286
+ </header> -->
287
+ <!-- <main class="w-full">
288
+
289
+ </main> -->
290
+ <div class="flex items-center justify-end gap-3 px-3">
291
+ <Button variant="outline">Reset</Button>
292
+ <Button class="flex-1" onclick={handleSave}>
293
+ {#if loading}
294
+ <Spinner className="text-lg" />
295
+ Saving...
296
+ {:else}
297
+ <Save />
298
+ Save settings
299
+ {/if}
300
+ </Button>
301
+ </div>
302
+ </main>
303
+ {/if}
304
  </Dialog.Content>
305
  </Dialog.Root>
src/lib/state/models.svelte.ts CHANGED
@@ -1,21 +1,73 @@
1
  import type { ChatModel } from '$lib/helpers/types';
2
 
3
  const HF_API = 'https://router.huggingface.co/v1/models';
 
4
 
5
- // function mapHFModelToChatModel(hfModel: ChatModel): ChatModel {
6
- // const [author, modelName] = hfModel.modelId.split('/');
7
- // const isTextGen = hfModel.pipeline_tag === 'text-generation';
8
- // const isImageText = hfModel.pipeline_tag === 'image-text-to-text';
9
- // return {
10
- // id: hfModel.modelId,
11
- // modelName,
12
- // author,
13
- // modelId: hfModel.modelId,
14
- // avatarUrl: `https://huggingface.co/api/avatars/${author}`,
15
- // pipeline_tag: isTextGen ? 'text-generation' : isImageText ? 'image-text-to-text' : undefined,
16
- // provider: 'auto'
17
- // };
18
- // }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
19
 
20
  export const modelsState = $state({
21
  models: [] as ChatModel[],
@@ -33,8 +85,7 @@ export async function fetchModels() {
33
  if (!response.ok) throw new Error(response.statusText);
34
 
35
  const { data: models } = (await response.json()) as { data: ChatModel[] };
36
- console.log('models', models);
37
- modelsState.models = models;
38
  } catch (e) {
39
  modelsState.error = e instanceof Error ? e.message : 'Failed to fetch models';
40
  modelsState.models = [];
 
1
  import type { ChatModel } from '$lib/helpers/types';
2
 
3
  const HF_API = 'https://router.huggingface.co/v1/models';
4
+ const STORAGE_KEY = 'hf-playground-models-settings';
5
 
6
+ export interface StoredModelSettings {
7
+ modelId: string;
8
+ temperature?: number;
9
+ top_p?: number;
10
+ max_tokens?: number;
11
+ provider?: string;
12
+ }
13
+
14
+ function loadStoredSettings(): Record<string, StoredModelSettings> {
15
+ if (typeof window === 'undefined') return {};
16
+ try {
17
+ const raw = localStorage.getItem(STORAGE_KEY);
18
+ if (!raw) return {};
19
+ const parsed = JSON.parse(raw) as StoredModelSettings[];
20
+ return Object.fromEntries(parsed.map((s) => [s.modelId, s]));
21
+ } catch {
22
+ return {};
23
+ }
24
+ }
25
+
26
+ function saveStoredSettings(settings: StoredModelSettings[]) {
27
+ if (typeof window === 'undefined') return;
28
+ try {
29
+ localStorage.setItem(STORAGE_KEY, JSON.stringify(settings));
30
+ } catch {
31
+ // ignore
32
+ }
33
+ }
34
+
35
+ function mergeModelsWithStoredSettings(models: ChatModel[]): ChatModel[] {
36
+ const stored = loadStoredSettings();
37
+ return models.map((m) => {
38
+ const custom = stored[m.id];
39
+ if (!custom) return m;
40
+ return {
41
+ ...m,
42
+ temperature: custom.temperature ?? m.temperature,
43
+ top_p: custom.top_p ?? m.top_p,
44
+ max_tokens: custom.max_tokens ?? m.max_tokens,
45
+ provider: custom.provider ?? m.provider ?? 'auto'
46
+ };
47
+ });
48
+ }
49
+
50
+ export function saveModelSettings(
51
+ modelId: string,
52
+ settings: Pick<StoredModelSettings, 'temperature' | 'top_p' | 'max_tokens' | 'provider'>
53
+ ) {
54
+ modelsState.models = modelsState.models.map((m) =>
55
+ m.id === modelId ? { ...m, ...settings } : m
56
+ );
57
+
58
+ const stored = loadStoredSettings();
59
+ const existing = stored[modelId] ?? { modelId };
60
+ const toStore = { ...existing, modelId } as StoredModelSettings & Record<string, unknown>;
61
+ for (const [key, value] of Object.entries(settings)) {
62
+ if (value === undefined) {
63
+ delete toStore[key];
64
+ } else {
65
+ toStore[key] = value;
66
+ }
67
+ }
68
+ stored[modelId] = toStore as StoredModelSettings;
69
+ saveStoredSettings(Object.values(stored));
70
+ }
71
 
72
  export const modelsState = $state({
73
  models: [] as ChatModel[],
 
85
  if (!response.ok) throw new Error(response.statusText);
86
 
87
  const { data: models } = (await response.json()) as { data: ChatModel[] };
88
+ modelsState.models = mergeModelsWithStoredSettings(models);
 
89
  } catch (e) {
90
  modelsState.error = e instanceof Error ? e.message : 'Failed to fetch models';
91
  modelsState.models = [];
src/routes/+page.svelte CHANGED
@@ -40,7 +40,9 @@
40
  isFirstNode: true,
41
  showWelcome: true,
42
  isParentNode: true,
43
- selectedModels: modelsState.models.slice(0, MAX_DEFAULT_MODELS) as ChatModel[]
 
 
44
  }
45
  }
46
  ];
 
40
  isFirstNode: true,
41
  showWelcome: true,
42
  isParentNode: true,
43
+ selectedModels: modelsState.models
44
+ .slice(0, MAX_DEFAULT_MODELS)
45
+ ?.map((m) => m.id) as string[]
46
  }
47
  }
48
  ];