nsarrazin commited on
Commit
5ad620b
·
unverified ·
1 Parent(s): 62917ce

refactor: api cleanup (#1849)

Browse files
package-lock.json CHANGED
@@ -104,6 +104,7 @@
104
  "prettier-plugin-tailwindcss": "^0.6.11",
105
  "prom-client": "^15.1.2",
106
  "sade": "^1.8.1",
 
107
  "svelte": "^5.33.3",
108
  "svelte-check": "^4.0.0",
109
  "svelte-gestures": "^5.1.3",
@@ -8601,6 +8602,22 @@
8601
  "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==",
8602
  "license": "MIT"
8603
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
8604
  "node_modules/cors": {
8605
  "version": "2.8.5",
8606
  "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz",
@@ -11625,6 +11642,19 @@
11625
  "url": "https://github.com/sponsors/sindresorhus"
11626
  }
11627
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
11628
  "node_modules/isexe": {
11629
  "version": "2.0.0",
11630
  "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
@@ -17001,6 +17031,19 @@
17001
  "node": ">=16 || 14 >=14.17"
17002
  }
17003
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
17004
  "node_modules/supports-color": {
17005
  "version": "7.2.0",
17006
  "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
 
104
  "prettier-plugin-tailwindcss": "^0.6.11",
105
  "prom-client": "^15.1.2",
106
  "sade": "^1.8.1",
107
+ "superjson": "^2.2.2",
108
  "svelte": "^5.33.3",
109
  "svelte-check": "^4.0.0",
110
  "svelte-gestures": "^5.1.3",
 
8602
  "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==",
8603
  "license": "MIT"
8604
  },
8605
+ "node_modules/copy-anything": {
8606
+ "version": "3.0.5",
8607
+ "resolved": "https://registry.npmjs.org/copy-anything/-/copy-anything-3.0.5.tgz",
8608
+ "integrity": "sha512-yCEafptTtb4bk7GLEQoM8KVJpxAfdBJYaXyzQEgQQQgYrZiDp8SJmGKlYza6CYjEDNstAdNdKA3UuoULlEbS6w==",
8609
+ "dev": true,
8610
+ "license": "MIT",
8611
+ "dependencies": {
8612
+ "is-what": "^4.1.8"
8613
+ },
8614
+ "engines": {
8615
+ "node": ">=12.13"
8616
+ },
8617
+ "funding": {
8618
+ "url": "https://github.com/sponsors/mesqueeb"
8619
+ }
8620
+ },
8621
  "node_modules/cors": {
8622
  "version": "2.8.5",
8623
  "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz",
 
11642
  "url": "https://github.com/sponsors/sindresorhus"
11643
  }
11644
  },
11645
+ "node_modules/is-what": {
11646
+ "version": "4.1.16",
11647
+ "resolved": "https://registry.npmjs.org/is-what/-/is-what-4.1.16.tgz",
11648
+ "integrity": "sha512-ZhMwEosbFJkA0YhFnNDgTM4ZxDRsS6HqTo7qsZM08fehyRYIYa0yHu5R6mgo1n/8MgaPBXiPimPD77baVFYg+A==",
11649
+ "dev": true,
11650
+ "license": "MIT",
11651
+ "engines": {
11652
+ "node": ">=12.13"
11653
+ },
11654
+ "funding": {
11655
+ "url": "https://github.com/sponsors/mesqueeb"
11656
+ }
11657
+ },
11658
  "node_modules/isexe": {
11659
  "version": "2.0.0",
11660
  "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
 
17031
  "node": ">=16 || 14 >=14.17"
17032
  }
17033
  },
17034
+ "node_modules/superjson": {
17035
+ "version": "2.2.2",
17036
+ "resolved": "https://registry.npmjs.org/superjson/-/superjson-2.2.2.tgz",
17037
+ "integrity": "sha512-5JRxVqC8I8NuOUjzBbvVJAKNM8qoVuH0O77h4WInc/qC2q5IreqKxYwgkga3PfA22OayK2ikceb/B26dztPl+Q==",
17038
+ "dev": true,
17039
+ "license": "MIT",
17040
+ "dependencies": {
17041
+ "copy-anything": "^3.0.2"
17042
+ },
17043
+ "engines": {
17044
+ "node": ">=16"
17045
+ }
17046
+ },
17047
  "node_modules/supports-color": {
17048
  "version": "7.2.0",
17049
  "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
package.json CHANGED
@@ -60,6 +60,7 @@
60
  "prettier-plugin-tailwindcss": "^0.6.11",
61
  "prom-client": "^15.1.2",
62
  "sade": "^1.8.1",
 
63
  "svelte": "^5.33.3",
64
  "svelte-check": "^4.0.0",
65
  "svelte-gestures": "^5.1.3",
 
60
  "prettier-plugin-tailwindcss": "^0.6.11",
61
  "prom-client": "^15.1.2",
62
  "sade": "^1.8.1",
63
+ "superjson": "^2.2.2",
64
  "svelte": "^5.33.3",
65
  "svelte-check": "^4.0.0",
66
  "svelte-gestures": "^5.1.3",
src/lib/APIClient.ts CHANGED
@@ -2,6 +2,7 @@ import type { App } from "$api";
2
  import { base } from "$app/paths";
3
  import { treaty, type Treaty } from "@elysiajs/eden";
4
  import { browser } from "$app/environment";
 
5
 
6
  export function useAPIClient({ fetch }: { fetch?: Treaty.Config["fetcher"] } = {}) {
7
  let url;
@@ -26,30 +27,26 @@ export function useAPIClient({ fetch }: { fetch?: Treaty.Config["fetcher"] } = {
26
  url = `${window.location.origin}${base}/api/v2`;
27
  }
28
  const app = treaty<App>(url, { fetcher: fetch });
29
-
30
  return app;
31
  }
32
 
33
- export function throwOnErrorNullable<T extends Record<number, unknown>>(
34
  response: Treaty.TreatyResponse<T>
35
  ): T[200] {
36
  if (response.error) {
37
  throw new Error(JSON.stringify(response.error));
38
  }
39
 
40
- return response.data as T[200];
 
 
41
  }
42
 
43
- export function throwOnError<T extends Record<number, unknown>>(
44
- response: Treaty.TreatyResponse<T>
45
- ): NonNullable<T[200]> {
46
- if (response.error) {
47
- throw new Error(JSON.stringify(response.error));
48
- }
49
-
50
- if (response.data === null) {
51
- throw new Error("No data received on API call");
52
  }
53
-
54
- return response.data as NonNullable<T[200]>;
55
- }
 
2
  import { base } from "$app/paths";
3
  import { treaty, type Treaty } from "@elysiajs/eden";
4
  import { browser } from "$app/environment";
5
+ import superjson from "superjson";
6
 
7
  export function useAPIClient({ fetch }: { fetch?: Treaty.Config["fetcher"] } = {}) {
8
  let url;
 
27
  url = `${window.location.origin}${base}/api/v2`;
28
  }
29
  const app = treaty<App>(url, { fetcher: fetch });
 
30
  return app;
31
  }
32
 
33
+ export function handleResponse<T extends Record<number, unknown>>(
34
  response: Treaty.TreatyResponse<T>
35
  ): T[200] {
36
  if (response.error) {
37
  throw new Error(JSON.stringify(response.error));
38
  }
39
 
40
+ return superjson.parse(
41
+ typeof response.data === "string" ? response.data : JSON.stringify(response.data)
42
+ ) as T[200];
43
  }
44
 
45
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
46
+ export type Success<T extends (...args: any) => any> =
47
+ Awaited<ReturnType<T>> extends {
48
+ data: infer D;
49
+ error: unknown;
 
 
 
 
50
  }
51
+ ? D
52
+ : never;
 
src/lib/components/AssistantSettings.svelte CHANGED
@@ -55,8 +55,8 @@
55
  let inputMessage3 = $state(assistant?.exampleInputs[2] ?? "");
56
  let inputMessage4 = $state(assistant?.exampleInputs[3] ?? "");
57
 
58
- function resetErrors() {
59
- errors = [];
60
  }
61
 
62
  function onFilesChange(e: Event) {
@@ -70,7 +70,7 @@
70
  return;
71
  }
72
  files = inputEl.files;
73
- resetErrors();
74
  deleteExistingAvatar = false;
75
  }
76
  }
@@ -164,6 +164,7 @@
164
  } else {
165
  $error = response.statusText;
166
  }
 
167
  }
168
  } else {
169
  response = await fetch(`${base}/api/assistant`, {
@@ -181,6 +182,7 @@
181
  } else {
182
  $error = response.statusText;
183
  }
 
184
  }
185
  }
186
  }}
@@ -245,6 +247,7 @@
245
  e.stopPropagation();
246
  files = null;
247
  deleteExistingAvatar = true;
 
248
  }}
249
  class="mx-auto w-max text-center text-xs text-gray-600 hover:underline"
250
  >
@@ -271,6 +274,7 @@
271
  class="w-full rounded-lg border-2 border-gray-200 bg-gray-100 p-2"
272
  placeholder="Assistant Name"
273
  value={assistant?.name ?? ""}
 
274
  />
275
  <p class="text-xs text-red-500">{getError("name")}</p>
276
  </label>
@@ -282,6 +286,7 @@
282
  class="h-15 w-full rounded-lg border-2 border-gray-200 bg-gray-100 p-2"
283
  placeholder="It knows everything about python"
284
  value={assistant?.description ?? ""}
 
285
  ></textarea>
286
  <p class="text-xs text-red-500">{getError("description")}</p>
287
  </label>
@@ -293,6 +298,7 @@
293
  name="modelId"
294
  class="w-full rounded-lg border-2 border-gray-200 bg-gray-100 p-2"
295
  bind:value={modelId}
 
296
  >
297
  {#each models.filter((model) => !model.unlisted) as model}
298
  <option value={model.id}>{model.displayName}</option>
@@ -415,12 +421,14 @@
415
  placeholder="Start Message 1"
416
  bind:value={inputMessage1}
417
  class="w-full rounded-lg border-2 border-gray-200 bg-gray-100 p-2"
 
418
  />
419
  <input
420
  name="exampleInput2"
421
  placeholder="Start Message 2"
422
  bind:value={inputMessage2}
423
  class="w-full rounded-lg border-2 border-gray-200 bg-gray-100 p-2"
 
424
  />
425
 
426
  <input
@@ -428,12 +436,14 @@
428
  placeholder="Start Message 3"
429
  bind:value={inputMessage3}
430
  class="w-full rounded-lg border-2 border-gray-200 bg-gray-100 p-2"
 
431
  />
432
  <input
433
  name="exampleInput4"
434
  placeholder="Start Message 4"
435
  bind:value={inputMessage4}
436
  class="w-full rounded-lg border-2 border-gray-200 bg-gray-100 p-2"
 
437
  />
438
  </div>
439
  <p class="text-xs text-red-500">{getError("inputMessage1")}</p>
@@ -524,6 +534,7 @@
524
  class="w-full rounded-lg border-2 border-gray-200 bg-gray-100 p-2"
525
  placeholder="wikipedia.org,bbc.com"
526
  value={assistant?.rag?.allowedDomains?.join(",") ?? ""}
 
527
  />
528
  <p class="text-xs text-red-500">{getError("ragDomainList")}</p>
529
  {/if}
@@ -550,6 +561,7 @@
550
  class="w-full rounded-lg border-2 border-gray-200 bg-gray-100 p-2"
551
  placeholder="https://raw.githubusercontent.com/huggingface/chat-ui/main/README.md"
552
  value={assistant?.rag?.allowedLinks.join(",") ?? ""}
 
553
  />
554
  <p class="text-xs text-red-500">{getError("ragLinkList")}</p>
555
  {/if}
@@ -605,6 +617,7 @@
605
  class="min-h-[8lh] flex-1 rounded-lg border-2 border-gray-200 bg-gray-100 p-2 text-sm"
606
  placeholder="You'll act as..."
607
  bind:value={systemPrompt}
 
608
  ></textarea>
609
  {#if modelId}
610
  {@const model = models.find((_model) => _model.id === modelId)}
 
55
  let inputMessage3 = $state(assistant?.exampleInputs[2] ?? "");
56
  let inputMessage4 = $state(assistant?.exampleInputs[3] ?? "");
57
 
58
+ function clearError(field: string) {
59
+ errors = errors.filter((e) => e.field !== field);
60
  }
61
 
62
  function onFilesChange(e: Event) {
 
70
  return;
71
  }
72
  files = inputEl.files;
73
+ clearError("avatar");
74
  deleteExistingAvatar = false;
75
  }
76
  }
 
164
  } else {
165
  $error = response.statusText;
166
  }
167
+ loading = false;
168
  }
169
  } else {
170
  response = await fetch(`${base}/api/assistant`, {
 
182
  } else {
183
  $error = response.statusText;
184
  }
185
+ loading = false;
186
  }
187
  }
188
  }}
 
247
  e.stopPropagation();
248
  files = null;
249
  deleteExistingAvatar = true;
250
+ clearError("avatar");
251
  }}
252
  class="mx-auto w-max text-center text-xs text-gray-600 hover:underline"
253
  >
 
274
  class="w-full rounded-lg border-2 border-gray-200 bg-gray-100 p-2"
275
  placeholder="Assistant Name"
276
  value={assistant?.name ?? ""}
277
+ oninput={() => clearError("name")}
278
  />
279
  <p class="text-xs text-red-500">{getError("name")}</p>
280
  </label>
 
286
  class="h-15 w-full rounded-lg border-2 border-gray-200 bg-gray-100 p-2"
287
  placeholder="It knows everything about python"
288
  value={assistant?.description ?? ""}
289
+ oninput={() => clearError("description")}
290
  ></textarea>
291
  <p class="text-xs text-red-500">{getError("description")}</p>
292
  </label>
 
298
  name="modelId"
299
  class="w-full rounded-lg border-2 border-gray-200 bg-gray-100 p-2"
300
  bind:value={modelId}
301
+ onchange={() => clearError("modelId")}
302
  >
303
  {#each models.filter((model) => !model.unlisted) as model}
304
  <option value={model.id}>{model.displayName}</option>
 
421
  placeholder="Start Message 1"
422
  bind:value={inputMessage1}
423
  class="w-full rounded-lg border-2 border-gray-200 bg-gray-100 p-2"
424
+ oninput={() => clearError("inputMessage1")}
425
  />
426
  <input
427
  name="exampleInput2"
428
  placeholder="Start Message 2"
429
  bind:value={inputMessage2}
430
  class="w-full rounded-lg border-2 border-gray-200 bg-gray-100 p-2"
431
+ oninput={() => clearError("inputMessage1")}
432
  />
433
 
434
  <input
 
436
  placeholder="Start Message 3"
437
  bind:value={inputMessage3}
438
  class="w-full rounded-lg border-2 border-gray-200 bg-gray-100 p-2"
439
+ oninput={() => clearError("inputMessage1")}
440
  />
441
  <input
442
  name="exampleInput4"
443
  placeholder="Start Message 4"
444
  bind:value={inputMessage4}
445
  class="w-full rounded-lg border-2 border-gray-200 bg-gray-100 p-2"
446
+ oninput={() => clearError("inputMessage1")}
447
  />
448
  </div>
449
  <p class="text-xs text-red-500">{getError("inputMessage1")}</p>
 
534
  class="w-full rounded-lg border-2 border-gray-200 bg-gray-100 p-2"
535
  placeholder="wikipedia.org,bbc.com"
536
  value={assistant?.rag?.allowedDomains?.join(",") ?? ""}
537
+ oninput={() => clearError("ragDomainList")}
538
  />
539
  <p class="text-xs text-red-500">{getError("ragDomainList")}</p>
540
  {/if}
 
561
  class="w-full rounded-lg border-2 border-gray-200 bg-gray-100 p-2"
562
  placeholder="https://raw.githubusercontent.com/huggingface/chat-ui/main/README.md"
563
  value={assistant?.rag?.allowedLinks.join(",") ?? ""}
564
+ oninput={() => clearError("ragLinkList")}
565
  />
566
  <p class="text-xs text-red-500">{getError("ragLinkList")}</p>
567
  {/if}
 
617
  class="min-h-[8lh] flex-1 rounded-lg border-2 border-gray-200 bg-gray-100 p-2 text-sm"
618
  placeholder="You'll act as..."
619
  bind:value={systemPrompt}
620
+ oninput={() => clearError("preprompt")}
621
  ></textarea>
622
  {#if modelId}
623
  {@const model = models.find((_model) => _model.id === modelId)}
src/lib/components/NavConversationItem.svelte CHANGED
@@ -93,7 +93,7 @@
93
  onclick={(e) => {
94
  e.preventDefault();
95
  confirmDelete = false;
96
- dispatch("deleteConversation", conv.id);
97
  }}
98
  >
99
  <CarbonCheckmark
@@ -109,7 +109,7 @@
109
  e.preventDefault();
110
  const newTitle = prompt("Edit this conversation title:", conv.title);
111
  if (!newTitle) return;
112
- dispatch("editConversationTitle", { id: conv.id, title: newTitle });
113
  }}
114
  >
115
  <CarbonEdit class="text-xs text-gray-400 hover:text-gray-500 dark:hover:text-gray-300" />
@@ -122,7 +122,7 @@
122
  onclick={(event) => {
123
  event.preventDefault();
124
  if (event.shiftKey) {
125
- dispatch("deleteConversation", conv.id);
126
  } else {
127
  confirmDelete = true;
128
  }
 
93
  onclick={(e) => {
94
  e.preventDefault();
95
  confirmDelete = false;
96
+ dispatch("deleteConversation", conv.id.toString());
97
  }}
98
  >
99
  <CarbonCheckmark
 
109
  e.preventDefault();
110
  const newTitle = prompt("Edit this conversation title:", conv.title);
111
  if (!newTitle) return;
112
+ dispatch("editConversationTitle", { id: conv.id.toString(), title: newTitle });
113
  }}
114
  >
115
  <CarbonEdit class="text-xs text-gray-400 hover:text-gray-500 dark:hover:text-gray-300" />
 
122
  onclick={(event) => {
123
  event.preventDefault();
124
  if (event.shiftKey) {
125
+ dispatch("deleteConversation", conv.id.toString());
126
  } else {
127
  confirmDelete = true;
128
  }
src/lib/components/NavMenu.svelte CHANGED
@@ -29,8 +29,7 @@
29
  import { usePublicConfig } from "$lib/utils/PublicConfig.svelte";
30
 
31
  import { isVirtualKeyboard } from "$lib/utils/isVirtualKeyboard";
32
- import { useAPIClient, throwOnError } from "$lib/APIClient";
33
- import { jsonSerialize } from "$lib/utils/serialize";
34
 
35
  const publicConfig = usePublicConfig();
36
  const client = useAPIClient();
@@ -77,13 +76,8 @@
77
  p,
78
  },
79
  })
80
- .then(throwOnError)
81
- .then(({ conversations }) =>
82
- conversations.map((conv) => ({
83
- ...jsonSerialize(conv),
84
- updatedAt: new Date(conv.updatedAt),
85
- }))
86
- )
87
  .catch(() => []);
88
 
89
  if (newConvs.length === 0) {
 
29
  import { usePublicConfig } from "$lib/utils/PublicConfig.svelte";
30
 
31
  import { isVirtualKeyboard } from "$lib/utils/isVirtualKeyboard";
32
+ import { useAPIClient, handleResponse } from "$lib/APIClient";
 
33
 
34
  const publicConfig = usePublicConfig();
35
  const client = useAPIClient();
 
76
  p,
77
  },
78
  })
79
+ .then(handleResponse)
80
+ .then((r) => r.conversations)
 
 
 
 
 
81
  .catch(() => []);
82
 
83
  if (newConvs.length === 0) {
src/lib/components/ToolBadge.svelte CHANGED
@@ -2,7 +2,7 @@
2
  import ToolLogo from "./ToolLogo.svelte";
3
  import { base } from "$app/paths";
4
  import { browser } from "$app/environment";
5
- import { throwOnError, useAPIClient } from "$lib/APIClient";
6
 
7
  interface Props {
8
  toolId: string;
@@ -17,7 +17,7 @@
17
  class="relative flex items-center justify-center space-x-2 rounded border border-gray-300 bg-gray-200 px-2 py-1"
18
  >
19
  {#if browser}
20
- {#await client.tools({ id: toolId }).get().then(throwOnError) then value}
21
  {#key value.color + value.icon}
22
  <ToolLogo color={value.color} icon={value.icon} size="sm" />
23
  {/key}
 
2
  import ToolLogo from "./ToolLogo.svelte";
3
  import { base } from "$app/paths";
4
  import { browser } from "$app/environment";
5
+ import { handleResponse, useAPIClient } from "$lib/APIClient";
6
 
7
  interface Props {
8
  toolId: string;
 
17
  class="relative flex items-center justify-center space-x-2 rounded border border-gray-300 bg-gray-200 px-2 py-1"
18
  >
19
  {#if browser}
20
+ {#await client.tools({ id: toolId }).get().then(handleResponse) then value}
21
  {#key value.color + value.icon}
22
  <ToolLogo color={value.color} icon={value.icon} size="sm" />
23
  {/key}
src/lib/components/chat/AssistantIntroduction.svelte CHANGED
@@ -18,14 +18,13 @@
18
  import { usePublicConfig } from "$lib/utils/PublicConfig.svelte";
19
 
20
  import { page } from "$app/state";
21
- import type { Serialize } from "$lib/utils/serialize";
22
 
23
  const publicConfig = usePublicConfig();
24
 
25
  interface Props {
26
  models: Model[];
27
  assistant: Pick<
28
- Serialize<Assistant>,
29
  | "avatar"
30
  | "name"
31
  | "rag"
 
18
  import { usePublicConfig } from "$lib/utils/PublicConfig.svelte";
19
 
20
  import { page } from "$app/state";
 
21
 
22
  const publicConfig = usePublicConfig();
23
 
24
  interface Props {
25
  models: Model[];
26
  assistant: Pick<
27
+ Assistant,
28
  | "avatar"
29
  | "name"
30
  | "rag"
src/lib/components/chat/ChatInput.svelte CHANGED
@@ -23,7 +23,6 @@
23
  import { captureScreen } from "$lib/utils/screenshot";
24
  import IconScreenshot from "../icons/IconScreenshot.svelte";
25
  import { loginModalOpen } from "$lib/stores/loginModal";
26
- import type { Serialize } from "$lib/utils/serialize";
27
 
28
  import { isVirtualKeyboard } from "$lib/utils/isVirtualKeyboard";
29
  interface Props {
@@ -33,7 +32,7 @@
33
  placeholder?: string;
34
  loading?: boolean;
35
  disabled?: boolean;
36
- assistant?: Serialize<Assistant> | undefined;
37
  modelHasTools?: boolean;
38
  modelIsMultimodal?: boolean;
39
  children?: import("svelte").Snippet;
 
23
  import { captureScreen } from "$lib/utils/screenshot";
24
  import IconScreenshot from "../icons/IconScreenshot.svelte";
25
  import { loginModalOpen } from "$lib/stores/loginModal";
 
26
 
27
  import { isVirtualKeyboard } from "$lib/utils/isVirtualKeyboard";
28
  interface Props {
 
32
  placeholder?: string;
33
  loading?: boolean;
34
  disabled?: boolean;
35
+ assistant?: Assistant | undefined;
36
  modelHasTools?: boolean;
37
  modelIsMultimodal?: boolean;
38
  children?: import("svelte").Snippet;
src/lib/components/chat/ChatWindow.svelte CHANGED
@@ -37,7 +37,6 @@
37
  import { cubicInOut } from "svelte/easing";
38
  import type { ToolFront } from "$lib/types/Tool";
39
  import { loginModalOpen } from "$lib/stores/loginModal";
40
- import type { Serialize } from "$lib/utils/serialize";
41
  import { beforeNavigate } from "$app/navigation";
42
  import { isVirtualKeyboard } from "$lib/utils/isVirtualKeyboard";
43
 
@@ -49,7 +48,7 @@
49
  shared?: boolean;
50
  currentModel: Model;
51
  models: Model[];
52
- assistant?: Serialize<Assistant> | undefined;
53
  preprompt?: string | undefined;
54
  files?: File[];
55
  }
 
37
  import { cubicInOut } from "svelte/easing";
38
  import type { ToolFront } from "$lib/types/Tool";
39
  import { loginModalOpen } from "$lib/stores/loginModal";
 
40
  import { beforeNavigate } from "$app/navigation";
41
  import { isVirtualKeyboard } from "$lib/utils/isVirtualKeyboard";
42
 
 
48
  shared?: boolean;
49
  currentModel: Model;
50
  models: Model[];
51
+ assistant?: Assistant | undefined;
52
  preprompt?: string | undefined;
53
  files?: File[];
54
  }
src/lib/components/chat/Search.svelte CHANGED
@@ -7,11 +7,7 @@
7
  </script>
8
 
9
  <script lang="ts">
10
- import { base } from "$app/paths";
11
-
12
  import { debounce } from "$lib/utils/debounce";
13
-
14
- import type { GETSearchEndpointReturn } from "../../../routes/api/conversations/search/+server";
15
  import NavConversationItem from "../NavConversationItem.svelte";
16
  import { titles } from "../NavMenu.svelte";
17
  import { beforeNavigate } from "$app/navigation";
@@ -19,6 +15,9 @@
19
  import CarbonClose from "~icons/carbon/close";
20
  import { fly } from "svelte/transition";
21
  import InfiniteScroll from "../InfiniteScroll.svelte";
 
 
 
22
 
23
  let searchContainer: HTMLDivElement | undefined = $state(undefined);
24
  let inputElement: HTMLInputElement | undefined = $state(undefined);
@@ -29,7 +28,7 @@
29
 
30
  let pending: boolean = $state(false);
31
 
32
- let conversations: GETSearchEndpointReturn = $state([]);
33
 
34
  let page: number = $state(0);
35
 
@@ -75,19 +74,14 @@
75
  };
76
 
77
  async function handleVisible(v: string) {
78
- const newConvs = await fetch(`${base}/api/conversations/search?q=${v}&p=${page++}`)
79
- .then(async (r) => {
80
- if (r.ok) {
81
- return await r.json().then((conversations) =>
82
- conversations.map((conv: GETSearchEndpointReturn[number]) => ({
83
- ...conv,
84
- updatedAt: new Date(conv.updatedAt),
85
- }))
86
- );
87
- } else {
88
- return [];
89
- }
90
  })
 
91
  .catch(() => []);
92
 
93
  if (newConvs.length === 0) {
 
7
  </script>
8
 
9
  <script lang="ts">
 
 
10
  import { debounce } from "$lib/utils/debounce";
 
 
11
  import NavConversationItem from "../NavConversationItem.svelte";
12
  import { titles } from "../NavMenu.svelte";
13
  import { beforeNavigate } from "$app/navigation";
 
15
  import CarbonClose from "~icons/carbon/close";
16
  import { fly } from "svelte/transition";
17
  import InfiniteScroll from "../InfiniteScroll.svelte";
18
+ import { handleResponse, useAPIClient, type Success } from "$lib/APIClient";
19
+
20
+ const client = useAPIClient();
21
 
22
  let searchContainer: HTMLDivElement | undefined = $state(undefined);
23
  let inputElement: HTMLInputElement | undefined = $state(undefined);
 
28
 
29
  let pending: boolean = $state(false);
30
 
31
+ let conversations: NonNullable<Success<typeof client.conversations.search.get>> = $state([]);
32
 
33
  let page: number = $state(0);
34
 
 
74
  };
75
 
76
  async function handleVisible(v: string) {
77
+ const newConvs = await client.conversations.search
78
+ .get({
79
+ query: {
80
+ q: v,
81
+ p: page++,
82
+ },
 
 
 
 
 
 
83
  })
84
+ .then(handleResponse)
85
  .catch(() => []);
86
 
87
  if (newConvs.length === 0) {
src/lib/server/api/index.ts CHANGED
@@ -11,9 +11,14 @@ import { base } from "$app/paths";
11
  import { swagger } from "@elysiajs/swagger";
12
  import { config } from "$lib/server/config";
13
 
 
 
14
  const prefix = `${base}/api/v2` as unknown as "";
15
 
16
  export const app = new Elysia({ prefix })
 
 
 
17
  .use(
18
  swagger({
19
  documentation: {
 
11
  import { swagger } from "@elysiajs/swagger";
12
  import { config } from "$lib/server/config";
13
 
14
+ import superjson from "superjson";
15
+
16
  const prefix = `${base}/api/v2` as unknown as "";
17
 
18
  export const app = new Elysia({ prefix })
19
+ .mapResponse(({ response }) => {
20
+ return new Response(superjson.stringify(response));
21
+ })
22
  .use(
23
  swagger({
24
  documentation: {
src/lib/server/api/routes/groups/assistants.ts CHANGED
@@ -7,19 +7,8 @@ import { SortKey, type Assistant } from "$lib/types/Assistant";
7
  import type { User } from "$lib/types/User";
8
  import { ReviewStatus } from "$lib/types/Review";
9
  import { generateQueryTokens } from "$lib/utils/searchTokens";
10
- import { jsonSerialize, type Serialize } from "$lib/utils/serialize";
11
  import { config } from "$lib/server/config";
12
 
13
- export type GETAssistantsSearchResponse = {
14
- assistants: Array<Serialize<Assistant>>;
15
- selectedModel: string;
16
- numTotalItems: number;
17
- numItemsPerPage: number;
18
- query: string | null;
19
- sort: SortKey;
20
- showUnfeatured: boolean;
21
- };
22
-
23
  const NUM_PER_PAGE = 24;
24
 
25
  export const assistantGroup = new Elysia().use(authPlugin).group("/assistants", (app) => {
@@ -95,7 +84,7 @@ export const assistantGroup = new Elysia().use(authPlugin).group("/assistants",
95
  const numTotalItems = await collections.assistants.countDocuments(filter);
96
 
97
  return {
98
- assistants: jsonSerialize(assistants),
99
  selectedModel: modelId ?? "",
100
  numTotalItems,
101
  numItemsPerPage: NUM_PER_PAGE,
 
7
  import type { User } from "$lib/types/User";
8
  import { ReviewStatus } from "$lib/types/Review";
9
  import { generateQueryTokens } from "$lib/utils/searchTokens";
 
10
  import { config } from "$lib/server/config";
11
 
 
 
 
 
 
 
 
 
 
 
12
  const NUM_PER_PAGE = 24;
13
 
14
  export const assistantGroup = new Elysia().use(authPlugin).group("/assistants", (app) => {
 
84
  const numTotalItems = await collections.assistants.countDocuments(filter);
85
 
86
  return {
87
+ assistants,
88
  selectedModel: modelId ?? "",
89
  numTotalItems,
90
  numItemsPerPage: NUM_PER_PAGE,
src/lib/server/api/routes/groups/conversations.ts CHANGED
@@ -6,8 +6,10 @@ import { authCondition } from "$lib/server/auth";
6
  import { models } from "$lib/server/models";
7
  import { convertLegacyConversation } from "$lib/utils/tree/convertLegacyConversation";
8
  import type { Conversation } from "$lib/types/Conversation";
9
- import { jsonSerialize } from "$lib/utils/serialize";
10
  import { CONV_NUM_PER_PAGE } from "$lib/constants/pagination";
 
 
11
 
12
  export const conversationGroup = new Elysia().use(authPlugin).group("/conversations", (app) => {
13
  return app
@@ -64,6 +66,252 @@ export const conversationGroup = new Elysia().use(authPlugin).group("/conversati
64
  });
65
  return res.deletedCount;
66
  })
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
67
  .group(
68
  "/:id",
69
  {
@@ -126,18 +374,16 @@ export const conversationGroup = new Elysia().use(authPlugin).group("/conversati
126
  return { conversation: convertedConv };
127
  })
128
  .get("", async ({ conversation }) => {
129
- return jsonSerialize({
130
  messages: conversation.messages,
131
  title: conversation.title,
132
  model: conversation.model,
133
  preprompt: conversation.preprompt,
134
  rootMessageId: conversation.rootMessageId,
135
  assistant: conversation.assistantId
136
- ? jsonSerialize(
137
- (await collections.assistants.findOne({
138
- _id: new ObjectId(conversation.assistantId),
139
- })) ?? undefined
140
- )
141
  : undefined,
142
  id: conversation._id.toString(),
143
  updatedAt: conversation.updatedAt,
@@ -145,7 +391,7 @@ export const conversationGroup = new Elysia().use(authPlugin).group("/conversati
145
  assistantId: conversation.assistantId,
146
  modelTools: models.find((m) => m.id == conversation.model)?.tools ?? false,
147
  shared: conversation.shared,
148
- });
149
  })
150
  .post("", () => {
151
  // todo: post new message
 
6
  import { models } from "$lib/server/models";
7
  import { convertLegacyConversation } from "$lib/utils/tree/convertLegacyConversation";
8
  import type { Conversation } from "$lib/types/Conversation";
9
+
10
  import { CONV_NUM_PER_PAGE } from "$lib/constants/pagination";
11
+ import pkg from "natural";
12
+ const { PorterStemmer } = pkg;
13
 
14
  export const conversationGroup = new Elysia().use(authPlugin).group("/conversations", (app) => {
15
  return app
 
66
  });
67
  return res.deletedCount;
68
  })
69
+ .get(
70
+ "/search",
71
+ async ({ locals, query }) => {
72
+ const searchQuery = query.q;
73
+ const p = query.p ?? 0;
74
+
75
+ if (!searchQuery || searchQuery.length < 3) {
76
+ return [];
77
+ }
78
+
79
+ if (!locals.user?._id && !locals.sessionId) {
80
+ throw new Error("Must have a valid session or user");
81
+ }
82
+
83
+ const convs = await collections.conversations
84
+ .find({
85
+ sessionId: undefined,
86
+ ...authCondition(locals),
87
+ $text: { $search: searchQuery },
88
+ })
89
+ .sort({
90
+ updatedAt: -1, // Sort by date updated in descending order
91
+ })
92
+ .project<
93
+ Pick<
94
+ Conversation,
95
+ "_id" | "title" | "updatedAt" | "model" | "assistantId" | "messages" | "userId"
96
+ >
97
+ >({
98
+ title: 1,
99
+ updatedAt: 1,
100
+ model: 1,
101
+ assistantId: 1,
102
+ messages: 1,
103
+ userId: 1,
104
+ })
105
+ .skip(p * 5)
106
+ .limit(5)
107
+ .toArray()
108
+ .then((convs) =>
109
+ convs.map((conv) => {
110
+ let matchedContent = "";
111
+ let matchedText = "";
112
+
113
+ // Find the best match using stemming to handle MongoDB's text search behavior
114
+ let bestMatch = null;
115
+ let bestMatchLength = 0;
116
+
117
+ // Simple function to find the best match in content
118
+ const findBestMatch = (
119
+ content: string,
120
+ query: string
121
+ ): { start: number; end: number; text: string } | null => {
122
+ const contentLower = content.toLowerCase();
123
+ const queryLower = query.toLowerCase();
124
+
125
+ // Try exact word boundary match first
126
+ const wordRegex = new RegExp(
127
+ `\\b${queryLower.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\b`,
128
+ "gi"
129
+ );
130
+ const wordMatch = wordRegex.exec(content);
131
+ if (wordMatch) {
132
+ return {
133
+ start: wordMatch.index,
134
+ end: wordMatch.index + wordMatch[0].length - 1,
135
+ text: wordMatch[0],
136
+ };
137
+ }
138
+
139
+ // Try simple substring match
140
+ const index = contentLower.indexOf(queryLower);
141
+ if (index !== -1) {
142
+ return {
143
+ start: index,
144
+ end: index + queryLower.length - 1,
145
+ text: content.substring(index, index + queryLower.length),
146
+ };
147
+ }
148
+
149
+ return null;
150
+ };
151
+
152
+ // Create search variations
153
+ const searchVariations = [searchQuery.toLowerCase()];
154
+
155
+ // Add stemmed variations
156
+ try {
157
+ const stemmed = PorterStemmer.stem(searchQuery.toLowerCase());
158
+ if (stemmed !== searchQuery.toLowerCase()) {
159
+ searchVariations.push(stemmed);
160
+ }
161
+
162
+ // Find actual words in conversations that stem to the same root
163
+ for (const message of conv.messages) {
164
+ if (message.content) {
165
+ const words = message.content.toLowerCase().match(/\b\w+\b/g) || [];
166
+ words.forEach((word: string) => {
167
+ if (
168
+ PorterStemmer.stem(word) === stemmed &&
169
+ !searchVariations.includes(word)
170
+ ) {
171
+ searchVariations.push(word);
172
+ }
173
+ });
174
+ }
175
+ }
176
+ } catch (e) {
177
+ console.warn("Stemming failed for:", searchQuery, e);
178
+ }
179
+
180
+ // Add simple variations
181
+ const query = searchQuery.toLowerCase();
182
+ if (query.endsWith("s") && query.length > 3) {
183
+ searchVariations.push(query.slice(0, -1));
184
+ } else if (!query.endsWith("s")) {
185
+ searchVariations.push(query + "s");
186
+ }
187
+
188
+ // Search through all messages for the best match
189
+ for (const message of conv.messages) {
190
+ if (!message.content) continue;
191
+
192
+ // Try each variation in order of preference
193
+ for (const variation of searchVariations) {
194
+ const match = findBestMatch(message.content, variation);
195
+ if (match) {
196
+ const isExactQuery = variation === searchQuery.toLowerCase();
197
+ const priority = isExactQuery ? 1000 : match.text.length;
198
+
199
+ if (priority > bestMatchLength) {
200
+ bestMatch = {
201
+ content: message.content,
202
+ matchStart: match.start,
203
+ matchEnd: match.end,
204
+ matchedText: match.text,
205
+ };
206
+ bestMatchLength = priority;
207
+
208
+ // If we found exact query match, we're done
209
+ if (isExactQuery) break;
210
+ }
211
+ }
212
+ }
213
+
214
+ // Stop if we found an exact match
215
+ if (bestMatchLength >= 1000) break;
216
+ }
217
+
218
+ if (bestMatch) {
219
+ const { content, matchStart, matchEnd } = bestMatch;
220
+ matchedText = bestMatch.matchedText;
221
+
222
+ // Create centered context around the match
223
+ const maxContextLength = 160; // Maximum length of actual content (no padding)
224
+ const matchLength = matchEnd - matchStart + 1;
225
+
226
+ // Calculate context window - don't exceed maxContextLength even if content is longer
227
+ const availableForContext =
228
+ Math.min(maxContextLength, content.length) - matchLength;
229
+ const contextPerSide = Math.floor(availableForContext / 2);
230
+
231
+ // Calculate snippet boundaries to center the match within maxContextLength
232
+ let snippetStart = Math.max(0, matchStart - contextPerSide);
233
+ let snippetEnd = Math.min(
234
+ content.length,
235
+ matchStart + matchLength + contextPerSide
236
+ );
237
+
238
+ // Ensure we don't exceed maxContextLength
239
+ if (snippetEnd - snippetStart > maxContextLength) {
240
+ if (matchStart - contextPerSide < 0) {
241
+ // Match is near start, extend end but limit to maxContextLength
242
+ snippetEnd = Math.min(content.length, snippetStart + maxContextLength);
243
+ } else {
244
+ // Match is not near start, limit to maxContextLength from match start
245
+ snippetEnd = Math.min(content.length, snippetStart + maxContextLength);
246
+ }
247
+ }
248
+
249
+ // Adjust to word boundaries if possible (but don't move more than 15 chars)
250
+ const originalStart = snippetStart;
251
+ const originalEnd = snippetEnd;
252
+
253
+ while (
254
+ snippetStart > 0 &&
255
+ content[snippetStart] !== " " &&
256
+ content[snippetStart] !== "\n" &&
257
+ originalStart - snippetStart < 15
258
+ ) {
259
+ snippetStart--;
260
+ }
261
+ while (
262
+ snippetEnd < content.length &&
263
+ content[snippetEnd] !== " " &&
264
+ content[snippetEnd] !== "\n" &&
265
+ snippetEnd - originalEnd < 15
266
+ ) {
267
+ snippetEnd++;
268
+ }
269
+
270
+ // Extract the content
271
+ let extractedContent = content.substring(snippetStart, snippetEnd).trim();
272
+ // Add ellipsis indicators only
273
+ if (snippetStart > 0) {
274
+ extractedContent = "..." + extractedContent;
275
+ }
276
+ if (snippetEnd < content.length) {
277
+ extractedContent = extractedContent + "...";
278
+ }
279
+
280
+ matchedContent = extractedContent;
281
+ } else {
282
+ // Fallback: use beginning of the first message if no match found
283
+ const firstMessage = conv.messages[0];
284
+ if (firstMessage?.content) {
285
+ const content = firstMessage.content;
286
+ matchedContent =
287
+ content.length > 200 ? content.substring(0, 200) + "..." : content;
288
+ matchedText = searchQuery; // Fallback to search query
289
+ }
290
+ }
291
+
292
+ return {
293
+ _id: conv._id,
294
+ id: conv._id,
295
+ title: conv.title,
296
+ content: matchedContent,
297
+ matchedText,
298
+ updatedAt: conv.updatedAt,
299
+ model: conv.model,
300
+ assistantId: conv.assistantId,
301
+ modelTools: models.find((m) => m.id == conv.model)?.tools ?? false,
302
+ };
303
+ })
304
+ );
305
+
306
+ return convs;
307
+ },
308
+ {
309
+ query: t.Object({
310
+ q: t.String(),
311
+ p: t.Optional(t.Number()),
312
+ }),
313
+ }
314
+ )
315
  .group(
316
  "/:id",
317
  {
 
374
  return { conversation: convertedConv };
375
  })
376
  .get("", async ({ conversation }) => {
377
+ return {
378
  messages: conversation.messages,
379
  title: conversation.title,
380
  model: conversation.model,
381
  preprompt: conversation.preprompt,
382
  rootMessageId: conversation.rootMessageId,
383
  assistant: conversation.assistantId
384
+ ? ((await collections.assistants.findOne({
385
+ _id: new ObjectId(conversation.assistantId),
386
+ })) ?? undefined)
 
 
387
  : undefined,
388
  id: conversation._id.toString(),
389
  updatedAt: conversation.updatedAt,
 
391
  assistantId: conversation.assistantId,
392
  modelTools: models.find((m) => m.id == conversation.model)?.tools ?? false,
393
  shared: conversation.shared,
394
+ };
395
  })
396
  .post("", () => {
397
  // todo: post new message
src/lib/server/api/routes/groups/tools.ts CHANGED
@@ -4,27 +4,16 @@ import { ReviewStatus } from "$lib/types/Review";
4
  import { toolFromConfigs } from "$lib/server/tools";
5
  import { collections } from "$lib/server/database";
6
  import { ObjectId, type Filter } from "mongodb";
7
- import type { CommunityToolDB, ConfigTool, ToolFront, ToolInputFile } from "$lib/types/Tool";
8
  import { MetricsServer } from "$lib/server/metrics";
9
  import { authCondition } from "$lib/server/auth";
10
  import { SortKey } from "$lib/types/Assistant";
11
  import type { User } from "$lib/types/User";
12
  import { generateQueryTokens, generateSearchTokens } from "$lib/utils/searchTokens";
13
- import { jsonSerialize, type Serialize } from "$lib/utils/serialize";
14
  import { config } from "$lib/server/config";
15
 
16
  const NUM_PER_PAGE = 16;
17
 
18
- export type GETToolsResponse = Array<ToolFront>;
19
- export type GETToolsSearchResponse = {
20
- tools: Array<Serialize<ConfigTool | CommunityToolDB>>;
21
- numTotalItems: number;
22
- numItemsPerPage: number;
23
- query: string | null;
24
- sort: SortKey;
25
- showUnfeatured: boolean;
26
- };
27
-
28
  export const toolGroup = new Elysia().use(authPlugin).group("/tools", (app) => {
29
  return app
30
  .get("/active", async ({ locals }) => {
@@ -154,13 +143,13 @@ export const toolGroup = new Elysia().use(authPlugin).group("/tools", (app) => {
154
  (await collections.tools.countDocuments(filter)) + toolFromConfigs.length;
155
 
156
  return {
157
- tools: jsonSerialize(tools),
158
  numTotalItems,
159
  numItemsPerPage: NUM_PER_PAGE,
160
  query: search,
161
  sort,
162
  showUnfeatured,
163
- } satisfies GETToolsSearchResponse;
164
  },
165
  {
166
  query: t.Object({
 
4
  import { toolFromConfigs } from "$lib/server/tools";
5
  import { collections } from "$lib/server/database";
6
  import { ObjectId, type Filter } from "mongodb";
7
+ import type { CommunityToolDB, ToolFront, ToolInputFile } from "$lib/types/Tool";
8
  import { MetricsServer } from "$lib/server/metrics";
9
  import { authCondition } from "$lib/server/auth";
10
  import { SortKey } from "$lib/types/Assistant";
11
  import type { User } from "$lib/types/User";
12
  import { generateQueryTokens, generateSearchTokens } from "$lib/utils/searchTokens";
 
13
  import { config } from "$lib/server/config";
14
 
15
  const NUM_PER_PAGE = 16;
16
 
 
 
 
 
 
 
 
 
 
 
17
  export const toolGroup = new Elysia().use(authPlugin).group("/tools", (app) => {
18
  return app
19
  .get("/active", async ({ locals }) => {
 
143
  (await collections.tools.countDocuments(filter)) + toolFromConfigs.length;
144
 
145
  return {
146
+ tools,
147
  numTotalItems,
148
  numItemsPerPage: NUM_PER_PAGE,
149
  query: search,
150
  sort,
151
  showUnfeatured,
152
+ };
153
  },
154
  {
155
  query: t.Object({
src/lib/server/api/routes/groups/user.ts CHANGED
@@ -8,7 +8,6 @@ import { DEFAULT_SETTINGS, type SettingsEditable } from "$lib/types/Settings";
8
  import { toolFromConfigs } from "$lib/server/tools";
9
  import { ObjectId } from "mongodb";
10
  import { z } from "zod";
11
- import { jsonSerialize } from "$lib/utils/serialize";
12
 
13
  export const userGroup = new Elysia()
14
  .use(authPlugin)
@@ -149,8 +148,7 @@ export const userGroup = new Elysia()
149
  .find({
150
  createdBy: locals.user?._id ?? locals.sessionId,
151
  })
152
- .toArray()
153
- .then((el) => el.map((el) => jsonSerialize(el)));
154
  return reports;
155
  })
156
  .get("/assistant/active", async ({ locals }) => {
 
8
  import { toolFromConfigs } from "$lib/server/tools";
9
  import { ObjectId } from "mongodb";
10
  import { z } from "zod";
 
11
 
12
  export const userGroup = new Elysia()
13
  .use(authPlugin)
 
148
  .find({
149
  createdBy: locals.user?._id ?? locals.sessionId,
150
  })
151
+ .toArray();
 
152
  return reports;
153
  })
154
  .get("/assistant/active", async ({ locals }) => {
src/lib/types/ConvSidebar.ts CHANGED
@@ -1,8 +1,10 @@
 
 
1
  export interface ConvSidebar {
2
- id: string;
3
  title: string;
4
  updatedAt: Date;
5
  model?: string;
6
- assistantId?: string;
7
  avatarUrl?: string | Promise<string | undefined>;
8
  }
 
1
+ import type { ObjectId } from "bson";
2
+
3
  export interface ConvSidebar {
4
+ id: ObjectId | string;
5
  title: string;
6
  updatedAt: Date;
7
  model?: string;
8
+ assistantId?: ObjectId | string;
9
  avatarUrl?: string | Promise<string | undefined>;
10
  }
src/lib/utils/fetchJSON.ts CHANGED
@@ -1,12 +1,10 @@
1
- import type { Serialize } from "./serialize";
2
-
3
  export async function fetchJSON<T>(
4
  url: string,
5
  options?: {
6
  fetch?: typeof window.fetch;
7
  allowNull?: boolean;
8
  }
9
- ): Promise<Serialize<T>> {
10
  const response = await (options?.fetch ?? fetch)(url);
11
  if (!response.ok) {
12
  throw new Error(`Failed to fetch ${url}: ${response.status} ${response.statusText}`);
@@ -16,7 +14,7 @@ export async function fetchJSON<T>(
16
  const text = await response.text();
17
  if (!text || text.trim() === "") {
18
  if (options?.allowNull) {
19
- return null as Serialize<T>;
20
  }
21
  throw new Error(`Received empty response from ${url} but allowNull is not set to true`);
22
  }
 
 
 
1
  export async function fetchJSON<T>(
2
  url: string,
3
  options?: {
4
  fetch?: typeof window.fetch;
5
  allowNull?: boolean;
6
  }
7
+ ): Promise<T> {
8
  const response = await (options?.fetch ?? fetch)(url);
9
  if (!response.ok) {
10
  throw new Error(`Failed to fetch ${url}: ${response.status} ${response.statusText}`);
 
14
  const text = await response.text();
15
  if (!text || text.trim() === "") {
16
  if (options?.allowNull) {
17
+ return null as T;
18
  }
19
  throw new Error(`Received empty response from ${url} but allowNull is not set to true`);
20
  }
src/lib/utils/serialize.ts DELETED
@@ -1,13 +0,0 @@
1
- import type { ObjectId } from "mongodb";
2
-
3
- export type Serialize<T> = T extends ObjectId | Date
4
- ? string
5
- : T extends Array<infer U>
6
- ? Array<Serialize<U>>
7
- : T extends object
8
- ? { [K in keyof T]: Serialize<T[K]> }
9
- : T;
10
-
11
- export function jsonSerialize<T>(data: T): Serialize<T> {
12
- return JSON.parse(JSON.stringify(data)) as Serialize<T>;
13
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
src/routes/+layout.ts CHANGED
@@ -1,7 +1,6 @@
1
  import { UrlDependency } from "$lib/types/UrlDependency";
2
  import type { ConvSidebar } from "$lib/types/ConvSidebar";
3
- import { jsonSerialize } from "../lib/utils/serialize";
4
- import { useAPIClient, throwOnError, throwOnErrorNullable } from "$lib/APIClient";
5
  import { getConfigManager } from "$lib/utils/PublicConfig.svelte";
6
 
7
  export const load = async ({ depends, fetch }) => {
@@ -21,16 +20,16 @@ export const load = async ({ depends, fetch }) => {
21
  featureFlags,
22
  conversationsData,
23
  ] = await Promise.all([
24
- client.user.settings.get().then(throwOnError),
25
- client.models.get().then(throwOnError),
26
- client.user.assistants.get().then(throwOnError),
27
- client.models.old.get().then(throwOnError),
28
- client.tools.active.get().then(throwOnError),
29
- client.tools.count.get().then(throwOnError),
30
- client.user.get().then(throwOnErrorNullable),
31
- client["public-config"].get().then(throwOnError),
32
- client["feature-flags"].get().then(throwOnError),
33
- client.conversations.get({ query: { p: 0 } }).then(throwOnError),
34
  ]);
35
 
36
  const defaultModel = models[0];
@@ -57,9 +56,9 @@ export const load = async ({ depends, fetch }) => {
57
  avatarUrl: client
58
  .assistants({ id: conv.assistantId.toString() })
59
  .get()
60
- .then(throwOnErrorNullable)
61
  .then((assistant) => {
62
- if (!assistant.avatar) {
63
  return undefined;
64
  }
65
  return `/settings/assistants/${conv.assistantId}/avatar.jpg?hash=${assistant.avatar}`;
@@ -76,8 +75,7 @@ export const load = async ({ depends, fetch }) => {
76
  ? await client
77
  .assistants({ id: settings?.activeModel })
78
  .get()
79
- .then(throwOnErrorNullable)
80
- .then(jsonSerialize)
81
  .catch(() => undefined)
82
  : undefined,
83
  assistants,
 
1
  import { UrlDependency } from "$lib/types/UrlDependency";
2
  import type { ConvSidebar } from "$lib/types/ConvSidebar";
3
+ import { useAPIClient, handleResponse } from "$lib/APIClient";
 
4
  import { getConfigManager } from "$lib/utils/PublicConfig.svelte";
5
 
6
  export const load = async ({ depends, fetch }) => {
 
20
  featureFlags,
21
  conversationsData,
22
  ] = await Promise.all([
23
+ client.user.settings.get().then(handleResponse),
24
+ client.models.get().then(handleResponse),
25
+ client.user.assistants.get().then(handleResponse),
26
+ client.models.old.get().then(handleResponse),
27
+ client.tools.active.get().then(handleResponse),
28
+ client.tools.count.get().then(handleResponse),
29
+ client.user.get().then(handleResponse),
30
+ client["public-config"].get().then(handleResponse),
31
+ client["feature-flags"].get().then(handleResponse),
32
+ client.conversations.get({ query: { p: 0 } }).then(handleResponse),
33
  ]);
34
 
35
  const defaultModel = models[0];
 
56
  avatarUrl: client
57
  .assistants({ id: conv.assistantId.toString() })
58
  .get()
59
+ .then(handleResponse)
60
  .then((assistant) => {
61
+ if (!assistant || !assistant.avatar) {
62
  return undefined;
63
  }
64
  return `/settings/assistants/${conv.assistantId}/avatar.jpg?hash=${assistant.avatar}`;
 
75
  ? await client
76
  .assistants({ id: settings?.activeModel })
77
  .get()
78
+ .then(handleResponse)
 
79
  .catch(() => undefined)
80
  : undefined,
81
  assistants,
src/routes/api/conversations/search/+server.ts DELETED
@@ -1,240 +0,0 @@
1
- import { authCondition } from "$lib/server/auth";
2
- import { collections } from "$lib/server/database";
3
- import { models } from "$lib/server/models";
4
- import type { RequestHandler } from "@sveltejs/kit";
5
- import pkg from "natural";
6
- const { PorterStemmer } = pkg;
7
-
8
- export type GETSearchEndpointReturn = Array<{
9
- id: string;
10
- title: string;
11
- content: string;
12
- matchedText: string;
13
- updatedAt: Date;
14
- model: string;
15
- assistantId?: string;
16
- mdoelTools?: boolean;
17
- }>;
18
-
19
- export const GET: RequestHandler = async ({ locals, url }) => {
20
- const searchQuery = url.searchParams.get("q");
21
- const p = parseInt(url.searchParams.get("p") ?? "0");
22
-
23
- if (!searchQuery || searchQuery.length < 3) {
24
- return Response.json([]);
25
- }
26
-
27
- if (locals.user?._id || locals.sessionId) {
28
- const convs = await collections.conversations
29
- .find({
30
- sessionId: undefined,
31
- ...authCondition(locals),
32
- $text: { $search: searchQuery },
33
- })
34
- .sort({
35
- updatedAt: -1, // Sort by date updated in descending order
36
- })
37
- .project({
38
- title: 1,
39
- updatedAt: 1,
40
- model: 1,
41
- assistantId: 1,
42
- messages: 1,
43
- userId: 1,
44
- })
45
- .skip(p * 5)
46
- .limit(5)
47
- .toArray()
48
- .then((convs) =>
49
- convs.map((conv) => {
50
- let matchedContent = "";
51
- let matchedText = "";
52
-
53
- // Find the best match using stemming to handle MongoDB's text search behavior
54
- let bestMatch = null;
55
- let bestMatchLength = 0;
56
-
57
- // Simple function to find the best match in content
58
- const findBestMatch = (
59
- content: string,
60
- query: string
61
- ): { start: number; end: number; text: string } | null => {
62
- const contentLower = content.toLowerCase();
63
- const queryLower = query.toLowerCase();
64
-
65
- // Try exact word boundary match first
66
- const wordRegex = new RegExp(
67
- `\\b${queryLower.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\b`,
68
- "gi"
69
- );
70
- const wordMatch = wordRegex.exec(content);
71
- if (wordMatch) {
72
- return {
73
- start: wordMatch.index,
74
- end: wordMatch.index + wordMatch[0].length - 1,
75
- text: wordMatch[0],
76
- };
77
- }
78
-
79
- // Try simple substring match
80
- const index = contentLower.indexOf(queryLower);
81
- if (index !== -1) {
82
- return {
83
- start: index,
84
- end: index + queryLower.length - 1,
85
- text: content.substring(index, index + queryLower.length),
86
- };
87
- }
88
-
89
- return null;
90
- };
91
-
92
- // Create search variations
93
- const searchVariations = [searchQuery.toLowerCase()];
94
-
95
- // Add stemmed variations
96
- try {
97
- const stemmed = PorterStemmer.stem(searchQuery.toLowerCase());
98
- if (stemmed !== searchQuery.toLowerCase()) {
99
- searchVariations.push(stemmed);
100
- }
101
-
102
- // Find actual words in conversations that stem to the same root
103
- for (const message of conv.messages) {
104
- if (message.content) {
105
- const words = message.content.toLowerCase().match(/\b\w+\b/g) || [];
106
- words.forEach((word: string) => {
107
- if (PorterStemmer.stem(word) === stemmed && !searchVariations.includes(word)) {
108
- searchVariations.push(word);
109
- }
110
- });
111
- }
112
- }
113
- } catch (e) {
114
- console.warn("Stemming failed for:", searchQuery, e);
115
- }
116
-
117
- // Add simple variations
118
- const query = searchQuery.toLowerCase();
119
- if (query.endsWith("s") && query.length > 3) {
120
- searchVariations.push(query.slice(0, -1));
121
- } else if (!query.endsWith("s")) {
122
- searchVariations.push(query + "s");
123
- }
124
-
125
- // Search through all messages for the best match
126
- for (const message of conv.messages) {
127
- if (!message.content) continue;
128
-
129
- // Try each variation in order of preference
130
- for (const variation of searchVariations) {
131
- const match = findBestMatch(message.content, variation);
132
- if (match) {
133
- const isExactQuery = variation === searchQuery.toLowerCase();
134
- const priority = isExactQuery ? 1000 : match.text.length;
135
-
136
- if (priority > bestMatchLength) {
137
- bestMatch = {
138
- content: message.content,
139
- matchStart: match.start,
140
- matchEnd: match.end,
141
- matchedText: match.text,
142
- };
143
- bestMatchLength = priority;
144
-
145
- // If we found exact query match, we're done
146
- if (isExactQuery) break;
147
- }
148
- }
149
- }
150
-
151
- // Stop if we found an exact match
152
- if (bestMatchLength >= 1000) break;
153
- }
154
-
155
- if (bestMatch) {
156
- const { content, matchStart, matchEnd } = bestMatch;
157
- matchedText = bestMatch.matchedText;
158
-
159
- // Create centered context around the match
160
- const maxContextLength = 160; // Maximum length of actual content (no padding)
161
- const matchLength = matchEnd - matchStart + 1;
162
-
163
- // Calculate context window - don't exceed maxContextLength even if content is longer
164
- const availableForContext = Math.min(maxContextLength, content.length) - matchLength;
165
- const contextPerSide = Math.floor(availableForContext / 2);
166
-
167
- // Calculate snippet boundaries to center the match within maxContextLength
168
- let snippetStart = Math.max(0, matchStart - contextPerSide);
169
- let snippetEnd = Math.min(content.length, matchStart + matchLength + contextPerSide);
170
-
171
- // Ensure we don't exceed maxContextLength
172
- if (snippetEnd - snippetStart > maxContextLength) {
173
- if (matchStart - contextPerSide < 0) {
174
- // Match is near start, extend end but limit to maxContextLength
175
- snippetEnd = Math.min(content.length, snippetStart + maxContextLength);
176
- } else {
177
- // Match is not near start, limit to maxContextLength from match start
178
- snippetEnd = Math.min(content.length, snippetStart + maxContextLength);
179
- }
180
- }
181
-
182
- // Adjust to word boundaries if possible (but don't move more than 15 chars)
183
- const originalStart = snippetStart;
184
- const originalEnd = snippetEnd;
185
-
186
- while (
187
- snippetStart > 0 &&
188
- content[snippetStart] !== " " &&
189
- content[snippetStart] !== "\n" &&
190
- originalStart - snippetStart < 15
191
- ) {
192
- snippetStart--;
193
- }
194
- while (
195
- snippetEnd < content.length &&
196
- content[snippetEnd] !== " " &&
197
- content[snippetEnd] !== "\n" &&
198
- snippetEnd - originalEnd < 15
199
- ) {
200
- snippetEnd++;
201
- }
202
-
203
- // Extract the content
204
- let extractedContent = content.substring(snippetStart, snippetEnd).trim();
205
- // Add ellipsis indicators only
206
- if (snippetStart > 0) {
207
- extractedContent = "..." + extractedContent;
208
- }
209
- if (snippetEnd < content.length) {
210
- extractedContent = extractedContent + "...";
211
- }
212
-
213
- matchedContent = extractedContent;
214
- } else {
215
- // Fallback: use beginning of the first message if no match found
216
- const firstMessage = conv.messages[0];
217
- if (firstMessage?.content) {
218
- const content = firstMessage.content;
219
- matchedContent = content.length > 200 ? content.substring(0, 200) + "..." : content;
220
- matchedText = searchQuery; // Fallback to search query
221
- }
222
- }
223
-
224
- return {
225
- _id: conv._id,
226
- id: conv._id,
227
- title: conv.title,
228
- content: matchedContent,
229
- matchedText,
230
- updatedAt: conv.updatedAt,
231
- model: conv.model,
232
- assistantId: conv.assistantId,
233
- modelTools: models.find((m) => m.id == conv.model)?.tools ?? false,
234
- };
235
- })
236
- );
237
- return Response.json(convs as GETSearchEndpointReturn);
238
- }
239
- return Response.json([]);
240
- };
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
src/routes/assistant/[assistantId]/+page.ts CHANGED
@@ -1,14 +1,9 @@
1
- import { useAPIClient, throwOnError } from "$lib/APIClient";
2
- import { jsonSerialize } from "$lib/utils/serialize";
3
 
4
  export async function load({ fetch, params }) {
5
  const client = useAPIClient({ fetch });
6
 
7
- const data = client
8
- .assistants({ id: params.assistantId })
9
- .get()
10
- .then(throwOnError)
11
- .then(jsonSerialize);
12
 
13
  await client.assistants({ id: params.assistantId }).follow.post();
14
 
 
1
+ import { useAPIClient, handleResponse } from "$lib/APIClient";
 
2
 
3
  export async function load({ fetch, params }) {
4
  const client = useAPIClient({ fetch });
5
 
6
+ const data = client.assistants({ id: params.assistantId }).get().then(handleResponse);
 
 
 
 
7
 
8
  await client.assistants({ id: params.assistantId }).follow.post();
9
 
src/routes/assistants/+page.ts CHANGED
@@ -1,11 +1,11 @@
1
- import { useAPIClient, throwOnError } from "$lib/APIClient";
2
 
3
  export const load = async ({ url, fetch }) => {
4
  const client = useAPIClient({ fetch });
5
 
6
  const data = client.assistants.search
7
  .get({ query: Object.fromEntries(url.searchParams.entries()) })
8
- .then(throwOnError);
9
 
10
  return data;
11
  };
 
1
+ import { useAPIClient, handleResponse } from "$lib/APIClient";
2
 
3
  export const load = async ({ url, fetch }) => {
4
  const client = useAPIClient({ fetch });
5
 
6
  const data = client.assistants.search
7
  .get({ query: Object.fromEntries(url.searchParams.entries()) })
8
+ .then(handleResponse);
9
 
10
  return data;
11
  };
src/routes/conversation/[id]/+page.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { useAPIClient, throwOnError } from "$lib/APIClient";
2
  import { UrlDependency } from "$lib/types/UrlDependency";
3
  import { redirect } from "@sveltejs/kit";
4
 
@@ -8,7 +8,7 @@ export const load = async ({ params, depends, fetch }) => {
8
  const client = useAPIClient({ fetch });
9
 
10
  try {
11
- return await client.conversations({ id: params.id }).get().then(throwOnError);
12
  } catch {
13
  redirect(302, "/");
14
  }
 
1
+ import { useAPIClient, handleResponse } from "$lib/APIClient";
2
  import { UrlDependency } from "$lib/types/UrlDependency";
3
  import { redirect } from "@sveltejs/kit";
4
 
 
8
  const client = useAPIClient({ fetch });
9
 
10
  try {
11
+ return await client.conversations({ id: params.id }).get().then(handleResponse);
12
  } catch {
13
  redirect(302, "/");
14
  }
src/routes/settings/(nav)/+layout.svelte CHANGED
@@ -18,7 +18,7 @@
18
  import { debounce } from "$lib/utils/debounce";
19
 
20
  import { fly } from "svelte/transition";
21
- import { throwOnError, useAPIClient } from "$lib/APIClient";
22
 
23
  interface Props {
24
  data: LayoutData;
@@ -255,7 +255,7 @@
255
  id: assistant._id,
256
  })
257
  .follow.delete()
258
- .then(throwOnError)
259
  .then(() => {
260
  if (assistant._id.toString() === page.params.assistantId) {
261
  goto(`${base}/settings`, { invalidateAll: true });
 
18
  import { debounce } from "$lib/utils/debounce";
19
 
20
  import { fly } from "svelte/transition";
21
+ import { handleResponse, useAPIClient } from "$lib/APIClient";
22
 
23
  interface Props {
24
  data: LayoutData;
 
255
  id: assistant._id,
256
  })
257
  .follow.delete()
258
+ .then(handleResponse)
259
  .then(() => {
260
  if (assistant._id.toString() === page.params.assistantId) {
261
  goto(`${base}/settings`, { invalidateAll: true });
src/routes/settings/(nav)/assistants/[assistantId]/edit/+page.svelte CHANGED
@@ -12,13 +12,4 @@
12
  let assistant = data.assistants.find((el) => el._id.toString() === page.params.assistantId);
13
  </script>
14
 
15
- <AssistantSettings
16
- assistant={assistant
17
- ? {
18
- ...assistant,
19
- updatedAt: new Date(assistant.updatedAt),
20
- createdAt: new Date(assistant.createdAt),
21
- }
22
- : undefined}
23
- models={data.models}
24
- />
 
12
  let assistant = data.assistants.find((el) => el._id.toString() === page.params.assistantId);
13
  </script>
14
 
15
+ <AssistantSettings {assistant} models={data.models} />
 
 
 
 
 
 
 
 
 
src/routes/settings/+layout.ts CHANGED
@@ -1,14 +1,16 @@
1
- import { useAPIClient, throwOnError } from "$lib/APIClient";
2
 
3
  export const load = async ({ parent, fetch }) => {
4
  const client = useAPIClient({ fetch });
5
 
6
- const reports = await client.user.reports.get().then(throwOnError);
7
 
8
  return {
9
  assistants: (await parent().then((data) => data.assistants)).map((el) => ({
10
  ...el,
11
- reported: reports.some((r) => r.contentId === el._id && r.object === "assistant"),
 
 
12
  })),
13
  };
14
  };
 
1
+ import { useAPIClient, handleResponse } from "$lib/APIClient";
2
 
3
  export const load = async ({ parent, fetch }) => {
4
  const client = useAPIClient({ fetch });
5
 
6
+ const reports = await client.user.reports.get().then(handleResponse);
7
 
8
  return {
9
  assistants: (await parent().then((data) => data.assistants)).map((el) => ({
10
  ...el,
11
+ reported: reports.some(
12
+ (r) => r.contentId.toString() === el._id.toString() && r.object === "assistant"
13
+ ),
14
  })),
15
  };
16
  };
src/routes/tools/+page.ts CHANGED
@@ -1,9 +1,9 @@
1
- import { throwOnError, useAPIClient } from "$lib/APIClient";
2
 
3
  export const load = async ({ url, fetch }) => {
4
  const client = useAPIClient({ fetch });
5
 
6
  return client.tools.search
7
  .get({ query: Object.fromEntries(url.searchParams.entries()) })
8
- .then(throwOnError);
9
  };
 
1
+ import { handleResponse, useAPIClient } from "$lib/APIClient";
2
 
3
  export const load = async ({ url, fetch }) => {
4
  const client = useAPIClient({ fetch });
5
 
6
  return client.tools.search
7
  .get({ query: Object.fromEntries(url.searchParams.entries()) })
8
+ .then(handleResponse);
9
  };
src/routes/tools/ToolEdit.svelte CHANGED
@@ -15,7 +15,7 @@
15
 
16
  import CarbonInformation from "~icons/carbon/information";
17
  import { page } from "$app/state";
18
- import { throwOnError, useAPIClient } from "$lib/APIClient";
19
 
20
  interface Props {
21
  tool?: CommunityToolEditable | undefined;
@@ -76,7 +76,7 @@
76
  space: editableTool.baseUrl,
77
  },
78
  })
79
- .then(throwOnError);
80
 
81
  const newInputs = api.named_endpoints[editableTool.endpoint].parameters.map((param, idx) => {
82
  if (tool?.inputs[idx]?.name === param.parameter_name) {
@@ -328,7 +328,7 @@
328
  {/if}
329
 
330
  {#if editableTool.baseUrl}
331
- {#await client["spaces-config"].get({ query: { space: spaceUrl } }).then(throwOnError)}
332
  <p class="text-sm text-gray-500">Loading...</p>
333
  {:then api}
334
  <div class="flex flex-row flex-wrap gap-4">
 
15
 
16
  import CarbonInformation from "~icons/carbon/information";
17
  import { page } from "$app/state";
18
+ import { handleResponse, useAPIClient } from "$lib/APIClient";
19
 
20
  interface Props {
21
  tool?: CommunityToolEditable | undefined;
 
76
  space: editableTool.baseUrl,
77
  },
78
  })
79
+ .then(handleResponse);
80
 
81
  const newInputs = api.named_endpoints[editableTool.endpoint].parameters.map((param, idx) => {
82
  if (tool?.inputs[idx]?.name === param.parameter_name) {
 
328
  {/if}
329
 
330
  {#if editableTool.baseUrl}
331
+ {#await client["spaces-config"].get({ query: { space: spaceUrl } }).then(handleResponse)}
332
  <p class="text-sm text-gray-500">Loading...</p>
333
  {:then api}
334
  <div class="flex flex-row flex-wrap gap-4">
src/routes/tools/[toolId]/+layout.ts CHANGED
@@ -1,5 +1,4 @@
1
- import { useAPIClient, throwOnError } from "$lib/APIClient";
2
- import { jsonSerialize } from "$lib/utils/serialize";
3
 
4
  export const load = async ({ params, fetch }) => {
5
  const client = useAPIClient({ fetch });
@@ -9,8 +8,7 @@ export const load = async ({ params, fetch }) => {
9
  id: params.toolId,
10
  })
11
  .get()
12
- .then(throwOnError)
13
- .then(jsonSerialize);
14
 
15
  return { tool: await data };
16
  };
 
1
+ import { useAPIClient, handleResponse } from "$lib/APIClient";
 
2
 
3
  export const load = async ({ params, fetch }) => {
4
  const client = useAPIClient({ fetch });
 
8
  id: params.toolId,
9
  })
10
  .get()
11
+ .then(handleResponse);
 
12
 
13
  return { tool: await data };
14
  };