Thomas G. Lopes commited on
Commit
b2170a7
·
1 Parent(s): 64cfbce

query param handling

Browse files
src/lib/components/InferencePlayground/InferencePlayground.svelte CHANGED
@@ -8,30 +8,26 @@
8
  } from "./inferencePlaygroundUtils";
9
 
10
  import { models } from "$lib/stores/models";
11
- import { getActiveProject, session } from "$lib/stores/session";
12
  import { token } from "$lib/stores/token";
13
  import { isMac } from "$lib/utils/platform";
14
  import { HfInference } from "@huggingface/inference";
15
  import { onDestroy } from "svelte";
 
16
  import IconCode from "~icons/carbon/code";
17
  import IconCompare from "~icons/carbon/compare";
18
- import IconDelete from "~icons/carbon/trash-can";
19
  import IconInfo from "~icons/carbon/information";
20
- import IconThrashcan from "~icons/carbon/trash-can";
21
  import PlaygroundConversation from "./InferencePlaygroundConversation.svelte";
22
  import PlaygroundConversationHeader from "./InferencePlaygroundConversationHeader.svelte";
23
  import GenerationConfig from "./InferencePlaygroundGenerationConfig.svelte";
24
  import HFTokenModal from "./InferencePlaygroundHFTokenModal.svelte";
25
  import ModelSelector from "./InferencePlaygroundModelSelector.svelte";
26
  import ModelSelectorModal from "./InferencePlaygroundModelSelectorModal.svelte";
27
- import IconExternal from "~icons/carbon/arrow-up-right";
28
  import InferencePlaygroundProjectSelect from "./InferencePlaygroundProjectSelect.svelte";
29
 
30
  const startMessageUser: ConversationMessage = { role: "user", content: "" };
31
 
32
- $: project = getActiveProject($session);
33
- project = getActiveProject($session); // needed, otherwise its undefined on startup (not sure why).
34
-
35
  let viewCode = false;
36
  let viewSettings = false;
37
  let loading = false;
@@ -43,34 +39,15 @@
43
  latency: number;
44
  generatedTokensCount: number;
45
  }
46
- let generationStats = project.conversations.map(_ => ({ latency: 0, generatedTokensCount: 0 })) as
47
  | [GenerationStatistics]
48
  | [GenerationStatistics, GenerationStatistics];
49
 
50
- $: systemPromptSupported = project.conversations.some(conversation => isSystemPromptSupported(conversation.model));
51
- $: compareActive = project.conversations.length === 2;
52
-
53
- function addMessage(conversationIdx: number) {
54
- const conversation = project.conversations[conversationIdx];
55
- if (!conversation) return;
56
- const msgs = conversation.messages.slice();
57
- conversation.messages = [
58
- ...msgs,
59
- {
60
- role: msgs.at(-1)?.role === "user" ? "assistant" : "user",
61
- content: "",
62
- },
63
- ];
64
- $session = $session;
65
- }
66
-
67
- function deleteMessage(conversationIdx: number, idx: number) {
68
- project.conversations[conversationIdx]?.messages.splice(idx, 1)[0];
69
- $session = $session;
70
- }
71
 
72
  function reset() {
73
- project.conversations.map(conversation => {
74
  conversation.systemMessage.content = "";
75
  conversation.messages = [{ ...startMessageUser }];
76
  });
@@ -140,10 +117,10 @@
140
  return;
141
  }
142
 
143
- for (const [idx, conversation] of project.conversations.entries()) {
144
  if (conversation.messages.at(-1)?.role === "assistant") {
145
  let prefix = "";
146
- if (project.conversations.length === 2) {
147
  prefix = `Error on ${idx === 0 ? "left" : "right"} conversation. `;
148
  }
149
  return alert(`${prefix}Messages must alternate between user/assistant roles.`);
@@ -154,10 +131,10 @@
154
  loading = true;
155
 
156
  try {
157
- const promises = project.conversations.map((conversation, idx) => runInference(conversation, idx));
158
  await Promise.all(promises);
159
  } catch (error) {
160
- for (const conversation of project.conversations) {
161
  if (conversation.messages.at(-1)?.role === "assistant" && !conversation.messages.at(-1)?.content?.trim()) {
162
  conversation.messages.pop();
163
  conversation.messages = [...conversation.messages];
@@ -201,16 +178,16 @@
201
 
202
  function addCompareModel(modelId: ModelWithTokenizer["id"]) {
203
  const model = $models.find(m => m.id === modelId);
204
- if (!model || project.conversations.length === 2) {
205
  return;
206
  }
207
- const newConversation = { ...JSON.parse(JSON.stringify(project.conversations[0])), model };
208
- project.conversations = [...project.conversations, newConversation];
209
  generationStats = [generationStats[0], { latency: 0, generatedTokensCount: 0 }];
210
  }
211
 
212
  function removeCompareModal(conversationIdx: number) {
213
- project.conversations.splice(conversationIdx, 1)[0];
214
  $session = $session;
215
  generationStats.splice(conversationIdx, 1)[0];
216
  generationStats = generationStats;
@@ -253,9 +230,9 @@
253
  placeholder={systemPromptSupported
254
  ? "Enter a custom prompt"
255
  : "System prompt is not supported with the chosen model."}
256
- value={systemPromptSupported ? project.conversations[0].systemMessage.content : ""}
257
  on:input={e => {
258
- for (const conversation of project.conversations) {
259
  conversation.systemMessage.content = e.currentTarget.value;
260
  }
261
  $session = $session;
@@ -268,7 +245,7 @@
268
  <div
269
  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"
270
  >
271
- {#each project.conversations as conversation, conversationIdx}
272
  <div class="max-sm:min-w-full">
273
  {#if compareActive}
274
  <PlaygroundConversationHeader
@@ -279,11 +256,9 @@
279
  {/if}
280
  <PlaygroundConversation
281
  {loading}
282
- {conversation}
283
  {viewCode}
284
  {compareActive}
285
- on:addMessage={() => addMessage(conversationIdx)}
286
- on:deleteMessage={e => deleteMessage(conversationIdx, e.detail)}
287
  on:closeCode={() => (viewCode = false)}
288
  />
289
  </div>
@@ -332,7 +307,7 @@
332
  {#if loading}
333
  <div class="flex flex-none items-center gap-[3px]">
334
  <span class="mr-2">
335
- {#if project.conversations[0].streaming || project.conversations[1]?.streaming}
336
  Stop
337
  {:else}
338
  Cancel
@@ -367,7 +342,7 @@
367
  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"
368
  >
369
  <div class="flex flex-col gap-2">
370
- <ModelSelector bind:conversation={project.conversations[0]} />
371
  <div class="flex items-center gap-2 self-end px-2 text-xs whitespace-nowrap">
372
  <button
373
  class="flex items-center gap-0.5 text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-300"
@@ -377,7 +352,7 @@
377
  Compare
378
  </button>
379
  <a
380
- href="https://huggingface.co/{project.conversations[0].model.id}?inference_provider={project
381
  .conversations[0].provider}"
382
  target="_blank"
383
  class="flex items-center gap-0.5 text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-300"
@@ -388,7 +363,7 @@
388
  </div>
389
  </div>
390
 
391
- <GenerationConfig bind:conversation={project.conversations[0]} />
392
  {#if $token.value}
393
  <button
394
  on:click={token.reset}
@@ -447,7 +422,7 @@
447
 
448
  {#if selectCompareModelOpen}
449
  <ModelSelectorModal
450
- conversation={project.conversations[0]}
451
  on:modelSelected={e => addCompareModel(e.detail)}
452
  on:close={() => (selectCompareModelOpen = false)}
453
  />
 
8
  } from "./inferencePlaygroundUtils";
9
 
10
  import { models } from "$lib/stores/models";
11
+ import { project, session } from "$lib/stores/session";
12
  import { token } from "$lib/stores/token";
13
  import { isMac } from "$lib/utils/platform";
14
  import { HfInference } from "@huggingface/inference";
15
  import { onDestroy } from "svelte";
16
+ import IconExternal from "~icons/carbon/arrow-up-right";
17
  import IconCode from "~icons/carbon/code";
18
  import IconCompare from "~icons/carbon/compare";
 
19
  import IconInfo from "~icons/carbon/information";
20
+ import { default as IconDelete, default as IconThrashcan } from "~icons/carbon/trash-can";
21
  import PlaygroundConversation from "./InferencePlaygroundConversation.svelte";
22
  import PlaygroundConversationHeader from "./InferencePlaygroundConversationHeader.svelte";
23
  import GenerationConfig from "./InferencePlaygroundGenerationConfig.svelte";
24
  import HFTokenModal from "./InferencePlaygroundHFTokenModal.svelte";
25
  import ModelSelector from "./InferencePlaygroundModelSelector.svelte";
26
  import ModelSelectorModal from "./InferencePlaygroundModelSelectorModal.svelte";
 
27
  import InferencePlaygroundProjectSelect from "./InferencePlaygroundProjectSelect.svelte";
28
 
29
  const startMessageUser: ConversationMessage = { role: "user", content: "" };
30
 
 
 
 
31
  let viewCode = false;
32
  let viewSettings = false;
33
  let loading = false;
 
39
  latency: number;
40
  generatedTokensCount: number;
41
  }
42
+ let generationStats = $project.conversations.map(_ => ({ latency: 0, generatedTokensCount: 0 })) as
43
  | [GenerationStatistics]
44
  | [GenerationStatistics, GenerationStatistics];
45
 
46
+ $: systemPromptSupported = $project.conversations.some(conversation => isSystemPromptSupported(conversation.model));
47
+ $: compareActive = $project.conversations.length === 2;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
48
 
49
  function reset() {
50
+ $project.conversations.map(conversation => {
51
  conversation.systemMessage.content = "";
52
  conversation.messages = [{ ...startMessageUser }];
53
  });
 
117
  return;
118
  }
119
 
120
+ for (const [idx, conversation] of $project.conversations.entries()) {
121
  if (conversation.messages.at(-1)?.role === "assistant") {
122
  let prefix = "";
123
+ if ($project.conversations.length === 2) {
124
  prefix = `Error on ${idx === 0 ? "left" : "right"} conversation. `;
125
  }
126
  return alert(`${prefix}Messages must alternate between user/assistant roles.`);
 
131
  loading = true;
132
 
133
  try {
134
+ const promises = $project.conversations.map((conversation, idx) => runInference(conversation, idx));
135
  await Promise.all(promises);
136
  } catch (error) {
137
+ for (const conversation of $project.conversations) {
138
  if (conversation.messages.at(-1)?.role === "assistant" && !conversation.messages.at(-1)?.content?.trim()) {
139
  conversation.messages.pop();
140
  conversation.messages = [...conversation.messages];
 
178
 
179
  function addCompareModel(modelId: ModelWithTokenizer["id"]) {
180
  const model = $models.find(m => m.id === modelId);
181
+ if (!model || $project.conversations.length === 2) {
182
  return;
183
  }
184
+ const newConversation = { ...JSON.parse(JSON.stringify($project.conversations[0])), model };
185
+ $project.conversations = [...$project.conversations, newConversation];
186
  generationStats = [generationStats[0], { latency: 0, generatedTokensCount: 0 }];
187
  }
188
 
189
  function removeCompareModal(conversationIdx: number) {
190
+ $project.conversations.splice(conversationIdx, 1)[0];
191
  $session = $session;
192
  generationStats.splice(conversationIdx, 1)[0];
193
  generationStats = generationStats;
 
230
  placeholder={systemPromptSupported
231
  ? "Enter a custom prompt"
232
  : "System prompt is not supported with the chosen model."}
233
+ value={systemPromptSupported ? $project.conversations[0].systemMessage.content : ""}
234
  on:input={e => {
235
+ for (const conversation of $project.conversations) {
236
  conversation.systemMessage.content = e.currentTarget.value;
237
  }
238
  $session = $session;
 
245
  <div
246
  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"
247
  >
248
+ {#each $project.conversations as conversation, conversationIdx}
249
  <div class="max-sm:min-w-full">
250
  {#if compareActive}
251
  <PlaygroundConversationHeader
 
256
  {/if}
257
  <PlaygroundConversation
258
  {loading}
259
+ bind:conversation
260
  {viewCode}
261
  {compareActive}
 
 
262
  on:closeCode={() => (viewCode = false)}
263
  />
264
  </div>
 
307
  {#if loading}
308
  <div class="flex flex-none items-center gap-[3px]">
309
  <span class="mr-2">
310
+ {#if $project.conversations[0].streaming || $project.conversations[1]?.streaming}
311
  Stop
312
  {:else}
313
  Cancel
 
342
  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"
343
  >
344
  <div class="flex flex-col gap-2">
345
+ <ModelSelector bind:conversation={$project.conversations[0]} />
346
  <div class="flex items-center gap-2 self-end px-2 text-xs whitespace-nowrap">
347
  <button
348
  class="flex items-center gap-0.5 text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-300"
 
352
  Compare
353
  </button>
354
  <a
355
+ href="https://huggingface.co/{$project.conversations[0].model.id}?inference_provider={$project
356
  .conversations[0].provider}"
357
  target="_blank"
358
  class="flex items-center gap-0.5 text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-300"
 
363
  </div>
364
  </div>
365
 
366
+ <GenerationConfig bind:conversation={$project.conversations[0]} />
367
  {#if $token.value}
368
  <button
369
  on:click={token.reset}
 
422
 
423
  {#if selectCompareModelOpen}
424
  <ModelSelectorModal
425
+ conversation={$project.conversations[0]}
426
  on:modelSelected={e => addCompareModel(e.detail)}
427
  on:close={() => (selectCompareModelOpen = false)}
428
  />
src/lib/components/InferencePlayground/InferencePlaygroundConversation.svelte CHANGED
@@ -1,11 +1,11 @@
1
  <script lang="ts">
2
  import type { Conversation } from "$lib/types";
3
 
4
- import { createEventDispatcher, tick } from "svelte";
5
 
 
6
  import CodeSnippets from "./InferencePlaygroundCodeSnippets.svelte";
7
  import Message from "./InferencePlaygroundMessage.svelte";
8
- import IconPlus from "~icons/carbon/add";
9
 
10
  export let conversation: Conversation;
11
  export let loading: boolean;
@@ -16,11 +16,6 @@
16
  let isProgrammaticScroll = true;
17
  let conversationLength = conversation.messages.length;
18
 
19
- const dispatch = createEventDispatcher<{
20
- addMessage: void;
21
- deleteMessage: number;
22
- }>();
23
-
24
  let messageContainer: HTMLDivElement | null = null;
25
 
26
  async function resizeMessageTextAreas() {
@@ -60,6 +55,23 @@
60
  }
61
 
62
  $: viewCode, resizeMessageTextAreas();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
63
  </script>
64
 
65
  <svelte:window on:resize={resizeMessageTextAreas} />
@@ -82,24 +94,24 @@
82
  {#each conversation.messages as message, messageIdx}
83
  <Message
84
  class="border-b"
85
- {message}
86
  {loading}
87
  on:input={resizeMessageTextAreas}
88
- on:delete={() => dispatch("deleteMessage", messageIdx)}
89
  autofocus={!loading && messageIdx === conversation.messages.length - 1}
90
  />
91
  {/each}
92
 
93
  <button
94
  class="flex px-3.5 py-6 hover:bg-gray-50 md:px-6 dark:hover:bg-gray-800/50"
95
- on:click={() => dispatch("addMessage")}
96
  disabled={loading}
97
  >
98
  <div class="flex items-center gap-2 p-0! text-sm font-semibold">
99
  <div class="text-lg">
100
  <IconPlus />
101
  </div>
102
- Add message
103
  </div>
104
  </button>
105
  {:else}
 
1
  <script lang="ts">
2
  import type { Conversation } from "$lib/types";
3
 
4
+ import { tick } from "svelte";
5
 
6
+ import IconPlus from "~icons/carbon/add";
7
  import CodeSnippets from "./InferencePlaygroundCodeSnippets.svelte";
8
  import Message from "./InferencePlaygroundMessage.svelte";
 
9
 
10
  export let conversation: Conversation;
11
  export let loading: boolean;
 
16
  let isProgrammaticScroll = true;
17
  let conversationLength = conversation.messages.length;
18
 
 
 
 
 
 
19
  let messageContainer: HTMLDivElement | null = null;
20
 
21
  async function resizeMessageTextAreas() {
 
55
  }
56
 
57
  $: viewCode, resizeMessageTextAreas();
58
+
59
+ function addMessage() {
60
+ const msgs = conversation.messages.slice();
61
+ conversation.messages = [
62
+ ...msgs,
63
+ {
64
+ role: msgs.at(-1)?.role === "user" ? "assistant" : "user",
65
+ content: "",
66
+ },
67
+ ];
68
+ conversation = conversation;
69
+ }
70
+
71
+ function deleteMessage(idx: number) {
72
+ conversation.messages.splice(idx, 1);
73
+ conversation = conversation;
74
+ }
75
  </script>
76
 
77
  <svelte:window on:resize={resizeMessageTextAreas} />
 
94
  {#each conversation.messages as message, messageIdx}
95
  <Message
96
  class="border-b"
97
+ bind:message
98
  {loading}
99
  on:input={resizeMessageTextAreas}
100
+ on:delete={() => deleteMessage(messageIdx)}
101
  autofocus={!loading && messageIdx === conversation.messages.length - 1}
102
  />
103
  {/each}
104
 
105
  <button
106
  class="flex px-3.5 py-6 hover:bg-gray-50 md:px-6 dark:hover:bg-gray-800/50"
107
+ on:click={addMessage}
108
  disabled={loading}
109
  >
110
  <div class="flex items-center gap-2 p-0! text-sm font-semibold">
111
  <div class="text-lg">
112
  <IconPlus />
113
  </div>
114
+ Add message
115
  </div>
116
  </button>
117
  {:else}
src/lib/components/InferencePlayground/InferencePlaygroundModelSelector.svelte CHANGED
@@ -2,8 +2,8 @@
2
  import type { Conversation, ModelWithTokenizer } from "$lib/types";
3
 
4
  import { models } from "$lib/stores/models";
5
- import Avatar from "../Avatar.svelte";
6
  import IconCaret from "~icons/carbon/chevron-down";
 
7
  import ModelSelectorModal from "./InferencePlaygroundModelSelectorModal.svelte";
8
  import ProviderSelect from "./InferencePlaygroundProviderSelect.svelte";
9
  import { defaultSystemMessage } from "./inferencePlaygroundUtils";
 
2
  import type { Conversation, ModelWithTokenizer } from "$lib/types";
3
 
4
  import { models } from "$lib/stores/models";
 
5
  import IconCaret from "~icons/carbon/chevron-down";
6
+ import Avatar from "../Avatar.svelte";
7
  import ModelSelectorModal from "./InferencePlaygroundModelSelectorModal.svelte";
8
  import ProviderSelect from "./InferencePlaygroundProviderSelect.svelte";
9
  import { defaultSystemMessage } from "./inferencePlaygroundUtils";
src/lib/components/InferencePlayground/InferencePlaygroundProjectSelect.svelte CHANGED
@@ -3,15 +3,15 @@
3
  import { cn } from "$lib/utils/cn";
4
  import { createSelect, createSync } from "@melt-ui/svelte";
5
  import IconCaret from "~icons/carbon/chevron-down";
6
- import IconDelete from "~icons/carbon/trash-can";
7
  import IconCross from "~icons/carbon/close";
8
- import IconSave from "~icons/carbon/save";
9
  import IconEdit from "~icons/carbon/edit";
 
 
10
 
11
  let classNames: string = "";
12
  export { classNames as class };
13
 
14
- $: isDefault = getActiveProject($session).id === "default";
15
 
16
  const {
17
  elements: { trigger, menu, option },
 
3
  import { cn } from "$lib/utils/cn";
4
  import { createSelect, createSync } from "@melt-ui/svelte";
5
  import IconCaret from "~icons/carbon/chevron-down";
 
6
  import IconCross from "~icons/carbon/close";
 
7
  import IconEdit from "~icons/carbon/edit";
8
+ import IconSave from "~icons/carbon/save";
9
+ import IconDelete from "~icons/carbon/trash-can";
10
 
11
  let classNames: string = "";
12
  export { classNames as class };
13
 
14
+ $: isDefault = $session.activeProjectId === "default";
15
 
16
  const {
17
  elements: { trigger, menu, option },
src/lib/stores/models.ts CHANGED
@@ -3,6 +3,6 @@ import type { ModelWithTokenizer } from "$lib/types";
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
  });
 
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/session.ts CHANGED
@@ -42,12 +42,6 @@ function getDefaults() {
42
  const featured = getTrending($models);
43
  const defaultModel = featured[0] ?? $models[0] ?? emptyModel;
44
 
45
- // Parse URL query parameters
46
- const searchParams = new URLSearchParams(window.location.search);
47
- const searchProviders = searchParams.getAll("provider");
48
- const searchModelIds = searchParams.getAll("modelId");
49
- const modelsFromSearch = searchModelIds.map(id => $models.find(model => model.id === id)).filter(Boolean);
50
-
51
  const defaultConversation: Conversation = {
52
  model: defaultModel,
53
  config: { ...defaultGenerationConfig },
@@ -79,23 +73,37 @@ function createSessionStore() {
79
  if (savedData) {
80
  const parsed = safeParse(savedData);
81
  const res = typia.validate<Session>(parsed);
82
- if (res.success) savedSession = parsed;
83
- else localStorage.setItem(LOCAL_STORAGE_KEY, JSON.stringify(savedSession));
 
 
 
84
  }
85
 
86
- // Merge query params with savedSession.
87
  // Query params models and providers take precedence over savedSession's.
88
  // In any case, we try to merge the two, and the amount of conversations
89
  // is the maximum between the two.
90
- // const max = Math.max(savedSession.conversations.length, modelsFromSearch.length, searchProviders.length);
91
- // for (let i = 0; i < max; i++) {
92
- // const conversation = savedSession.conversations[i] ?? defaultConversation;
93
- // savedSession.conversations[i] = {
94
- // ...conversation,
95
- // model: modelsFromSearch[i] ?? conversation.model,
96
- // provider: searchProviders[i] ?? conversation.provider,
97
- // };
98
- // }
 
 
 
 
 
 
 
 
 
 
 
99
 
100
  set(savedSession);
101
  });
@@ -139,7 +147,6 @@ function createSessionStore() {
139
  });
140
  };
141
 
142
- // Override set method to use our custom update
143
  const set: typeof store.set = (...args) => {
144
  update(_ => args[0]);
145
  };
@@ -195,7 +202,7 @@ function createSessionStore() {
195
  }
196
 
197
  const currProject = projects.find(p => p.id === s.activeProjectId);
198
- const newSession = { ...s, projects, activeProjectId: currProject?.id ?? projects[0]?.id! };
199
  return typia.is<Session>(newSession) ? newSession : s;
200
  });
201
  }
@@ -214,5 +221,31 @@ function createSessionStore() {
214
  export const session = createSessionStore();
215
 
216
  export function getActiveProject(s: Session) {
217
- return s.projects.find(p => p.id === s.activeProjectId) ?? s.projects[0]!;
218
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
42
  const featured = getTrending($models);
43
  const defaultModel = featured[0] ?? $models[0] ?? emptyModel;
44
 
 
 
 
 
 
 
45
  const defaultConversation: Conversation = {
46
  model: defaultModel,
47
  config: { ...defaultGenerationConfig },
 
73
  if (savedData) {
74
  const parsed = safeParse(savedData);
75
  const res = typia.validate<Session>(parsed);
76
+ if (res.success) {
77
+ savedSession = parsed;
78
+ } else {
79
+ localStorage.setItem(LOCAL_STORAGE_KEY, JSON.stringify(savedSession));
80
+ }
81
  }
82
 
83
+ // Merge query params with savedSession's default project
84
  // Query params models and providers take precedence over savedSession's.
85
  // In any case, we try to merge the two, and the amount of conversations
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 => $models.find(model => model.id === id)).filter(Boolean);
95
+ if (modelsFromSearch.length > 0) savedSession.activeProjectId = "default";
96
+
97
+ const max = Math.max(dp.conversations.length, modelsFromSearch.length, searchProviders.length);
98
+ for (let i = 0; i < max; i++) {
99
+ const conversation = dp.conversations[i] ?? defaultConversation;
100
+ dp.conversations[i] = {
101
+ ...conversation,
102
+ model: modelsFromSearch[i] ?? conversation.model,
103
+ provider: searchProviders[i] ?? conversation.provider,
104
+ };
105
+ }
106
+ }
107
 
108
  set(savedSession);
109
  });
 
147
  });
148
  };
149
 
 
150
  const set: typeof store.set = (...args) => {
151
  update(_ => args[0]);
152
  };
 
202
  }
203
 
204
  const currProject = projects.find(p => p.id === s.activeProjectId);
205
+ const newSession = { ...s, projects, activeProjectId: currProject?.id ?? projects[0]?.id };
206
  return typia.is<Session>(newSession) ? newSession : s;
207
  });
208
  }
 
221
  export const session = createSessionStore();
222
 
223
  export function getActiveProject(s: Session) {
224
+ return s.projects.find(p => p.id === s.activeProjectId) ?? s.projects[0];
225
  }
226
+
227
+ function createProjectStore() {
228
+ const store = writable<Project>(undefined, set => {
229
+ return session.subscribe(s => {
230
+ set(getActiveProject(s));
231
+ });
232
+ });
233
+
234
+ const update: (typeof store)["update"] = cb => {
235
+ session.update(s => {
236
+ const project = getActiveProject(s);
237
+ const newProject = cb(project);
238
+ const projects = s.projects.map(p => (p.id === project.id ? newProject : p));
239
+ const newSession = { ...s, projects };
240
+ return typia.is<Session>(newSession) ? newSession : s;
241
+ });
242
+ };
243
+
244
+ const set: typeof store.set = (...args) => {
245
+ update(_ => args[0]);
246
+ };
247
+
248
+ return { ...store, update, set };
249
+ }
250
+
251
+ export const project = createProjectStore();