nsarrazin commited on
Commit
91e839b
·
1 Parent(s): 35a395c

Revert "refactor: new API & universal load functions (#1743)"

Browse files
This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. package-lock.json +0 -0
  2. package.json +2 -7
  3. src/hooks.server.ts +109 -53
  4. src/hooks.ts +0 -6
  5. src/lib/APIClient.ts +0 -55
  6. src/lib/components/AssistantSettings.svelte +1 -3
  7. src/lib/components/DisclaimerModal.svelte +15 -15
  8. src/lib/components/LoginModal.svelte +11 -7
  9. src/lib/components/NavConversationItem.svelte +5 -9
  10. src/lib/components/NavMenu.svelte +26 -36
  11. src/lib/components/ToolsMenu.svelte +1 -3
  12. src/lib/components/chat/AssistantIntroduction.svelte +2 -5
  13. src/lib/components/chat/ChatInput.svelte +1 -3
  14. src/lib/components/chat/ChatIntroduction.svelte +2 -3
  15. src/lib/components/chat/ChatWindow.svelte +2 -3
  16. src/lib/components/icons/Logo.svelte +15 -3
  17. src/lib/server/api/authPlugin.ts +0 -25
  18. src/lib/server/api/index.ts +0 -35
  19. src/lib/server/api/routes/groups/assistants.ts +0 -180
  20. src/lib/server/api/routes/groups/conversations.ts +0 -171
  21. src/lib/server/api/routes/groups/misc.ts +0 -77
  22. src/lib/server/api/routes/groups/models.ts +0 -106
  23. src/lib/server/api/routes/groups/tools.ts +0 -253
  24. src/lib/server/api/routes/groups/user.ts +0 -197
  25. src/lib/server/auth.ts +0 -136
  26. src/lib/server/config.ts +2 -2
  27. src/lib/server/isURLLocal.ts +0 -10
  28. src/lib/server/models.ts +5 -5
  29. src/lib/types/ConvSidebar.ts +1 -1
  30. src/lib/types/UrlDependency.ts +1 -1
  31. src/lib/utils/PublicConfig.svelte.ts +20 -53
  32. src/lib/utils/fetchJSON.ts +0 -25
  33. src/lib/utils/getShareUrl.ts +2 -3
  34. src/lib/utils/messageUpdates.ts +2 -3
  35. src/lib/utils/serialize.ts +0 -13
  36. src/lib/utils/tree/addChildren.ts +10 -5
  37. src/lib/utils/tree/addSibling.spec.ts +4 -5
  38. src/lib/utils/tree/addSibling.ts +8 -3
  39. src/lib/utils/tree/buildSubtree.ts +6 -2
  40. src/lib/utils/tree/tree.d.ts +0 -14
  41. src/routes/+layout.server.ts +286 -0
  42. src/routes/+layout.svelte +39 -12
  43. src/routes/+layout.ts +0 -81
  44. src/routes/+page.svelte +3 -5
  45. src/routes/api/assistant/[id]/subscribe/+server.ts +0 -1
  46. src/routes/api/v2/[...slugs]/+server.ts +0 -9
  47. src/routes/assistant/[assistantId]/+page.server.ts +42 -0
  48. src/routes/assistant/[assistantId]/+page.svelte +1 -3
  49. src/routes/assistant/[assistantId]/+page.ts +0 -16
  50. src/routes/assistants/+page.server.ts +83 -0
package-lock.json CHANGED
The diff for this file is too large to render. See raw diff
 
package.json CHANGED
@@ -1,6 +1,6 @@
1
  {
2
  "name": "chat-ui",
3
- "version": "0.10.0",
4
  "private": true,
5
  "packageManager": "npm@9.5.0",
6
  "scripts": {
@@ -18,9 +18,6 @@
18
  "prepare": "husky"
19
  },
20
  "devDependencies": {
21
- "@elysiajs/cors": "^1.3.3",
22
- "@elysiajs/eden": "^1.3.2",
23
- "@elysiajs/node": "^1.2.6",
24
  "@faker-js/faker": "^8.4.1",
25
  "@iconify-json/carbon": "^1.1.16",
26
  "@iconify-json/eos-icons": "^1.1.6",
@@ -44,7 +41,6 @@
44
  "@typescript-eslint/eslint-plugin": "^6.x",
45
  "@typescript-eslint/parser": "^6.x",
46
  "dompurify": "^3.2.4",
47
- "elysia": "^1.3.2",
48
  "eslint": "^8.28.0",
49
  "eslint-config-prettier": "^8.5.0",
50
  "eslint-plugin-svelte": "^2.45.1",
@@ -75,7 +71,6 @@
75
  "dependencies": {
76
  "@aws-sdk/credential-providers": "^3.592.0",
77
  "@cliqz/adblocker-playwright": "^1.34.0",
78
- "@elysiajs/swagger": "^1.3.0",
79
  "@gradio/client": "^1.8.0",
80
  "@huggingface/hub": "^0.5.1",
81
  "@huggingface/inference": "^3.12.1",
@@ -91,7 +86,7 @@
91
  "date-fns": "^2.29.3",
92
  "dotenv": "^16.5.0",
93
  "express": "^4.21.2",
94
- "file-type": "^21.0.0",
95
  "google-auth-library": "^9.13.0",
96
  "handlebars": "^4.7.8",
97
  "highlight.js": "^11.7.0",
 
1
  {
2
  "name": "chat-ui",
3
+ "version": "0.9.5",
4
  "private": true,
5
  "packageManager": "npm@9.5.0",
6
  "scripts": {
 
18
  "prepare": "husky"
19
  },
20
  "devDependencies": {
 
 
 
21
  "@faker-js/faker": "^8.4.1",
22
  "@iconify-json/carbon": "^1.1.16",
23
  "@iconify-json/eos-icons": "^1.1.6",
 
41
  "@typescript-eslint/eslint-plugin": "^6.x",
42
  "@typescript-eslint/parser": "^6.x",
43
  "dompurify": "^3.2.4",
 
44
  "eslint": "^8.28.0",
45
  "eslint-config-prettier": "^8.5.0",
46
  "eslint-plugin-svelte": "^2.45.1",
 
71
  "dependencies": {
72
  "@aws-sdk/credential-providers": "^3.592.0",
73
  "@cliqz/adblocker-playwright": "^1.34.0",
 
74
  "@gradio/client": "^1.8.0",
75
  "@huggingface/hub": "^0.5.1",
76
  "@huggingface/inference": "^3.12.1",
 
86
  "date-fns": "^2.29.3",
87
  "dotenv": "^16.5.0",
88
  "express": "^4.21.2",
89
+ "file-type": "^19.4.1",
90
  "google-auth-library": "^9.13.0",
91
  "handlebars": "^4.7.8",
92
  "highlight.js": "^11.7.0",
src/hooks.server.ts CHANGED
@@ -2,19 +2,20 @@ import { config, ready } from "$lib/server/config";
2
  import type { Handle, HandleServerError, ServerInit } from "@sveltejs/kit";
3
  import { collections } from "$lib/server/database";
4
  import { base } from "$app/paths";
5
- import { authenticateRequest, refreshSessionCookie, requiresUser } from "$lib/server/auth";
6
  import { ERROR_MESSAGES } from "$lib/stores/errors";
 
7
  import { addWeeks } from "date-fns";
8
  import { checkAndRunMigrations } from "$lib/migrations/migrations";
9
- import { building, dev } from "$app/environment";
10
  import { logger } from "$lib/server/logger";
11
  import { AbortedGenerations } from "$lib/server/abortedGenerations";
12
  import { MetricsServer } from "$lib/server/metrics";
13
  import { initExitHandler } from "$lib/server/exitHandler";
 
14
  import { refreshAssistantsCounts } from "$lib/jobs/refresh-assistants-counts";
15
  import { refreshConversationStats } from "$lib/jobs/refresh-conversation-stats";
16
  import { adminTokenManager } from "$lib/server/adminToken";
17
- import { isHostLocalhost } from "$lib/server/isURLLocal";
18
 
19
  export const init: ServerInit = async () => {
20
  // Wait for config to be fully loaded
@@ -119,13 +120,108 @@ export const handle: Handle = async ({ event, resolve }) => {
119
  }
120
  }
121
 
122
- const auth = await authenticateRequest(
123
- { type: "svelte", value: event.request.headers },
124
- { type: "svelte", value: event.cookies }
125
- );
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
126
 
127
- event.locals.user = auth.user || undefined;
128
- event.locals.sessionId = auth.sessionId;
 
 
 
 
 
 
 
 
129
 
130
  event.locals.isAdmin =
131
  event.locals.user?.isAdmin || adminTokenManager.isAdmin(event.locals.sessionId);
@@ -158,16 +254,12 @@ export const handle: Handle = async ({ event, resolve }) => {
158
  }
159
  }
160
 
161
- if (
162
- event.request.method === "POST" ||
163
- event.url.pathname.startsWith(`${base}/login`) ||
164
- event.url.pathname.startsWith(`${base}/login/callback`)
165
- ) {
166
- // if the request is a POST request or login-related we refresh the cookie
167
- refreshSessionCookie(event.cookies, auth.secretSessionId);
168
 
169
  await collections.sessions.updateOne(
170
- { sessionId: auth.sessionId },
171
  { $set: { updatedAt: new Date(), expiresAt: addWeeks(new Date(), 2) } }
172
  );
173
  }
@@ -217,9 +309,6 @@ export const handle: Handle = async ({ event, resolve }) => {
217
 
218
  return chunk.html.replace("%gaId%", config.PUBLIC_GOOGLE_ANALYTICS_ID);
219
  },
220
- filterSerializedResponseHeaders: (header) => {
221
- return header.includes("content-type");
222
- },
223
  });
224
 
225
  // Add CSP header to disallow framing if ALLOW_IFRAME is not "true"
@@ -227,38 +316,5 @@ export const handle: Handle = async ({ event, resolve }) => {
227
  response.headers.append("Content-Security-Policy", "frame-ancestors 'none';");
228
  }
229
 
230
- if (
231
- event.url.pathname.startsWith(`${base}/login/callback`) ||
232
- event.url.pathname.startsWith(`${base}/login`)
233
- ) {
234
- response.headers.append("Cache-Control", "no-store");
235
- }
236
-
237
- if (event.url.pathname.startsWith(`${base}/api/`)) {
238
- // get origin from the request
239
- const requestOrigin = event.request.headers.get("origin");
240
-
241
- // get origin from the config if its defined
242
- let allowedOrigin = config.PUBLIC_ORIGIN ? new URL(config.PUBLIC_ORIGIN).origin : undefined;
243
-
244
- if (
245
- dev || // if we're in dev mode
246
- !requestOrigin || // or the origin is null (SSR)
247
- isHostLocalhost(new URL(requestOrigin).hostname) // or the origin is localhost
248
- ) {
249
- allowedOrigin = "*"; // allow all origins
250
- } else if (allowedOrigin === requestOrigin) {
251
- allowedOrigin = requestOrigin; // echo back the caller
252
- }
253
-
254
- if (allowedOrigin) {
255
- response.headers.set("Access-Control-Allow-Origin", allowedOrigin);
256
- response.headers.set(
257
- "Access-Control-Allow-Methods",
258
- "GET, POST, PUT, PATCH, DELETE, OPTIONS"
259
- );
260
- response.headers.set("Access-Control-Allow-Headers", "Content-Type, Authorization");
261
- }
262
- }
263
  return response;
264
  };
 
2
  import type { Handle, HandleServerError, ServerInit } from "@sveltejs/kit";
3
  import { collections } from "$lib/server/database";
4
  import { base } from "$app/paths";
5
+ import { findUser, refreshSessionCookie, requiresUser } from "$lib/server/auth";
6
  import { ERROR_MESSAGES } from "$lib/stores/errors";
7
+ import { sha256 } from "$lib/utils/sha256";
8
  import { addWeeks } from "date-fns";
9
  import { checkAndRunMigrations } from "$lib/migrations/migrations";
10
+ import { building } from "$app/environment";
11
  import { logger } from "$lib/server/logger";
12
  import { AbortedGenerations } from "$lib/server/abortedGenerations";
13
  import { MetricsServer } from "$lib/server/metrics";
14
  import { initExitHandler } from "$lib/server/exitHandler";
15
+ import { ObjectId } from "mongodb";
16
  import { refreshAssistantsCounts } from "$lib/jobs/refresh-assistants-counts";
17
  import { refreshConversationStats } from "$lib/jobs/refresh-conversation-stats";
18
  import { adminTokenManager } from "$lib/server/adminToken";
 
19
 
20
  export const init: ServerInit = async () => {
21
  // Wait for config to be fully loaded
 
120
  }
121
  }
122
 
123
+ const token = event.cookies.get(config.COOKIE_NAME);
124
+
125
+ // if the trusted email header is set we use it to get the user email
126
+ const email = config.TRUSTED_EMAIL_HEADER
127
+ ? event.request.headers.get(config.TRUSTED_EMAIL_HEADER)
128
+ : null;
129
+
130
+ let secretSessionId: string | null = null;
131
+ let sessionId: string | null = null;
132
+
133
+ if (email) {
134
+ secretSessionId = sessionId = await sha256(email);
135
+
136
+ event.locals.user = {
137
+ // generate id based on email
138
+ _id: new ObjectId(sessionId.slice(0, 24)),
139
+ name: email,
140
+ email,
141
+ createdAt: new Date(),
142
+ updatedAt: new Date(),
143
+ hfUserId: email,
144
+ avatarUrl: "",
145
+ logoutDisabled: true,
146
+ };
147
+ } else if (token) {
148
+ secretSessionId = token;
149
+ sessionId = await sha256(token);
150
+
151
+ const user = await findUser(sessionId);
152
+
153
+ if (user) {
154
+ event.locals.user = user;
155
+ }
156
+ } else if (
157
+ event.url.pathname.startsWith(`${base}/api/`) &&
158
+ config.USE_HF_TOKEN_IN_API === "true"
159
+ ) {
160
+ // if the request goes to the API and no user is available in the header
161
+ // check if a bearer token is available in the Authorization header
162
+
163
+ const authorization = event.request.headers.get("Authorization");
164
+
165
+ if (authorization && authorization.startsWith("Bearer ")) {
166
+ const token = authorization.slice(7);
167
+
168
+ const hash = await sha256(token);
169
+
170
+ sessionId = secretSessionId = hash;
171
+
172
+ // check if the hash is in the DB and get the user
173
+ // else check against https://huggingface.co/api/whoami-v2
174
+
175
+ const cacheHit = await collections.tokenCaches.findOne({ tokenHash: hash });
176
+
177
+ if (cacheHit) {
178
+ const user = await collections.users.findOne({ hfUserId: cacheHit.userId });
179
+
180
+ if (!user) {
181
+ return errorResponse(500, "User not found");
182
+ }
183
+
184
+ event.locals.user = user;
185
+ } else {
186
+ const response = await fetch("https://huggingface.co/api/whoami-v2", {
187
+ headers: {
188
+ Authorization: `Bearer ${token}`,
189
+ },
190
+ });
191
+
192
+ if (!response.ok) {
193
+ return errorResponse(401, "Unauthorized");
194
+ }
195
+
196
+ const data = await response.json();
197
+ const user = await collections.users.findOne({ hfUserId: data.id });
198
+
199
+ if (!user) {
200
+ return errorResponse(500, "User not found");
201
+ }
202
+
203
+ await collections.tokenCaches.insertOne({
204
+ tokenHash: hash,
205
+ userId: data.id,
206
+ createdAt: new Date(),
207
+ updatedAt: new Date(),
208
+ });
209
+
210
+ event.locals.user = user;
211
+ }
212
+ }
213
+ }
214
 
215
+ if (!sessionId || !secretSessionId) {
216
+ secretSessionId = crypto.randomUUID();
217
+ sessionId = await sha256(secretSessionId);
218
+
219
+ if (await collections.sessions.findOne({ sessionId })) {
220
+ return errorResponse(500, "Session ID collision");
221
+ }
222
+ }
223
+
224
+ event.locals.sessionId = sessionId;
225
 
226
  event.locals.isAdmin =
227
  event.locals.user?.isAdmin || adminTokenManager.isAdmin(event.locals.sessionId);
 
254
  }
255
  }
256
 
257
+ if (event.request.method === "POST") {
258
+ // if the request is a POST request we refresh the cookie
259
+ refreshSessionCookie(event.cookies, secretSessionId);
 
 
 
 
260
 
261
  await collections.sessions.updateOne(
262
+ { sessionId },
263
  { $set: { updatedAt: new Date(), expiresAt: addWeeks(new Date(), 2) } }
264
  );
265
  }
 
309
 
310
  return chunk.html.replace("%gaId%", config.PUBLIC_GOOGLE_ANALYTICS_ID);
311
  },
 
 
 
312
  });
313
 
314
  // Add CSP header to disallow framing if ALLOW_IFRAME is not "true"
 
316
  response.headers.append("Content-Security-Policy", "frame-ancestors 'none';");
317
  }
318
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
319
  return response;
320
  };
src/hooks.ts DELETED
@@ -1,6 +0,0 @@
1
- import { publicConfigTransporter } from "$lib/utils/PublicConfig.svelte";
2
- import type { Transport } from "@sveltejs/kit";
3
-
4
- export const transport: Transport = {
5
- PublicConfig: publicConfigTransporter,
6
- };
 
 
 
 
 
 
 
src/lib/APIClient.ts DELETED
@@ -1,55 +0,0 @@
1
- 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;
8
-
9
- if (!browser) {
10
- let port;
11
- if (process.argv.includes("--port")) {
12
- port = parseInt(process.argv[process.argv.indexOf("--port") + 1]);
13
- } else {
14
- const mode = process.argv.find((arg) => arg === "preview" || arg === "dev");
15
- if (mode === "preview") {
16
- port = 4173;
17
- } else if (mode === "dev") {
18
- port = 5173;
19
- } else {
20
- port = 3000;
21
- }
22
- }
23
- // Always use localhost for server-side requests to avoid external HTTP calls during SSR
24
- url = `http://localhost:${port}${base}/api/v2`;
25
- } else {
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
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
src/lib/components/AssistantSettings.svelte CHANGED
@@ -12,6 +12,7 @@
12
  import CarbonTools from "~icons/carbon/tools";
13
 
14
  import { useSettingsStore } from "$lib/stores/settings";
 
15
  import IconInternet from "./icons/IconInternet.svelte";
16
  import TokensCounter from "./TokensCounter.svelte";
17
  import HoverTooltip from "./HoverTooltip.svelte";
@@ -19,9 +20,6 @@
19
  import AssistantToolPicker from "./AssistantToolPicker.svelte";
20
  import { error } from "$lib/stores/errors";
21
  import { goto } from "$app/navigation";
22
- import { usePublicConfig } from "$lib/utils/PublicConfig.svelte";
23
-
24
- const publicConfig = usePublicConfig();
25
 
26
  type AssistantFront = Omit<Assistant, "_id" | "createdById"> & { _id: string };
27
 
 
12
  import CarbonTools from "~icons/carbon/tools";
13
 
14
  import { useSettingsStore } from "$lib/stores/settings";
15
+ import { publicConfig } from "$lib/utils/PublicConfig.svelte";
16
  import IconInternet from "./icons/IconInternet.svelte";
17
  import TokensCounter from "./TokensCounter.svelte";
18
  import HoverTooltip from "./HoverTooltip.svelte";
 
20
  import AssistantToolPicker from "./AssistantToolPicker.svelte";
21
  import { error } from "$lib/stores/errors";
22
  import { goto } from "$app/navigation";
 
 
 
23
 
24
  type AssistantFront = Omit<Assistant, "_id" | "createdById"> & { _id: string };
25
 
src/lib/components/DisclaimerModal.svelte CHANGED
@@ -1,15 +1,13 @@
1
  <script lang="ts">
2
  import { base } from "$app/paths";
3
  import { page } from "$app/state";
 
4
 
5
  import LogoHuggingFaceBorderless from "$lib/components/icons/LogoHuggingFaceBorderless.svelte";
6
  import Modal from "$lib/components/Modal.svelte";
7
  import { useSettingsStore } from "$lib/stores/settings";
8
  import { cookiesAreEnabled } from "$lib/utils/cookiesAreEnabled";
9
  import Logo from "./icons/Logo.svelte";
10
- import { usePublicConfig } from "$lib/utils/PublicConfig.svelte";
11
-
12
- const publicConfig = usePublicConfig();
13
 
14
  const settings = useSettingsStore();
15
  </script>
@@ -58,18 +56,20 @@
58
  {/if}
59
  </button>
60
  {#if page.data.loginEnabled}
61
- <a
62
- href="{base}/login"
63
- class="flex w-full flex-wrap items-center justify-center whitespace-nowrap rounded-full border-2 border-black bg-black px-5 py-2 text-lg font-semibold text-gray-100 transition-colors hover:bg-gray-900"
64
- >
65
- Sign in
66
- {#if publicConfig.isHuggingChat}
67
- <span class="flex items-center">
68
- &nbsp;with <LogoHuggingFaceBorderless classNames="text-xl mr-1 ml-1.5 flex-none" /> Hugging
69
- Face
70
- </span>
71
- {/if}
72
- </a>
 
 
73
  {/if}
74
  </div>
75
  </div>
 
1
  <script lang="ts">
2
  import { base } from "$app/paths";
3
  import { page } from "$app/state";
4
+ import { publicConfig } from "$lib/utils/PublicConfig.svelte";
5
 
6
  import LogoHuggingFaceBorderless from "$lib/components/icons/LogoHuggingFaceBorderless.svelte";
7
  import Modal from "$lib/components/Modal.svelte";
8
  import { useSettingsStore } from "$lib/stores/settings";
9
  import { cookiesAreEnabled } from "$lib/utils/cookiesAreEnabled";
10
  import Logo from "./icons/Logo.svelte";
 
 
 
11
 
12
  const settings = useSettingsStore();
13
  </script>
 
56
  {/if}
57
  </button>
58
  {#if page.data.loginEnabled}
59
+ <form action="{base}/login" target="_parent" method="POST" class="w-full">
60
+ <button
61
+ type="submit"
62
+ class="flex w-full flex-wrap items-center justify-center whitespace-nowrap rounded-full border-2 border-black bg-black px-5 py-2 text-lg font-semibold text-gray-100 transition-colors hover:bg-gray-900"
63
+ >
64
+ Sign in
65
+ {#if publicConfig.isHuggingChat}
66
+ <span class="flex items-center">
67
+ &nbsp;with <LogoHuggingFaceBorderless classNames="text-xl mr-1 ml-1.5 flex-none" /> Hugging
68
+ Face
69
+ </span>
70
+ {/if}
71
+ </button>
72
+ </form>
73
  {/if}
74
  </div>
75
  </div>
src/lib/components/LoginModal.svelte CHANGED
@@ -1,15 +1,14 @@
1
  <script lang="ts">
2
  import { base } from "$app/paths";
3
  import { page } from "$app/state";
 
4
 
5
  import LogoHuggingFaceBorderless from "$lib/components/icons/LogoHuggingFaceBorderless.svelte";
6
  import Modal from "$lib/components/Modal.svelte";
7
  import { useSettingsStore } from "$lib/stores/settings";
8
  import { cookiesAreEnabled } from "$lib/utils/cookiesAreEnabled";
9
  import Logo from "./icons/Logo.svelte";
10
- import { usePublicConfig } from "$lib/utils/PublicConfig.svelte";
11
 
12
- const publicConfig = usePublicConfig();
13
  const settings = useSettingsStore();
14
  </script>
15
 
@@ -28,10 +27,15 @@
28
  {publicConfig.PUBLIC_APP_GUEST_MESSAGE}
29
  </p>
30
 
31
- <div class="flex w-full flex-col items-center gap-2">
 
 
 
 
 
32
  {#if page.data.loginRequired}
33
- <a
34
- href="{base}/login"
35
  class="flex w-full flex-wrap items-center justify-center whitespace-nowrap rounded-full bg-black px-5 py-2 text-center text-lg font-semibold text-gray-100 transition-colors hover:bg-gray-900"
36
  >
37
  Sign in
@@ -40,7 +44,7 @@
40
  &nbsp;with <LogoHuggingFaceBorderless classNames="text-xl mr-1 ml-1.5" /> Hugging Face
41
  </span>
42
  {/if}
43
- </a>
44
  {:else}
45
  <button
46
  class="flex w-full items-center justify-center whitespace-nowrap rounded-full border-2 border-black bg-black px-5 py-2 text-lg font-semibold text-gray-100 transition-colors hover:bg-gray-900"
@@ -55,6 +59,6 @@
55
  Start chatting
56
  </button>
57
  {/if}
58
- </div>
59
  </div>
60
  </Modal>
 
1
  <script lang="ts">
2
  import { base } from "$app/paths";
3
  import { page } from "$app/state";
4
+ import { publicConfig } from "$lib/utils/PublicConfig.svelte";
5
 
6
  import LogoHuggingFaceBorderless from "$lib/components/icons/LogoHuggingFaceBorderless.svelte";
7
  import Modal from "$lib/components/Modal.svelte";
8
  import { useSettingsStore } from "$lib/stores/settings";
9
  import { cookiesAreEnabled } from "$lib/utils/cookiesAreEnabled";
10
  import Logo from "./icons/Logo.svelte";
 
11
 
 
12
  const settings = useSettingsStore();
13
  </script>
14
 
 
27
  {publicConfig.PUBLIC_APP_GUEST_MESSAGE}
28
  </p>
29
 
30
+ <form
31
+ action="{base}/{page.data.loginRequired ? 'login' : 'settings'}"
32
+ target="_parent"
33
+ method="POST"
34
+ class="flex w-full flex-col items-center gap-2"
35
+ >
36
  {#if page.data.loginRequired}
37
+ <button
38
+ type="submit"
39
  class="flex w-full flex-wrap items-center justify-center whitespace-nowrap rounded-full bg-black px-5 py-2 text-center text-lg font-semibold text-gray-100 transition-colors hover:bg-gray-900"
40
  >
41
  Sign in
 
44
  &nbsp;with <LogoHuggingFaceBorderless classNames="text-xl mr-1 ml-1.5" /> Hugging Face
45
  </span>
46
  {/if}
47
+ </button>
48
  {:else}
49
  <button
50
  class="flex w-full items-center justify-center whitespace-nowrap rounded-full border-2 border-black bg-black px-5 py-2 text-lg font-semibold text-gray-100 transition-colors hover:bg-gray-900"
 
59
  Start chatting
60
  </button>
61
  {/if}
62
+ </form>
63
  </div>
64
  </Modal>
src/lib/components/NavConversationItem.svelte CHANGED
@@ -43,15 +43,11 @@
43
  <span class="mr-1 font-semibold"> Delete </span>
44
  {/if}
45
  {#if conv.avatarUrl}
46
- {#await conv.avatarUrl then avatarUrl}
47
- {#if avatarUrl}
48
- <img
49
- src="{base}{avatarUrl}"
50
- alt="Assistant avatar"
51
- class="mr-1.5 inline size-4 flex-none rounded-full object-cover"
52
- />
53
- {/if}
54
- {/await}
55
  {conv.title.replace(/\p{Emoji}/gu, "")}
56
  {:else if conv.assistantId}
57
  <div
 
43
  <span class="mr-1 font-semibold"> Delete </span>
44
  {/if}
45
  {#if conv.avatarUrl}
46
+ <img
47
+ src="{base}{conv.avatarUrl}"
48
+ alt="Assistant avatar"
49
+ class="mr-1.5 inline size-4 flex-none rounded-full object-cover"
50
+ />
 
 
 
 
51
  {conv.title.replace(/\p{Emoji}/gu, "")}
52
  {:else if conv.assistantId}
53
  <div
src/lib/components/NavMenu.svelte CHANGED
@@ -13,6 +13,7 @@
13
  import Logo from "$lib/components/icons/Logo.svelte";
14
  import { switchTheme } from "$lib/switchTheme";
15
  import { isAborted } from "$lib/stores/isAborted";
 
16
 
17
  import NavConversationItem from "./NavConversationItem.svelte";
18
  import type { LayoutData } from "../../routes/$types";
@@ -20,20 +21,13 @@
20
  import type { Model } from "$lib/types/Model";
21
  import { page } from "$app/stores";
22
  import InfiniteScroll from "./InfiniteScroll.svelte";
 
23
  import { CONV_NUM_PER_PAGE } from "$lib/constants/pagination";
24
- import { goto } from "$app/navigation";
25
  import { browser } from "$app/environment";
26
  import { toggleSearch } from "./chat/Search.svelte";
27
  import CarbonSearch from "~icons/carbon/search";
28
  import { closeMobileNav } from "./MobileNav.svelte";
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();
37
 
38
  interface Props {
39
  conversations: ConvSidebar[];
@@ -71,18 +65,15 @@
71
 
72
  async function handleVisible() {
73
  p++;
74
- const newConvs = await client.conversations
75
- .get({
76
- query: {
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
 
@@ -175,13 +166,9 @@
175
  class="flex touch-none flex-col gap-1 rounded-r-xl p-3 text-sm md:mt-3 md:bg-gradient-to-l md:from-gray-50 md:dark:from-gray-800/30"
176
  >
177
  {#if user?.username || user?.email}
178
- <button
179
- onclick={async () => {
180
- await fetch(`${base}/logout`, {
181
- method: "POST",
182
- });
183
- await goto(base + "/", { invalidateAll: true });
184
- }}
185
  class="group flex items-center gap-1.5 rounded-lg pl-2.5 pr-2 hover:bg-gray-100 dark:hover:bg-gray-700"
186
  >
187
  <span
@@ -189,21 +176,24 @@
189
  >{user?.username || user?.email}</span
190
  >
191
  {#if !user.logoutDisabled}
192
- <span
 
193
  class="ml-auto h-6 flex-none items-center gap-1.5 rounded-md border bg-white px-2 text-gray-700 shadow-sm group-hover:flex hover:shadow-none dark:border-gray-600 dark:bg-gray-600 dark:text-gray-400 dark:hover:text-gray-300 md:hidden"
194
  >
195
  Sign Out
196
- </span>
197
  {/if}
198
- </button>
199
  {/if}
200
  {#if canLogin}
201
- <a
202
- href="{base}/login"
203
- class="flex h-9 w-full flex-none items-center gap-1.5 rounded-lg pl-2.5 pr-2 text-gray-500 hover:bg-gray-100 dark:text-gray-400 dark:hover:bg-gray-700"
204
- >
205
- Login
206
- </a>
 
 
207
  {/if}
208
  {#if nModels > 1}
209
  <a
 
13
  import Logo from "$lib/components/icons/Logo.svelte";
14
  import { switchTheme } from "$lib/switchTheme";
15
  import { isAborted } from "$lib/stores/isAborted";
16
+ import { publicConfig } from "$lib/utils/PublicConfig.svelte";
17
 
18
  import NavConversationItem from "./NavConversationItem.svelte";
19
  import type { LayoutData } from "../../routes/$types";
 
21
  import type { Model } from "$lib/types/Model";
22
  import { page } from "$app/stores";
23
  import InfiniteScroll from "./InfiniteScroll.svelte";
24
+ import type { Conversation } from "$lib/types/Conversation";
25
  import { CONV_NUM_PER_PAGE } from "$lib/constants/pagination";
 
26
  import { browser } from "$app/environment";
27
  import { toggleSearch } from "./chat/Search.svelte";
28
  import CarbonSearch from "~icons/carbon/search";
29
  import { closeMobileNav } from "./MobileNav.svelte";
 
 
30
  import { isVirtualKeyboard } from "$lib/utils/isVirtualKeyboard";
 
 
 
 
 
31
 
32
  interface Props {
33
  conversations: ConvSidebar[];
 
65
 
66
  async function handleVisible() {
67
  p++;
68
+ const newConvs = await fetch(`${base}/api/conversations?p=${p}`)
69
+ .then((res) => res.json())
70
+ .then((convs) =>
71
+ convs.map(
72
+ (conv: Pick<Conversation, "_id" | "title" | "updatedAt" | "model" | "assistantId">) => ({
73
+ ...conv,
74
+ updatedAt: new Date(conv.updatedAt),
75
+ })
76
+ )
 
 
 
77
  )
78
  .catch(() => []);
79
 
 
166
  class="flex touch-none flex-col gap-1 rounded-r-xl p-3 text-sm md:mt-3 md:bg-gradient-to-l md:from-gray-50 md:dark:from-gray-800/30"
167
  >
168
  {#if user?.username || user?.email}
169
+ <form
170
+ action="{base}/logout"
171
+ method="post"
 
 
 
 
172
  class="group flex items-center gap-1.5 rounded-lg pl-2.5 pr-2 hover:bg-gray-100 dark:hover:bg-gray-700"
173
  >
174
  <span
 
176
  >{user?.username || user?.email}</span
177
  >
178
  {#if !user.logoutDisabled}
179
+ <button
180
+ type="submit"
181
  class="ml-auto h-6 flex-none items-center gap-1.5 rounded-md border bg-white px-2 text-gray-700 shadow-sm group-hover:flex hover:shadow-none dark:border-gray-600 dark:bg-gray-600 dark:text-gray-400 dark:hover:text-gray-300 md:hidden"
182
  >
183
  Sign Out
184
+ </button>
185
  {/if}
186
+ </form>
187
  {/if}
188
  {#if canLogin}
189
+ <form action="{base}/login" method="POST" target="_parent">
190
+ <button
191
+ type="submit"
192
+ class="flex h-9 w-full flex-none items-center gap-1.5 rounded-lg pl-2.5 pr-2 text-gray-500 hover:bg-gray-100 dark:text-gray-400 dark:hover:bg-gray-700"
193
+ >
194
+ Login
195
+ </button>
196
+ </form>
197
  {/if}
198
  {#if nModels > 1}
199
  <a
src/lib/components/ToolsMenu.svelte CHANGED
@@ -4,12 +4,10 @@
4
  import { clickOutside } from "$lib/actions/clickOutside";
5
  import { useSettingsStore } from "$lib/stores/settings";
6
  import type { ToolFront } from "$lib/types/Tool";
 
7
  import IconTool from "./icons/IconTool.svelte";
8
  import CarbonInformation from "~icons/carbon/information";
9
  import CarbonGlobe from "~icons/carbon/earth-filled";
10
- import { usePublicConfig } from "$lib/utils/PublicConfig.svelte";
11
-
12
- const publicConfig = usePublicConfig();
13
 
14
  interface Props {
15
  loading?: boolean;
 
4
  import { clickOutside } from "$lib/actions/clickOutside";
5
  import { useSettingsStore } from "$lib/stores/settings";
6
  import type { ToolFront } from "$lib/types/Tool";
7
+ import { publicConfig } from "$lib/utils/PublicConfig.svelte";
8
  import IconTool from "./icons/IconTool.svelte";
9
  import CarbonInformation from "~icons/carbon/information";
10
  import CarbonGlobe from "~icons/carbon/earth-filled";
 
 
 
11
 
12
  interface Props {
13
  loading?: boolean;
src/lib/components/chat/AssistantIntroduction.svelte CHANGED
@@ -15,17 +15,14 @@
15
  import CarbonTools from "~icons/carbon/tools";
16
 
17
  import { share } from "$lib/utils/share";
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"
 
15
  import CarbonTools from "~icons/carbon/tools";
16
 
17
  import { share } from "$lib/utils/share";
18
+ import { publicConfig } from "$lib/utils/PublicConfig.svelte";
19
 
20
  import { page } from "$app/state";
 
 
 
21
 
22
  interface Props {
23
  models: Model[];
24
  assistant: Pick<
25
+ Assistant,
26
  | "avatar"
27
  | "name"
28
  | "rag"
src/lib/components/chat/ChatInput.svelte CHANGED
@@ -23,8 +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 {
30
  files?: File[];
@@ -33,7 +31,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
  import { isVirtualKeyboard } from "$lib/utils/isVirtualKeyboard";
27
  interface Props {
28
  files?: File[];
 
31
  placeholder?: string;
32
  loading?: boolean;
33
  disabled?: boolean;
34
+ assistant?: Assistant | undefined;
35
  modelHasTools?: boolean;
36
  modelIsMultimodal?: boolean;
37
  children?: import("svelte").Snippet;
src/lib/components/chat/ChatIntroduction.svelte CHANGED
@@ -1,4 +1,6 @@
1
  <script lang="ts">
 
 
2
  import Logo from "$lib/components/icons/Logo.svelte";
3
  import { createEventDispatcher } from "svelte";
4
  import IconGear from "~icons/bi/gear-fill";
@@ -7,9 +9,6 @@
7
  import ModelCardMetadata from "../ModelCardMetadata.svelte";
8
  import { base } from "$app/paths";
9
  import JSON5 from "json5";
10
- import { usePublicConfig } from "$lib/utils/PublicConfig.svelte";
11
-
12
- const publicConfig = usePublicConfig();
13
 
14
  interface Props {
15
  currentModel: Model;
 
1
  <script lang="ts">
2
+ import { publicConfig } from "$lib/utils/PublicConfig.svelte";
3
+
4
  import Logo from "$lib/components/icons/Logo.svelte";
5
  import { createEventDispatcher } from "svelte";
6
  import IconGear from "~icons/bi/gear-fill";
 
9
  import ModelCardMetadata from "../ModelCardMetadata.svelte";
10
  import { base } from "$app/paths";
11
  import JSON5 from "json5";
 
 
 
12
 
13
  interface Props {
14
  currentModel: Model;
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
  }
@@ -260,7 +259,7 @@
260
  {#if assistant && !!messages.length}
261
  <a
262
  class="mx-auto flex items-center gap-1.5 rounded-full border border-gray-100 bg-gray-50 py-1 pl-1 pr-3 text-sm text-gray-800 hover:bg-gray-100 dark:border-gray-800 dark:bg-gray-800 dark:text-gray-200 dark:hover:bg-gray-700"
263
- href="{base}/assistant/{assistant._id}"
264
  >
265
  {#if assistant.avatar}
266
  <img
 
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
  }
 
259
  {#if assistant && !!messages.length}
260
  <a
261
  class="mx-auto flex items-center gap-1.5 rounded-full border border-gray-100 bg-gray-50 py-1 pl-1 pr-3 text-sm text-gray-800 hover:bg-gray-100 dark:border-gray-800 dark:bg-gray-800 dark:text-gray-200 dark:hover:bg-gray-700"
262
+ href="{base}/settings/assistants/{assistant._id}"
263
  >
264
  {#if assistant.avatar}
265
  <img
src/lib/components/icons/Logo.svelte CHANGED
@@ -1,7 +1,8 @@
1
  <script lang="ts">
2
- import { usePublicConfig } from "$lib/utils/PublicConfig.svelte";
 
3
 
4
- const publicConfig = usePublicConfig();
5
 
6
  interface Props {
7
  classNames?: string;
@@ -10,6 +11,16 @@
10
  let { classNames = "" }: Props = $props();
11
  </script>
12
 
 
 
 
 
 
 
 
 
 
 
13
  {#if publicConfig.PUBLIC_APP_ASSETS === "chatui"}
14
  <svg
15
  height="30"
@@ -27,6 +38,7 @@
27
  <img
28
  class={classNames}
29
  alt="{publicConfig.PUBLIC_APP_NAME} logo"
30
- src="{publicConfig.assetPath}/logo.svg"
 
31
  />
32
  {/if}
 
1
  <script lang="ts">
2
+ import { page } from "$app/state";
3
+ import { publicConfig } from "$lib/utils/PublicConfig.svelte";
4
 
5
+ import { base } from "$app/paths";
6
 
7
  interface Props {
8
  classNames?: string;
 
11
  let { classNames = "" }: Props = $props();
12
  </script>
13
 
14
+ <svelte:head>
15
+ <link
16
+ rel="preload"
17
+ href="{publicConfig.PUBLIC_ORIGIN ||
18
+ page.url.origin}{base}/{publicConfig.PUBLIC_APP_ASSETS}/logo.svg"
19
+ as="image"
20
+ type="image/svg+xml"
21
+ />
22
+ </svelte:head>
23
+
24
  {#if publicConfig.PUBLIC_APP_ASSETS === "chatui"}
25
  <svg
26
  height="30"
 
38
  <img
39
  class={classNames}
40
  alt="{publicConfig.PUBLIC_APP_NAME} logo"
41
+ src="{publicConfig.PUBLIC_ORIGIN ||
42
+ page.url.origin}{base}/{publicConfig.PUBLIC_APP_ASSETS}/logo.svg"
43
  />
44
  {/if}
src/lib/server/api/authPlugin.ts DELETED
@@ -1,25 +0,0 @@
1
- import Elysia from "elysia";
2
- import { authenticateRequest } from "../auth";
3
-
4
- export const authPlugin = new Elysia({ name: "auth" }).derive(
5
- { as: "scoped" },
6
- async ({
7
- headers,
8
- cookie,
9
- }): Promise<{
10
- locals: App.Locals;
11
- }> => {
12
- const auth = await authenticateRequest(
13
- { type: "elysia", value: headers },
14
- { type: "elysia", value: cookie },
15
- true
16
- );
17
- return {
18
- locals: {
19
- user: auth?.user,
20
- sessionId: auth?.sessionId,
21
- isAdmin: auth?.isAdmin,
22
- },
23
- };
24
- }
25
- );
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
src/lib/server/api/index.ts DELETED
@@ -1,35 +0,0 @@
1
- import { authPlugin } from "$api/authPlugin";
2
- import { conversationGroup } from "$api/routes/groups/conversations";
3
- import { assistantGroup } from "$api/routes/groups/assistants";
4
- import { userGroup } from "$api/routes/groups/user";
5
- import { toolGroup } from "$api/routes/groups/tools";
6
- import { misc } from "$api/routes/groups/misc";
7
- import { modelGroup } from "$api/routes/groups/models";
8
-
9
- import { Elysia } from "elysia";
10
- import { base } from "$app/paths";
11
- import { swagger } from "@elysiajs/swagger";
12
-
13
- const prefix = `${base}/api/v2` as unknown as "";
14
-
15
- export const app = new Elysia({ prefix })
16
- .use(
17
- swagger({
18
- documentation: {
19
- info: {
20
- title: "Elysia Documentation",
21
- version: "1.0.0",
22
- },
23
- },
24
- provider: "swagger-ui",
25
- })
26
- )
27
- .use(authPlugin)
28
- .use(conversationGroup)
29
- .use(toolGroup)
30
- .use(assistantGroup)
31
- .use(userGroup)
32
- .use(modelGroup)
33
- .use(misc);
34
-
35
- export type App = typeof app;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
src/lib/server/api/routes/groups/assistants.ts DELETED
@@ -1,180 +0,0 @@
1
- import { Elysia, t } from "elysia";
2
- import { authPlugin } from "$api/authPlugin";
3
- import { collections } from "$lib/server/database";
4
- import { ObjectId, type Filter } from "mongodb";
5
- import { authCondition } from "$lib/server/auth";
6
- 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) => {
26
- return app
27
- .get("/", () => {
28
- // todo: get assistants
29
- throw new Error("Not implemented");
30
- })
31
- .post("/", () => {
32
- // todo: post new assistant
33
- throw new Error("Not implemented");
34
- })
35
- .get(
36
- "/search",
37
- async ({ query, locals, error }) => {
38
- if (!config.ENABLE_ASSISTANTS) {
39
- error(403, "Assistants are not enabled");
40
- }
41
- const modelId = query.modelId;
42
- const pageIndex = query.p ?? 0;
43
- const username = query.user;
44
- const search = query.q?.trim() ?? null;
45
- const sort = query.sort ?? SortKey.TRENDING;
46
- const showUnfeatured = query.showUnfeatured ?? false;
47
- const createdByCurrentUser = locals.user?.username && locals.user.username === username;
48
-
49
- let user: Pick<User, "_id"> | null = null;
50
- if (username) {
51
- user = await collections.users.findOne<Pick<User, "_id">>(
52
- { username },
53
- { projection: { _id: 1 } }
54
- );
55
- if (!user) {
56
- error(404, `User "${username}" doesn't exist`);
57
- }
58
- }
59
- // if we require featured assistants, that we are not on a user page and we are not an admin who wants to see unfeatured assistants, we show featured assistants
60
- let shouldBeFeatured = {};
61
-
62
- if (config.REQUIRE_FEATURED_ASSISTANTS === "true" && !(locals.isAdmin && showUnfeatured)) {
63
- if (!user) {
64
- // only show featured assistants on the community page
65
- shouldBeFeatured = { review: ReviewStatus.APPROVED };
66
- } else if (!createdByCurrentUser) {
67
- // on a user page show assistants that have been approved or are pending
68
- shouldBeFeatured = { review: { $in: [ReviewStatus.APPROVED, ReviewStatus.PENDING] } };
69
- }
70
- }
71
-
72
- const noSpecificSearch = !user && !search;
73
- // fetch the top assistants sorted by user count from biggest to smallest.
74
- // filter by model too if modelId is provided or query if query is provided
75
- // only show assistants that have been used by more than 5 users if no specific search is made
76
- const filter: Filter<Assistant> = {
77
- ...(modelId && { modelId }),
78
- ...(user && { createdById: user._id }),
79
- ...(search && { searchTokens: { $all: generateQueryTokens(search) } }),
80
- ...(noSpecificSearch && { userCount: { $gte: 5 } }),
81
- ...shouldBeFeatured,
82
- };
83
-
84
- const assistants = await collections.assistants
85
- .find(filter)
86
- .sort({
87
- ...(sort === SortKey.TRENDING && { last24HoursCount: -1 }),
88
- userCount: -1,
89
- _id: 1,
90
- })
91
- .skip(NUM_PER_PAGE * pageIndex)
92
- .limit(NUM_PER_PAGE)
93
- .toArray();
94
-
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,
102
- query: search,
103
- sort,
104
- showUnfeatured,
105
- };
106
- },
107
- {
108
- query: t.Object({
109
- user: t.Optional(t.String()),
110
- q: t.Optional(t.String()),
111
- sort: t.Optional(t.Enum(SortKey)),
112
- p: t.Optional(t.Numeric()),
113
- showUnfeatured: t.Optional(t.Boolean()),
114
- modelId: t.Optional(t.String()),
115
- }),
116
- }
117
- )
118
- .group("/:id", (app) => {
119
- return app
120
- .derive(async ({ params, error }) => {
121
- const assistant = await collections.assistants.findOne({
122
- _id: new ObjectId(params.id),
123
- });
124
-
125
- if (!assistant) {
126
- return error(404, "Assistant not found");
127
- }
128
-
129
- return { assistant };
130
- })
131
- .get("", ({ assistant }) => {
132
- return assistant;
133
- })
134
- .patch("", () => {
135
- // todo: patch assistant
136
- throw new Error("Not implemented");
137
- })
138
- .delete("/", () => {
139
- // todo: delete assistant
140
- throw new Error("Not implemented");
141
- })
142
- .post("/report", () => {
143
- // todo: report assistant
144
- throw new Error("Not implemented");
145
- })
146
- .patch("/review", () => {
147
- // todo: review assistant
148
- throw new Error("Not implemented");
149
- })
150
- .post("/follow", async ({ locals, assistant }) => {
151
- const result = await collections.settings.updateOne(authCondition(locals), {
152
- $addToSet: { assistants: assistant._id },
153
- $set: { activeModel: assistant._id.toString() },
154
- });
155
-
156
- if (result.modifiedCount > 0) {
157
- await collections.assistants.updateOne(
158
- { _id: assistant._id },
159
- { $inc: { userCount: 1 } }
160
- );
161
- }
162
-
163
- return { message: "Assistant subscribed" };
164
- })
165
- .delete("/follow", async ({ locals, assistant }) => {
166
- const result = await collections.settings.updateOne(authCondition(locals), {
167
- $pull: { assistants: assistant._id },
168
- });
169
-
170
- if (result.modifiedCount > 0) {
171
- await collections.assistants.updateOne(
172
- { _id: assistant._id },
173
- { $inc: { userCount: -1 } }
174
- );
175
- }
176
-
177
- return { message: "Assistant unsubscribed" };
178
- });
179
- });
180
- });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
src/lib/server/api/routes/groups/conversations.ts DELETED
@@ -1,171 +0,0 @@
1
- import { Elysia, error, t } from "elysia";
2
- import { authPlugin } from "$api/authPlugin";
3
- import { collections } from "$lib/server/database";
4
- import { ObjectId } from "mongodb";
5
- 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
14
- .guard({
15
- as: "scoped",
16
- beforeHandle: async ({ locals }) => {
17
- if (!locals.user?._id && !locals.sessionId) {
18
- return error(401, "Must have a valid session or user");
19
- }
20
- },
21
- })
22
- .get(
23
- "",
24
- async ({ locals, query }) => {
25
- const convs = await collections.conversations
26
- .find(authCondition(locals))
27
- .project<Pick<Conversation, "_id" | "title" | "updatedAt" | "model" | "assistantId">>({
28
- title: 1,
29
- updatedAt: 1,
30
- model: 1,
31
- assistantId: 1,
32
- })
33
- .sort({ updatedAt: -1 })
34
- .skip((query.p ?? 0) * CONV_NUM_PER_PAGE)
35
- .limit(CONV_NUM_PER_PAGE)
36
- .toArray();
37
-
38
- const nConversations = await collections.conversations.countDocuments(
39
- authCondition(locals)
40
- );
41
-
42
- const res = convs.map((conv) => ({
43
- _id: conv._id,
44
- id: conv._id, // legacy param iOS
45
- title: conv.title,
46
- updatedAt: conv.updatedAt,
47
- model: conv.model,
48
- modelId: conv.model, // legacy param iOS
49
- assistantId: conv.assistantId,
50
- modelTools: models.find((m) => m.id == conv.model)?.tools ?? false,
51
- }));
52
-
53
- return { conversations: res, nConversations };
54
- },
55
- {
56
- query: t.Object({
57
- p: t.Optional(t.Number()),
58
- }),
59
- }
60
- )
61
- .delete("", async ({ locals }) => {
62
- const res = await collections.conversations.deleteMany({
63
- ...authCondition(locals),
64
- });
65
- return res.deletedCount;
66
- })
67
- .group(
68
- "/:id",
69
- {
70
- params: t.Object({
71
- id: t.String(),
72
- }),
73
- },
74
- (app) => {
75
- return app
76
- .derive(async ({ locals, params, error }) => {
77
- let conversation;
78
- let shared = false;
79
-
80
- // if the conver
81
- if (params.id.length === 7) {
82
- // shared link of length 7
83
- conversation = await collections.sharedConversations.findOne({
84
- _id: params.id,
85
- });
86
- shared = true;
87
-
88
- if (!conversation) {
89
- return error(404, "Conversation not found");
90
- }
91
- } else {
92
- // todo: add validation on params.id
93
- try {
94
- new ObjectId(params.id);
95
- } catch {
96
- return error(400, "Invalid conversation ID format");
97
- }
98
- conversation = await collections.conversations.findOne({
99
- _id: new ObjectId(params.id),
100
- ...authCondition(locals),
101
- });
102
-
103
- if (!conversation) {
104
- const conversationExists =
105
- (await collections.conversations.countDocuments({
106
- _id: new ObjectId(params.id),
107
- })) !== 0;
108
-
109
- if (conversationExists) {
110
- return error(
111
- 403,
112
- "You don't have access to this conversation. If someone gave you this link, ask them to use the 'share' feature instead."
113
- );
114
- }
115
-
116
- return error(404, "Conversation not found.");
117
- }
118
- }
119
-
120
- const convertedConv = {
121
- ...conversation,
122
- ...convertLegacyConversation(conversation),
123
- shared,
124
- };
125
-
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,
144
- modelId: conversation.model,
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
152
- throw new Error("Not implemented");
153
- })
154
- .delete("", () => {
155
- throw new Error("Not implemented");
156
- })
157
- .get("/output/:sha256", () => {
158
- // todo: get output
159
- throw new Error("Not implemented");
160
- })
161
- .post("/share", () => {
162
- // todo: share conversation
163
- throw new Error("Not implemented");
164
- })
165
- .post("/stop-generating", () => {
166
- // todo: stop generating
167
- throw new Error("Not implemented");
168
- });
169
- }
170
- );
171
- });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
src/lib/server/api/routes/groups/misc.ts DELETED
@@ -1,77 +0,0 @@
1
- import { Elysia } from "elysia";
2
- import { authPlugin } from "../../authPlugin";
3
- import { requiresUser } from "$lib/server/auth";
4
- import { collections } from "$lib/server/database";
5
- import { authCondition } from "$lib/server/auth";
6
- import { config } from "$lib/server/config";
7
-
8
- export interface FeatureFlags {
9
- searchEnabled: boolean;
10
- enableAssistants: boolean;
11
- enableAssistantsRAG: boolean;
12
- enableCommunityTools: boolean;
13
- loginEnabled: boolean;
14
- loginRequired: boolean;
15
- guestMode: boolean;
16
- isAdmin: boolean;
17
- }
18
-
19
- export const misc = new Elysia()
20
- .use(authPlugin)
21
- .get("/public-config", async () => config.getPublicConfig())
22
- .get("/feature-flags", async ({ locals }) => {
23
- let loginRequired = false;
24
- const messagesBeforeLogin = config.MESSAGES_BEFORE_LOGIN
25
- ? parseInt(config.MESSAGES_BEFORE_LOGIN)
26
- : 0;
27
- const nConversations = await collections.conversations.countDocuments(authCondition(locals));
28
-
29
- if (requiresUser && !locals.user) {
30
- if (messagesBeforeLogin === 0) {
31
- loginRequired = true;
32
- } else if (nConversations >= messagesBeforeLogin) {
33
- loginRequired = true;
34
- } else {
35
- // get the number of messages where `from === "assistant"` across all conversations.
36
- const totalMessages =
37
- (
38
- await collections.conversations
39
- .aggregate([
40
- { $match: { ...authCondition(locals), "messages.from": "assistant" } },
41
- { $project: { messages: 1 } },
42
- { $limit: messagesBeforeLogin + 1 },
43
- { $unwind: "$messages" },
44
- { $match: { "messages.from": "assistant" } },
45
- { $count: "messages" },
46
- ])
47
- .toArray()
48
- )[0]?.messages ?? 0;
49
-
50
- loginRequired = totalMessages >= messagesBeforeLogin;
51
- }
52
- }
53
-
54
- return {
55
- searchEnabled: !!(
56
- config.SERPAPI_KEY ||
57
- config.SERPER_API_KEY ||
58
- config.SERPSTACK_API_KEY ||
59
- config.SEARCHAPI_KEY ||
60
- config.YDC_API_KEY ||
61
- config.USE_LOCAL_WEBSEARCH ||
62
- config.SEARXNG_QUERY_URL ||
63
- config.BING_SUBSCRIPTION_KEY
64
- ),
65
- enableAssistants: config.ENABLE_ASSISTANTS === "true",
66
- enableAssistantsRAG: config.ENABLE_ASSISTANTS_RAG === "true",
67
- enableCommunityTools: config.COMMUNITY_TOOLS === "true",
68
- loginEnabled: requiresUser, // misnomer, this is actually whether the feature is available, not required
69
- loginRequired,
70
- guestMode: requiresUser && messagesBeforeLogin > 0,
71
- isAdmin: locals.isAdmin,
72
- } satisfies FeatureFlags;
73
- })
74
- .get("/spaces-config", () => {
75
- // todo: get spaces config
76
- return;
77
- });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
src/lib/server/api/routes/groups/models.ts DELETED
@@ -1,106 +0,0 @@
1
- import { Elysia } from "elysia";
2
- import { models, oldModels, type BackendModel } from "$lib/server/models";
3
- import { authPlugin } from "../../authPlugin";
4
- import { authCondition } from "$lib/server/auth";
5
- import { collections } from "$lib/server/database";
6
-
7
- export type GETModelsResponse = Array<{
8
- id: string;
9
- name: string;
10
- websiteUrl?: string;
11
- modelUrl?: string;
12
- tokenizer?: string | { tokenizerUrl: string; tokenizerConfigUrl: string };
13
- datasetName?: string;
14
- datasetUrl?: string;
15
- displayName: string;
16
- description?: string;
17
- reasoning: boolean;
18
- logoUrl?: string;
19
- promptExamples?: { title: string; prompt: string }[];
20
- parameters: BackendModel["parameters"];
21
- preprompt?: string;
22
- multimodal: boolean;
23
- multimodalAcceptedMimetypes?: string[];
24
- tools: boolean;
25
- unlisted: boolean;
26
- hasInferenceAPI: boolean;
27
- }>;
28
-
29
- export type GETOldModelsResponse = Array<{
30
- id: string;
31
- name: string;
32
- displayName: string;
33
- transferTo?: string;
34
- }>;
35
-
36
- export const modelGroup = new Elysia().group("/models", (app) =>
37
- app
38
- .get("/", () => {
39
- return models
40
- .filter((m) => m.unlisted == false)
41
- .map((model) => ({
42
- id: model.id,
43
- name: model.name,
44
- websiteUrl: model.websiteUrl,
45
- modelUrl: model.modelUrl,
46
- tokenizer: model.tokenizer,
47
- datasetName: model.datasetName,
48
- datasetUrl: model.datasetUrl,
49
- displayName: model.displayName,
50
- description: model.description,
51
- reasoning: !!model.reasoning,
52
- logoUrl: model.logoUrl,
53
- promptExamples: model.promptExamples,
54
- parameters: model.parameters,
55
- preprompt: model.preprompt,
56
- multimodal: model.multimodal,
57
- multimodalAcceptedMimetypes: model.multimodalAcceptedMimetypes,
58
- tools: model.tools,
59
- unlisted: model.unlisted,
60
- hasInferenceAPI: model.hasInferenceAPI,
61
- })) satisfies GETModelsResponse;
62
- })
63
- .get("/old", () => {
64
- return oldModels satisfies GETOldModelsResponse;
65
- })
66
- .group("/:namespace/:model?", (app) =>
67
- app
68
- .derive(async ({ params, error }) => {
69
- let modelId: string = params.namespace;
70
- if (params.model) {
71
- modelId += "/" + params.model;
72
- }
73
- const model = models.find((m) => m.id === modelId);
74
- if (!model || model.unlisted) {
75
- return error(404, "Model not found");
76
- }
77
- return { model };
78
- })
79
- .get("/", ({ model }) => {
80
- return model;
81
- })
82
- .use(authPlugin)
83
- .post("/subscribe", async ({ locals, model, error }) => {
84
- if (!locals.sessionId) {
85
- return error(401, "Unauthorized");
86
- }
87
- await collections.settings.updateOne(
88
- authCondition(locals),
89
- {
90
- $set: {
91
- activeModel: model.id,
92
- updatedAt: new Date(),
93
- },
94
- $setOnInsert: {
95
- createdAt: new Date(),
96
- },
97
- },
98
- {
99
- upsert: true,
100
- }
101
- );
102
-
103
- return new Response();
104
- })
105
- )
106
- );
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
src/lib/server/api/routes/groups/tools.ts DELETED
@@ -1,253 +0,0 @@
1
- import { Elysia, t } from "elysia";
2
- import { authPlugin } from "$api/authPlugin";
3
- 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 }) => {
31
- const settings = await collections.settings.findOne(authCondition(locals));
32
-
33
- if (!settings) {
34
- return [];
35
- }
36
-
37
- const toolUseDuration = (await MetricsServer.getMetrics().tool.toolUseDuration.get()).values;
38
-
39
- const activeCommunityToolIds = settings.tools ?? [];
40
-
41
- const communityTools = await collections.tools
42
- .find({ _id: { $in: activeCommunityToolIds.map((el) => new ObjectId(el)) } })
43
- .toArray()
44
- .then((tools) =>
45
- tools.map((tool) => ({
46
- ...tool,
47
- isHidden: false,
48
- isOnByDefault: true,
49
- isLocked: true,
50
- }))
51
- );
52
-
53
- const fullTools = [...communityTools, ...toolFromConfigs];
54
-
55
- return fullTools
56
- .filter((tool) => !tool.isHidden)
57
- .map(
58
- (tool) =>
59
- ({
60
- _id: tool._id.toString(),
61
- type: tool.type,
62
- displayName: tool.displayName,
63
- name: tool.name,
64
- description: tool.description,
65
- mimeTypes: (tool.inputs ?? [])
66
- .filter((input): input is ToolInputFile => input.type === "file")
67
- .map((input) => (input as ToolInputFile).mimeTypes)
68
- .flat(),
69
- isOnByDefault: tool.isOnByDefault ?? true,
70
- isLocked: tool.isLocked ?? true,
71
- timeToUseMS:
72
- toolUseDuration.find(
73
- (el) => el.labels.tool === tool._id.toString() && el.labels.quantile === 0.9
74
- )?.value ?? 15_000,
75
- color: tool.color,
76
- icon: tool.icon,
77
- }) satisfies ToolFront
78
- );
79
- })
80
- .get(
81
- "/search",
82
- async ({ query, locals, error }) => {
83
- if (config.COMMUNITY_TOOLS !== "true") {
84
- error(403, "Community tools are not enabled");
85
- }
86
-
87
- const username = query.user;
88
- const search = query.q?.trim() ?? null;
89
-
90
- const pageIndex = query.p ?? 0;
91
- const sort = query.sort ?? SortKey.TRENDING;
92
- const createdByCurrentUser = locals.user?.username && locals.user.username === username;
93
- const activeOnly = query.active ?? false;
94
- const showUnfeatured = query.showUnfeatured ?? false;
95
-
96
- let user: Pick<User, "_id"> | null = null;
97
- if (username) {
98
- user = await collections.users.findOne<Pick<User, "_id">>(
99
- { username },
100
- { projection: { _id: 1 } }
101
- );
102
- if (!user) {
103
- error(404, `User "${username}" doesn't exist`);
104
- }
105
- }
106
-
107
- const settings = await collections.settings.findOne(authCondition(locals));
108
-
109
- if (!settings && activeOnly) {
110
- error(404, "No user settings found");
111
- }
112
-
113
- const queryTokens = !!search && generateQueryTokens(search);
114
-
115
- const filter: Filter<CommunityToolDB> = {
116
- ...(!createdByCurrentUser &&
117
- !activeOnly &&
118
- !(locals.isAdmin && showUnfeatured) && { review: ReviewStatus.APPROVED }),
119
- ...(user && { createdById: user._id }),
120
- ...(queryTokens && { searchTokens: { $all: queryTokens } }),
121
- ...(activeOnly && {
122
- _id: {
123
- $in: (settings?.tools ?? []).map((key) => {
124
- return new ObjectId(key);
125
- }),
126
- },
127
- }),
128
- };
129
-
130
- const communityTools = await collections.tools
131
- .find(filter)
132
- .skip(NUM_PER_PAGE * pageIndex)
133
- .sort({
134
- ...(sort === SortKey.TRENDING && { last24HoursUseCount: -1 }),
135
- useCount: -1,
136
- })
137
- .limit(NUM_PER_PAGE)
138
- .toArray();
139
-
140
- const configTools = toolFromConfigs
141
- .filter((tool) => !tool?.isHidden)
142
- .filter((tool) => {
143
- if (queryTokens) {
144
- return generateSearchTokens(tool.displayName).some((token) =>
145
- queryTokens.some((queryToken) => queryToken.test(token))
146
- );
147
- }
148
- return true;
149
- });
150
-
151
- const tools = [...(pageIndex == 0 && !username ? configTools : []), ...communityTools];
152
-
153
- const numTotalItems =
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({
167
- user: t.Optional(t.String()),
168
- q: t.Optional(t.String()),
169
- sort: t.Optional(t.Enum(SortKey)),
170
- p: t.Optional(t.Numeric()),
171
- showUnfeatured: t.Optional(t.Boolean()),
172
- active: t.Optional(t.Boolean()),
173
- }),
174
- }
175
- )
176
- .get("/count", () => {
177
- // return community tool count
178
- return collections.tools.countDocuments({ type: "community", review: ReviewStatus.APPROVED });
179
- })
180
- .group("/:id", (app) => {
181
- return app
182
- .derive(async ({ params, error, locals }) => {
183
- const tool = await collections.tools.findOne({ _id: new ObjectId(params.id) });
184
-
185
- if (!tool) {
186
- const tool = toolFromConfigs.find((el) => el._id.toString() === params.id);
187
- if (!tool) {
188
- throw error(404, "Tool not found");
189
- } else {
190
- return {
191
- tool: {
192
- ...tool,
193
- _id: tool._id.toString(),
194
- call: undefined,
195
- createdById: null,
196
- createdByName: null,
197
- createdByMe: false,
198
- reported: false,
199
- review: ReviewStatus.APPROVED,
200
- },
201
- };
202
- }
203
- } else {
204
- const reported = await collections.reports.findOne({
205
- contentId: tool._id,
206
- object: "tool",
207
- });
208
-
209
- return {
210
- tool: {
211
- ...tool,
212
- _id: tool._id.toString(),
213
- call: undefined,
214
- createdById: tool.createdById.toString(),
215
- createdByMe:
216
- tool.createdById.toString() === (locals.user?._id ?? locals.sessionId).toString(),
217
- reported: !!reported,
218
- },
219
- };
220
- }
221
- })
222
- .get("", ({ tool }) => {
223
- return tool;
224
- })
225
- .post("/", () => {
226
- // todo: post new tool
227
- throw new Error("Not implemented");
228
- })
229
- .group("/:toolId", (app) => {
230
- return app
231
- .get("/", () => {
232
- // todo: get tool
233
- throw new Error("Not implemented");
234
- })
235
- .patch("/", () => {
236
- // todo: patch tool
237
- throw new Error("Not implemented");
238
- })
239
- .delete("/", () => {
240
- // todo: delete tool
241
- throw new Error("Not implemented");
242
- })
243
- .post("/report", () => {
244
- // todo: report tool
245
- throw new Error("Not implemented");
246
- })
247
- .patch("/review", () => {
248
- // todo: review tool
249
- throw new Error("Not implemented");
250
- });
251
- });
252
- });
253
- });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
src/lib/server/api/routes/groups/user.ts DELETED
@@ -1,197 +0,0 @@
1
- import { Elysia } from "elysia";
2
- import { authPlugin } from "$api/authPlugin";
3
- import { defaultModel } from "$lib/server/models";
4
- import { collections } from "$lib/server/database";
5
- import { authCondition } from "$lib/server/auth";
6
- import { models, validateModel } from "$lib/server/models";
7
- 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)
15
- .get("/login", () => {
16
- // todo: login
17
- throw new Error("Not implemented");
18
- })
19
- .get("/login/callback", () => {
20
- // todo: login callback
21
- throw new Error("Not implemented");
22
- })
23
- .post("/logout", () => {
24
- // todo: logout
25
- throw new Error("Not implemented");
26
- })
27
- .group("/user", (app) => {
28
- return app
29
- .get("/", ({ locals }) => {
30
- return locals.user
31
- ? {
32
- id: locals.user._id.toString(),
33
- username: locals.user.username,
34
- avatarUrl: locals.user.avatarUrl,
35
- email: locals.user.email,
36
- logoutDisabled: locals.user.logoutDisabled,
37
- isAdmin: locals.user.isAdmin ?? false,
38
- isEarlyAccess: locals.user.isEarlyAccess ?? false,
39
- }
40
- : null;
41
- })
42
- .get("/settings", async ({ locals }) => {
43
- const settings = await collections.settings.findOne(authCondition(locals));
44
-
45
- if (
46
- settings &&
47
- !validateModel(models).safeParse(settings?.activeModel).success &&
48
- !settings.assistants?.map((el) => el.toString())?.includes(settings?.activeModel)
49
- ) {
50
- settings.activeModel = defaultModel.id;
51
- await collections.settings.updateOne(authCondition(locals), {
52
- $set: { activeModel: defaultModel.id },
53
- });
54
- }
55
-
56
- // if the model is unlisted, set the active model to the default model
57
- if (
58
- settings?.activeModel &&
59
- models.find((m) => m.id === settings?.activeModel)?.unlisted === true
60
- ) {
61
- settings.activeModel = defaultModel.id;
62
- await collections.settings.updateOne(authCondition(locals), {
63
- $set: { activeModel: defaultModel.id },
64
- });
65
- }
66
-
67
- // todo: get user settings
68
- return {
69
- ethicsModalAccepted: !!settings?.ethicsModalAcceptedAt,
70
- ethicsModalAcceptedAt: settings?.ethicsModalAcceptedAt ?? null,
71
-
72
- activeModel: settings?.activeModel ?? DEFAULT_SETTINGS.activeModel,
73
- hideEmojiOnSidebar: settings?.hideEmojiOnSidebar ?? DEFAULT_SETTINGS.hideEmojiOnSidebar,
74
- disableStream: settings?.disableStream ?? DEFAULT_SETTINGS.disableStream,
75
- directPaste: settings?.directPaste ?? DEFAULT_SETTINGS.directPaste,
76
- shareConversationsWithModelAuthors:
77
- settings?.shareConversationsWithModelAuthors ??
78
- DEFAULT_SETTINGS.shareConversationsWithModelAuthors,
79
-
80
- customPrompts: settings?.customPrompts ?? {},
81
- assistants: settings?.assistants?.map((assistantId) => assistantId.toString()) ?? [],
82
- tools:
83
- settings?.tools ??
84
- toolFromConfigs
85
- .filter((el) => !el.isHidden && el.isOnByDefault)
86
- .map((el) => el._id.toString()),
87
- };
88
- })
89
- .post("/settings", async ({ locals, request }) => {
90
- const body = await request.json();
91
-
92
- const { ethicsModalAccepted, ...settings } = z
93
- .object({
94
- shareConversationsWithModelAuthors: z
95
- .boolean()
96
- .default(DEFAULT_SETTINGS.shareConversationsWithModelAuthors),
97
- hideEmojiOnSidebar: z.boolean().default(DEFAULT_SETTINGS.hideEmojiOnSidebar),
98
- ethicsModalAccepted: z.boolean().optional(),
99
- activeModel: z.string().default(DEFAULT_SETTINGS.activeModel),
100
- customPrompts: z.record(z.string()).default({}),
101
- tools: z.array(z.string()).optional(),
102
- disableStream: z.boolean().default(false),
103
- directPaste: z.boolean().default(false),
104
- })
105
- .parse(body) satisfies SettingsEditable;
106
-
107
- // make sure all tools exist
108
- // either in db or in config
109
- if (settings.tools) {
110
- const newTools = [
111
- ...(await collections.tools
112
- .find({ _id: { $in: settings.tools.map((toolId) => new ObjectId(toolId)) } })
113
- .project({ _id: 1 })
114
- .toArray()
115
- .then((tools) => tools.map((tool) => tool._id.toString()))),
116
- ...toolFromConfigs
117
- .filter((el) => (settings?.tools ?? []).includes(el._id.toString()))
118
- .map((el) => el._id.toString()),
119
- ];
120
-
121
- settings.tools = newTools;
122
- }
123
-
124
- await collections.settings.updateOne(
125
- authCondition(locals),
126
- {
127
- $set: {
128
- ...settings,
129
- ...(ethicsModalAccepted && { ethicsModalAcceptedAt: new Date() }),
130
- updatedAt: new Date(),
131
- },
132
- $setOnInsert: {
133
- createdAt: new Date(),
134
- },
135
- },
136
- {
137
- upsert: true,
138
- }
139
- );
140
- // return ok response
141
- return new Response();
142
- })
143
- .get("/reports", async ({ locals }) => {
144
- if (!locals.user || !locals.sessionId) {
145
- return [];
146
- }
147
-
148
- const reports = await collections.reports
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 }) => {
157
- const settings = await collections.settings.findOne(authCondition(locals));
158
-
159
- if (!settings) {
160
- return null;
161
- }
162
-
163
- if (settings.assistants?.map((el) => el.toString())?.includes(settings?.activeModel)) {
164
- return await collections.assistants.findOne({
165
- _id: new ObjectId(settings.activeModel),
166
- });
167
- }
168
-
169
- return null;
170
- })
171
- .get("/assistants", async ({ locals }) => {
172
- const settings = await collections.settings.findOne(authCondition(locals));
173
-
174
- if (!settings) {
175
- return [];
176
- }
177
-
178
- const userAssistants =
179
- settings?.assistants?.map((assistantId) => assistantId.toString()) ?? [];
180
-
181
- const assistants = await collections.assistants
182
- .find({
183
- _id: {
184
- $in: [...userAssistants.map((el) => new ObjectId(el))],
185
- },
186
- })
187
- .toArray();
188
-
189
- return assistants.map((el) => ({
190
- ...el,
191
- _id: el._id.toString(),
192
- createdById: undefined,
193
- createdByMe:
194
- el.createdById.toString() === (locals.user?._id ?? locals.sessionId).toString(),
195
- }));
196
- });
197
- });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
src/lib/server/auth.ts CHANGED
@@ -14,9 +14,6 @@ import type { Cookies } from "@sveltejs/kit";
14
  import { collections } from "$lib/server/database";
15
  import JSON5 from "json5";
16
  import { logger } from "$lib/server/logger";
17
- import { ObjectId } from "mongodb";
18
- import type { Cookie } from "elysia";
19
- import { adminTokenManager } from "./adminToken";
20
 
21
  export interface OIDCSettings {
22
  redirectURI: string;
@@ -82,10 +79,6 @@ export async function findUser(sessionId: string) {
82
  return await collections.users.findOne({ _id: session.userId });
83
  }
84
  export const authCondition = (locals: App.Locals) => {
85
- if (!locals.user && !locals.sessionId) {
86
- throw new Error("User or sessionId is required");
87
- }
88
-
89
  return locals.user
90
  ? { userId: locals.user._id }
91
  : { sessionId: locals.sessionId, userId: { $exists: false } };
@@ -172,7 +165,6 @@ export async function validateAndParseCsrfToken(
172
  signature: z.string().length(64),
173
  })
174
  .parse(JSON.parse(token));
175
-
176
  const reconstructSign = await sha256(JSON.stringify(data) + "##" + sessionId);
177
 
178
  if (data.expiration > Date.now() && signature === reconstructSign) {
@@ -183,131 +175,3 @@ export async function validateAndParseCsrfToken(
183
  }
184
  return null;
185
  }
186
-
187
- type CookieRecord =
188
- | { type: "elysia"; value: Record<string, Cookie<string | undefined>> }
189
- | { type: "svelte"; value: Cookies };
190
- type HeaderRecord =
191
- | { type: "elysia"; value: Record<string, string | undefined> }
192
- | { type: "svelte"; value: Headers };
193
-
194
- export async function authenticateRequest(
195
- headers: HeaderRecord,
196
- cookie: CookieRecord,
197
- isApi?: boolean
198
- ): Promise<App.Locals & { secretSessionId: string }> {
199
- // once the entire API has been moved to elysia
200
- // we can move this function to authPlugin.ts
201
- // and get rid of the isApi && type: "svelte" options
202
- const token =
203
- cookie.type === "elysia"
204
- ? cookie.value[config.COOKIE_NAME].value
205
- : cookie.value.get(config.COOKIE_NAME);
206
-
207
- let email = null;
208
- if (config.TRUSTED_EMAIL_HEADER) {
209
- if (headers.type === "elysia") {
210
- email = headers.value[config.TRUSTED_EMAIL_HEADER];
211
- } else {
212
- email = headers.value.get(config.TRUSTED_EMAIL_HEADER);
213
- }
214
- }
215
-
216
- let secretSessionId: string | null = null;
217
- let sessionId: string | null = null;
218
-
219
- if (email) {
220
- secretSessionId = sessionId = await sha256(email);
221
- return {
222
- user: {
223
- _id: new ObjectId(sessionId.slice(0, 24)),
224
- name: email,
225
- email,
226
- createdAt: new Date(),
227
- updatedAt: new Date(),
228
- hfUserId: email,
229
- avatarUrl: "",
230
- logoutDisabled: true,
231
- },
232
- sessionId,
233
- secretSessionId,
234
- isAdmin: adminTokenManager.isAdmin(sessionId),
235
- };
236
- }
237
-
238
- if (token) {
239
- secretSessionId = token;
240
- sessionId = await sha256(token);
241
- const user = await findUser(sessionId);
242
- return {
243
- user: user ?? undefined,
244
- sessionId,
245
- secretSessionId,
246
- isAdmin: user?.isAdmin || adminTokenManager.isAdmin(sessionId),
247
- };
248
- }
249
-
250
- if (isApi) {
251
- const authorization =
252
- headers.type === "elysia"
253
- ? headers.value["Authorization"]
254
- : headers.value.get("Authorization");
255
- if (authorization?.startsWith("Bearer ")) {
256
- const token = authorization.slice(7);
257
- const hash = await sha256(token);
258
- sessionId = secretSessionId = hash;
259
-
260
- const cacheHit = await collections.tokenCaches.findOne({ tokenHash: hash });
261
- if (cacheHit) {
262
- const user = await collections.users.findOne({ hfUserId: cacheHit.userId });
263
- if (!user) {
264
- throw new Error("User not found");
265
- }
266
- return {
267
- user,
268
- sessionId,
269
- secretSessionId,
270
- isAdmin: user.isAdmin || adminTokenManager.isAdmin(sessionId),
271
- };
272
- }
273
-
274
- const response = await fetch("https://huggingface.co/api/whoami-v2", {
275
- headers: { Authorization: `Bearer ${token}` },
276
- });
277
-
278
- if (!response.ok) {
279
- throw new Error("Unauthorized");
280
- }
281
-
282
- const data = await response.json();
283
- const user = await collections.users.findOne({ hfUserId: data.id });
284
- if (!user) {
285
- throw new Error("User not found");
286
- }
287
-
288
- await collections.tokenCaches.insertOne({
289
- tokenHash: hash,
290
- userId: data.id,
291
- createdAt: new Date(),
292
- updatedAt: new Date(),
293
- });
294
-
295
- return {
296
- user,
297
- sessionId,
298
- secretSessionId,
299
- isAdmin: user.isAdmin || adminTokenManager.isAdmin(sessionId),
300
- };
301
- }
302
- }
303
-
304
- // Generate new session if none exists
305
- secretSessionId = crypto.randomUUID();
306
- sessionId = await sha256(secretSessionId);
307
-
308
- if (await collections.sessions.findOne({ sessionId })) {
309
- throw new Error("Session ID collision");
310
- }
311
-
312
- return { user: undefined, sessionId, secretSessionId, isAdmin: false };
313
- }
 
14
  import { collections } from "$lib/server/database";
15
  import JSON5 from "json5";
16
  import { logger } from "$lib/server/logger";
 
 
 
17
 
18
  export interface OIDCSettings {
19
  redirectURI: string;
 
79
  return await collections.users.findOne({ _id: session.userId });
80
  }
81
  export const authCondition = (locals: App.Locals) => {
 
 
 
 
82
  return locals.user
83
  ? { userId: locals.user._id }
84
  : { sessionId: locals.sessionId, userId: { $exists: false } };
 
165
  signature: z.string().length(64),
166
  })
167
  .parse(JSON.parse(token));
 
168
  const reconstructSign = await sha256(JSON.stringify(data) + "##" + sessionId);
169
 
170
  if (data.expiration > Date.now() && signature === reconstructSign) {
 
175
  }
176
  return null;
177
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
src/lib/server/config.ts CHANGED
@@ -1,6 +1,6 @@
1
  import { env as publicEnv } from "$env/dynamic/public";
2
  import { env as serverEnv } from "$env/dynamic/private";
3
- import { serverPublicConfig } from "$lib/utils/PublicConfig.svelte";
4
  import { building } from "$app/environment";
5
  import type { Collection } from "mongodb";
6
  import type { ConfigKey as ConfigKeyType } from "$lib/types/ConfigKey";
@@ -152,7 +152,7 @@ const configManager = new ConfigManager();
152
  export const ready = (async () => {
153
  if (!building) {
154
  await configManager.init().then(() => {
155
- serverPublicConfig.init(configManager.getPublicConfig());
156
  });
157
  }
158
  })();
 
1
  import { env as publicEnv } from "$env/dynamic/public";
2
  import { env as serverEnv } from "$env/dynamic/private";
3
+ import { publicConfig } from "$lib/utils/PublicConfig.svelte";
4
  import { building } from "$app/environment";
5
  import type { Collection } from "mongodb";
6
  import type { ConfigKey as ConfigKeyType } from "$lib/types/ConfigKey";
 
152
  export const ready = (async () => {
153
  if (!building) {
154
  await configManager.init().then(() => {
155
+ publicConfig.init(configManager.getPublicConfig());
156
  });
157
  }
158
  })();
src/lib/server/isURLLocal.ts CHANGED
@@ -1,6 +1,5 @@
1
  import { Address6, Address4 } from "ip-address";
2
  import dns from "node:dns";
3
- import { isIP } from "node:net";
4
 
5
  const dnsLookup = (hostname: string): Promise<{ address: string; family: number }> => {
6
  return new Promise((resolve, reject) => {
@@ -37,12 +36,3 @@ export function isURLStringLocal(url: string) {
37
  return true;
38
  }
39
  }
40
-
41
- export function isHostLocalhost(host: string): boolean {
42
- if (host === "localhost") return true;
43
- if (host === "::1" || host === "[::1]") return true;
44
- if (host.startsWith("127.") && isIP(host)) return true;
45
- if (host.endsWith(".localhost")) return true;
46
-
47
- return false;
48
- }
 
1
  import { Address6, Address4 } from "ip-address";
2
  import dns from "node:dns";
 
3
 
4
  const dnsLookup = (hostname: string): Promise<{ address: string; family: number }> => {
5
  return new Promise((resolve, reject) => {
 
36
  return true;
37
  }
38
  }
 
 
 
 
 
 
 
 
 
src/lib/server/models.ts CHANGED
@@ -13,7 +13,6 @@ import JSON5 from "json5";
13
  import { getTokenizer } from "$lib/utils/getTokenizer";
14
  import { logger } from "$lib/server/logger";
15
  import { type ToolInput } from "$lib/types/Tool";
16
- import { fetchJSON } from "$lib/utils/fetchJSON";
17
  import { join, dirname } from "path";
18
  import { fileURLToPath } from "url";
19
  import { findRepoRoot } from "./findRepoRoot";
@@ -347,12 +346,13 @@ const addEndpoint = (m: Awaited<ReturnType<typeof processModel>>) => ({
347
  });
348
 
349
  const inferenceApiIds = config.isHuggingChat
350
- ? await fetchJSON<{ id: string }[]>(
351
  "https://huggingface.co/api/models?pipeline_tag=text-generation&inference=warm&filter=conversational"
352
  )
353
- .then((arr) => arr?.map((r) => r.id) || [])
354
- .catch(() => {
355
- logger.error("Failed to fetch inference API ids");
 
356
  return [];
357
  })
358
  : [];
 
13
  import { getTokenizer } from "$lib/utils/getTokenizer";
14
  import { logger } from "$lib/server/logger";
15
  import { type ToolInput } from "$lib/types/Tool";
 
16
  import { join, dirname } from "path";
17
  import { fileURLToPath } from "url";
18
  import { findRepoRoot } from "./findRepoRoot";
 
346
  });
347
 
348
  const inferenceApiIds = config.isHuggingChat
349
+ ? await fetch(
350
  "https://huggingface.co/api/models?pipeline_tag=text-generation&inference=warm&filter=conversational"
351
  )
352
+ .then((r) => r.json())
353
+ .then((json) => json.map((r: { id: string }) => r.id))
354
+ .catch((err) => {
355
+ logger.error(err, "Failed to fetch inference API ids");
356
  return [];
357
  })
358
  : [];
src/lib/types/ConvSidebar.ts CHANGED
@@ -4,5 +4,5 @@ export interface ConvSidebar {
4
  updatedAt: Date;
5
  model?: string;
6
  assistantId?: string;
7
- avatarUrl?: string | Promise<string | undefined>;
8
  }
 
4
  updatedAt: Date;
5
  model?: string;
6
  assistantId?: string;
7
+ avatarUrl?: string;
8
  }
src/lib/types/UrlDependency.ts CHANGED
@@ -1,5 +1,5 @@
1
  /* eslint-disable no-shadow */
2
  export enum UrlDependency {
3
  ConversationList = "conversation:list",
4
- Conversation = "conversation:id",
5
  }
 
1
  /* eslint-disable no-shadow */
2
  export enum UrlDependency {
3
  ConversationList = "conversation:list",
4
+ Conversation = "conversation",
5
  }
src/lib/utils/PublicConfig.svelte.ts CHANGED
@@ -1,21 +1,12 @@
1
  import type { env as publicEnv } from "$env/dynamic/public";
2
- import { page } from "$app/state";
3
- import { base } from "$app/paths";
4
-
5
- import type { Transporter } from "@sveltejs/kit";
6
- import { getContext } from "svelte";
7
 
8
  type PublicConfigKey = keyof typeof publicEnv;
9
 
10
  class PublicConfigManager {
11
  #configStore = $state<Record<PublicConfigKey, string>>({});
12
 
13
- constructor(initialConfig?: Record<PublicConfigKey, string>) {
14
  this.init = this.init.bind(this);
15
- this.getPublicConfig = this.getPublicConfig.bind(this);
16
- if (initialConfig) {
17
- this.init(initialConfig);
18
- }
19
  }
20
 
21
  init(publicConfig: Record<PublicConfigKey, string>) {
@@ -26,53 +17,29 @@ class PublicConfigManager {
26
  return this.#configStore[key];
27
  }
28
 
29
- getPublicConfig() {
30
- return this.#configStore;
31
- }
32
-
33
  get isHuggingChat() {
34
  return this.#configStore.PUBLIC_APP_ASSETS === "huggingchat";
35
  }
36
-
37
- get assetPath() {
38
- return (
39
- (this.#configStore.PUBLIC_ORIGIN || page.url.origin) +
40
- base +
41
- "/" +
42
- this.#configStore.PUBLIC_APP_ASSETS
43
- );
44
- }
45
  }
46
- type ConfigProxy = PublicConfigManager & { [K in PublicConfigKey]: string };
47
 
48
- export function getConfigManager(initialConfig?: Record<PublicConfigKey, string>) {
49
- const publicConfigManager = new PublicConfigManager(initialConfig);
50
 
51
- const publicConfig: ConfigProxy = new Proxy(publicConfigManager, {
52
- get(target, prop) {
53
- if (prop in target) {
54
- return Reflect.get(target, prop);
55
- }
56
- if (typeof prop === "string") {
57
- return target.get(prop as PublicConfigKey);
58
- }
59
- return undefined;
60
- },
61
- set(target, prop, value, receiver) {
62
- if (prop in target) {
63
- return Reflect.set(target, prop, value, receiver);
64
- }
65
- return false;
66
- },
67
- }) as ConfigProxy;
68
- return publicConfig;
69
- }
70
-
71
- export const publicConfigTransporter: Transporter = {
72
- encode: (value) =>
73
- value instanceof PublicConfigManager ? JSON.stringify(value.getPublicConfig()) : false,
74
- decode: (value) => getConfigManager(JSON.parse(value)),
75
- };
76
 
77
- export const serverPublicConfig = getConfigManager();
78
- export const usePublicConfig = () => getContext<ConfigProxy>("publicConfig");
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  import type { env as publicEnv } from "$env/dynamic/public";
 
 
 
 
 
2
 
3
  type PublicConfigKey = keyof typeof publicEnv;
4
 
5
  class PublicConfigManager {
6
  #configStore = $state<Record<PublicConfigKey, string>>({});
7
 
8
+ constructor() {
9
  this.init = this.init.bind(this);
 
 
 
 
10
  }
11
 
12
  init(publicConfig: Record<PublicConfigKey, string>) {
 
17
  return this.#configStore[key];
18
  }
19
 
 
 
 
 
20
  get isHuggingChat() {
21
  return this.#configStore.PUBLIC_APP_ASSETS === "huggingchat";
22
  }
 
 
 
 
 
 
 
 
 
23
  }
 
24
 
25
+ const publicConfigManager = new PublicConfigManager();
 
26
 
27
+ type ConfigProxy = PublicConfigManager & { [K in PublicConfigKey]: string };
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
28
 
29
+ export const publicConfig: ConfigProxy = new Proxy(publicConfigManager, {
30
+ get(target, prop) {
31
+ if (prop in target) {
32
+ return Reflect.get(target, prop);
33
+ }
34
+ if (typeof prop === "string") {
35
+ return target.get(prop as PublicConfigKey);
36
+ }
37
+ return undefined;
38
+ },
39
+ set(target, prop, value, receiver) {
40
+ if (prop in target) {
41
+ return Reflect.set(target, prop, value, receiver);
42
+ }
43
+ return false;
44
+ },
45
+ }) as ConfigProxy;
src/lib/utils/fetchJSON.ts DELETED
@@ -1,25 +0,0 @@
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}`);
13
- }
14
-
15
- // Handle empty responses (which parse to null)
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
- }
23
-
24
- return JSON.parse(text);
25
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
src/lib/utils/getShareUrl.ts CHANGED
@@ -1,9 +1,8 @@
1
  import { base } from "$app/paths";
2
- import { page } from "$app/state";
3
 
4
  export function getShareUrl(url: URL, shareId: string): string {
5
  return `${
6
- page.data.publicConfig.PUBLIC_SHARE_PREFIX ||
7
- `${page.data.publicConfig.PUBLIC_ORIGIN || url.origin}${base}`
8
  }/r/${shareId}`;
9
  }
 
1
  import { base } from "$app/paths";
2
+ import { publicConfig } from "$lib/utils/PublicConfig.svelte";
3
 
4
  export function getShareUrl(url: URL, shareId: string): string {
5
  return `${
6
+ publicConfig.PUBLIC_SHARE_PREFIX || `${publicConfig.PUBLIC_ORIGIN || url.origin}${base}`
 
7
  }/r/${shareId}`;
8
  }
src/lib/utils/messageUpdates.ts CHANGED
@@ -15,8 +15,7 @@ import {
15
  type MessageToolResultUpdate,
16
  } from "$lib/types/MessageUpdate";
17
 
18
- import { page } from "$app/state";
19
-
20
  export const isMessageWebSearchUpdate = (update: MessageUpdate): update is MessageWebSearchUpdate =>
21
  update.type === MessageUpdateType.WebSearch;
22
  export const isMessageWebSearchGeneralUpdate = (
@@ -97,7 +96,7 @@ export async function fetchMessageUpdates(
97
  throw Error("Body not defined");
98
  }
99
 
100
- if (!(page.data.publicConfig.PUBLIC_SMOOTH_UPDATES === "true")) {
101
  return endpointStreamToIterator(response, abortController);
102
  }
103
 
 
15
  type MessageToolResultUpdate,
16
  } from "$lib/types/MessageUpdate";
17
 
18
+ import { publicConfig } from "$lib/utils/PublicConfig.svelte";
 
19
  export const isMessageWebSearchUpdate = (update: MessageUpdate): update is MessageWebSearchUpdate =>
20
  update.type === MessageUpdateType.WebSearch;
21
  export const isMessageWebSearchGeneralUpdate = (
 
96
  throw Error("Body not defined");
97
  }
98
 
99
+ if (!(publicConfig.PUBLIC_SMOOTH_UPDATES === "true")) {
100
  return endpointStreamToIterator(response, abortController);
101
  }
102
 
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/lib/utils/tree/addChildren.ts CHANGED
@@ -1,7 +1,12 @@
 
 
1
  import { v4 } from "uuid";
2
- import type { Tree, TreeId, NewNode, TreeNode } from "./tree";
3
 
4
- export function addChildren<T>(conv: Tree<T>, message: NewNode<T>, parentId?: TreeId): TreeId {
 
 
 
 
5
  // if this is the first message we just push it
6
  if (conv.messages.length === 0) {
7
  const messageId = v4();
@@ -10,7 +15,7 @@ export function addChildren<T>(conv: Tree<T>, message: NewNode<T>, parentId?: Tr
10
  ...message,
11
  ancestors: [],
12
  id: messageId,
13
- } as TreeNode<T>);
14
  return messageId;
15
  }
16
 
@@ -24,7 +29,7 @@ export function addChildren<T>(conv: Tree<T>, message: NewNode<T>, parentId?: Tr
24
  if (!!parentId && parentId !== conv.messages[conv.messages.length - 1].id) {
25
  throw new Error("This is a legacy conversation, you can only append to the last message");
26
  }
27
- conv.messages.push({ ...message, id: messageId } as TreeNode<T>);
28
  return messageId;
29
  }
30
 
@@ -34,7 +39,7 @@ export function addChildren<T>(conv: Tree<T>, message: NewNode<T>, parentId?: Tr
34
  ancestors,
35
  id: messageId,
36
  children: [],
37
- } as TreeNode<T>);
38
 
39
  const parent = conv.messages.find((m) => m.id === parentId);
40
 
 
1
+ import type { Conversation } from "$lib/types/Conversation";
2
+ import type { Message } from "$lib/types/Message";
3
  import { v4 } from "uuid";
 
4
 
5
+ export function addChildren(
6
+ conv: Pick<Conversation, "messages" | "rootMessageId">,
7
+ message: Omit<Message, "id">,
8
+ parentId?: Message["id"]
9
+ ): Message["id"] {
10
  // if this is the first message we just push it
11
  if (conv.messages.length === 0) {
12
  const messageId = v4();
 
15
  ...message,
16
  ancestors: [],
17
  id: messageId,
18
+ });
19
  return messageId;
20
  }
21
 
 
29
  if (!!parentId && parentId !== conv.messages[conv.messages.length - 1].id) {
30
  throw new Error("This is a legacy conversation, you can only append to the last message");
31
  }
32
+ conv.messages.push({ ...message, id: messageId });
33
  return messageId;
34
  }
35
 
 
39
  ancestors,
40
  id: messageId,
41
  children: [],
42
+ });
43
 
44
  const parent = conv.messages.find((m) => m.id === parentId);
45
 
src/lib/utils/tree/addSibling.spec.ts CHANGED
@@ -5,11 +5,10 @@ import { describe, expect, it } from "vitest";
5
  import { insertLegacyConversation, insertSideBranchesConversation } from "./treeHelpers.spec";
6
  import type { Message } from "$lib/types/Message";
7
  import { addSibling } from "./addSibling";
8
- import type { Conversation } from "$lib/types/Conversation";
9
 
10
- const newMessage = {
11
  content: "new message",
12
- from: "user" as const,
13
  };
14
 
15
  Object.freeze(newMessage);
@@ -19,8 +18,8 @@ describe("addSibling", async () => {
19
  const conv = {
20
  _id: new ObjectId(),
21
  rootMessageId: undefined,
22
- messages: [] as Message[],
23
- } satisfies Pick<Conversation, "_id" | "rootMessageId" | "messages">;
24
 
25
  expect(() => addSibling(conv, newMessage, "not-a-real-id-test")).toThrow(
26
  "Cannot add a sibling to an empty conversation"
 
5
  import { insertLegacyConversation, insertSideBranchesConversation } from "./treeHelpers.spec";
6
  import type { Message } from "$lib/types/Message";
7
  import { addSibling } from "./addSibling";
 
8
 
9
+ const newMessage: Omit<Message, "id"> = {
10
  content: "new message",
11
+ from: "user",
12
  };
13
 
14
  Object.freeze(newMessage);
 
18
  const conv = {
19
  _id: new ObjectId(),
20
  rootMessageId: undefined,
21
+ messages: [],
22
+ };
23
 
24
  expect(() => addSibling(conv, newMessage, "not-a-real-id-test")).toThrow(
25
  "Cannot add a sibling to an empty conversation"
src/lib/utils/tree/addSibling.ts CHANGED
@@ -1,7 +1,12 @@
 
 
1
  import { v4 } from "uuid";
2
- import type { Tree, TreeId, NewNode, TreeNode } from "./tree";
3
 
4
- export function addSibling<T>(conv: Tree<T>, message: NewNode<T>, siblingId: TreeId): TreeId {
 
 
 
 
5
  if (conv.messages.length === 0) {
6
  throw new Error("Cannot add a sibling to an empty conversation");
7
  }
@@ -26,7 +31,7 @@ export function addSibling<T>(conv: Tree<T>, message: NewNode<T>, siblingId: Tre
26
  id: messageId,
27
  ancestors: sibling.ancestors,
28
  children: [],
29
- } as TreeNode<T>);
30
 
31
  const nearestAncestorId = sibling.ancestors[sibling.ancestors.length - 1];
32
  const nearestAncestor = conv.messages.find((m) => m.id === nearestAncestorId);
 
1
+ import type { Conversation } from "$lib/types/Conversation";
2
+ import type { Message } from "$lib/types/Message";
3
  import { v4 } from "uuid";
 
4
 
5
+ export function addSibling(
6
+ conv: Pick<Conversation, "messages" | "rootMessageId">,
7
+ message: Omit<Message, "id">,
8
+ siblingId: Message["id"]
9
+ ): Message["id"] {
10
  if (conv.messages.length === 0) {
11
  throw new Error("Cannot add a sibling to an empty conversation");
12
  }
 
31
  id: messageId,
32
  ancestors: sibling.ancestors,
33
  children: [],
34
+ });
35
 
36
  const nearestAncestorId = sibling.ancestors[sibling.ancestors.length - 1];
37
  const nearestAncestor = conv.messages.find((m) => m.id === nearestAncestorId);
src/lib/utils/tree/buildSubtree.ts CHANGED
@@ -1,6 +1,10 @@
1
- import type { Tree, TreeId, TreeNode } from "./tree";
 
2
 
3
- export function buildSubtree<T>(conv: Tree<T>, id: TreeId): TreeNode<T>[] {
 
 
 
4
  if (!conv.rootMessageId) {
5
  if (conv.messages.length === 0) return [];
6
  // legacy conversation slice up to id
 
1
+ import type { Conversation } from "$lib/types/Conversation";
2
+ import type { Message } from "$lib/types/Message";
3
 
4
+ export function buildSubtree(
5
+ conv: Pick<Conversation, "messages" | "rootMessageId">,
6
+ id: Message["id"]
7
+ ): Message[] {
8
  if (!conv.rootMessageId) {
9
  if (conv.messages.length === 0) return [];
10
  // legacy conversation slice up to id
src/lib/utils/tree/tree.d.ts DELETED
@@ -1,14 +0,0 @@
1
- export type TreeId = string;
2
-
3
- export type Tree<T> = {
4
- rootMessageId?: TreeId;
5
- messages: TreeNode<T>[];
6
- };
7
-
8
- export type TreeNode<T> = T & {
9
- id: TreeId;
10
- ancestors?: TreeId[];
11
- children?: TreeId[];
12
- };
13
-
14
- export type NewNode<T> = Omit<TreeNode<T>, "id">;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
src/routes/+layout.server.ts ADDED
@@ -0,0 +1,286 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import type { LayoutServerLoad } from "./$types";
2
+ import { collections } from "$lib/server/database";
3
+ import type { Conversation } from "$lib/types/Conversation";
4
+ import { UrlDependency } from "$lib/types/UrlDependency";
5
+ import { defaultModel, models, oldModels, validateModel } from "$lib/server/models";
6
+ import { authCondition, requiresUser } from "$lib/server/auth";
7
+ import { DEFAULT_SETTINGS } from "$lib/types/Settings";
8
+ import { config } from "$lib/server/config";
9
+ import { ObjectId } from "mongodb";
10
+ import type { ConvSidebar } from "$lib/types/ConvSidebar";
11
+ import { toolFromConfigs } from "$lib/server/tools";
12
+ import { MetricsServer } from "$lib/server/metrics";
13
+ import type { ToolFront, ToolInputFile } from "$lib/types/Tool";
14
+ import { ReviewStatus } from "$lib/types/Review";
15
+ import { base } from "$app/paths";
16
+ export const load: LayoutServerLoad = async ({ locals, depends, fetch }) => {
17
+ depends(UrlDependency.ConversationList);
18
+
19
+ const settings = await collections.settings.findOne(authCondition(locals));
20
+
21
+ // If the active model in settings is not valid, set it to the default model. This can happen if model was disabled.
22
+ if (
23
+ settings &&
24
+ !validateModel(models).safeParse(settings?.activeModel).success &&
25
+ !settings.assistants?.map((el) => el.toString())?.includes(settings?.activeModel)
26
+ ) {
27
+ settings.activeModel = defaultModel.id;
28
+ await collections.settings.updateOne(authCondition(locals), {
29
+ $set: { activeModel: defaultModel.id },
30
+ });
31
+ }
32
+
33
+ // if the model is unlisted, set the active model to the default model
34
+ if (
35
+ settings?.activeModel &&
36
+ models.find((m) => m.id === settings?.activeModel)?.unlisted === true
37
+ ) {
38
+ settings.activeModel = defaultModel.id;
39
+ await collections.settings.updateOne(authCondition(locals), {
40
+ $set: { activeModel: defaultModel.id },
41
+ });
42
+ }
43
+
44
+ const enableAssistants = config.ENABLE_ASSISTANTS === "true";
45
+
46
+ const assistantActive = !models.map(({ id }) => id).includes(settings?.activeModel ?? "");
47
+
48
+ const assistant = assistantActive
49
+ ? await collections.assistants.findOne({
50
+ _id: new ObjectId(settings?.activeModel),
51
+ })
52
+ : null;
53
+
54
+ const nConversations = await collections.conversations.countDocuments(authCondition(locals));
55
+
56
+ const conversations =
57
+ nConversations === 0
58
+ ? Promise.resolve([])
59
+ : fetch(`${base}/api/conversations`)
60
+ .then((res) => res.json())
61
+ .then(
62
+ (
63
+ convs: Pick<Conversation, "_id" | "title" | "updatedAt" | "model" | "assistantId">[]
64
+ ) =>
65
+ convs.map((conv) => ({
66
+ ...conv,
67
+ updatedAt: new Date(conv.updatedAt),
68
+ }))
69
+ );
70
+
71
+ const userAssistants = settings?.assistants?.map((assistantId) => assistantId.toString()) ?? [];
72
+ const userAssistantsSet = new Set(userAssistants);
73
+
74
+ const assistants = conversations.then((conversations) =>
75
+ collections.assistants
76
+ .find({
77
+ _id: {
78
+ $in: [
79
+ ...userAssistants.map((el) => new ObjectId(el)),
80
+ ...(conversations.map((conv) => conv.assistantId).filter((el) => !!el) as ObjectId[]),
81
+ ],
82
+ },
83
+ })
84
+ .toArray()
85
+ );
86
+
87
+ const messagesBeforeLogin = config.MESSAGES_BEFORE_LOGIN
88
+ ? parseInt(config.MESSAGES_BEFORE_LOGIN)
89
+ : 0;
90
+
91
+ let loginRequired = false;
92
+
93
+ if (requiresUser && !locals.user) {
94
+ if (messagesBeforeLogin === 0) {
95
+ loginRequired = true;
96
+ } else if (nConversations >= messagesBeforeLogin) {
97
+ loginRequired = true;
98
+ } else {
99
+ // get the number of messages where `from === "assistant"` across all conversations.
100
+ const totalMessages =
101
+ (
102
+ await collections.conversations
103
+ .aggregate([
104
+ { $match: { ...authCondition(locals), "messages.from": "assistant" } },
105
+ { $project: { messages: 1 } },
106
+ { $limit: messagesBeforeLogin + 1 },
107
+ { $unwind: "$messages" },
108
+ { $match: { "messages.from": "assistant" } },
109
+ { $count: "messages" },
110
+ ])
111
+ .toArray()
112
+ )[0]?.messages ?? 0;
113
+
114
+ loginRequired = totalMessages >= messagesBeforeLogin;
115
+ }
116
+ }
117
+
118
+ const toolUseDuration = (await MetricsServer.getMetrics().tool.toolUseDuration.get()).values;
119
+
120
+ const configToolIds = toolFromConfigs.map((el) => el._id.toString());
121
+
122
+ let activeCommunityToolIds = (settings?.tools ?? []).filter(
123
+ (key) => !configToolIds.includes(key)
124
+ );
125
+
126
+ if (assistant) {
127
+ activeCommunityToolIds = [...activeCommunityToolIds, ...(assistant.tools ?? [])];
128
+ }
129
+
130
+ const communityTools = await collections.tools
131
+ .find({ _id: { $in: activeCommunityToolIds.map((el) => new ObjectId(el)) } })
132
+ .toArray()
133
+ .then((tools) =>
134
+ tools.map((tool) => ({
135
+ ...tool,
136
+ isHidden: false,
137
+ isOnByDefault: true,
138
+ isLocked: true,
139
+ }))
140
+ );
141
+
142
+ return {
143
+ nConversations,
144
+ conversations: await conversations.then(
145
+ async (convs) =>
146
+ await Promise.all(
147
+ convs.map(async (conv) => {
148
+ if (settings?.hideEmojiOnSidebar) {
149
+ conv.title = conv.title.replace(/\p{Emoji}/gu, "");
150
+ }
151
+
152
+ // remove invalid unicode and trim whitespaces
153
+ conv.title = conv.title.replace(/\uFFFD/gu, "").trimStart();
154
+
155
+ let avatarUrl: string | undefined = undefined;
156
+
157
+ if (conv.assistantId) {
158
+ const hash = (
159
+ await collections.assistants.findOne({
160
+ _id: new ObjectId(conv.assistantId),
161
+ })
162
+ )?.avatar;
163
+ if (hash) {
164
+ avatarUrl = `/settings/assistants/${conv.assistantId}/avatar.jpg?hash=${hash}`;
165
+ }
166
+ }
167
+
168
+ return {
169
+ id: conv._id.toString(),
170
+ title: conv.title,
171
+ model: conv.model ?? defaultModel,
172
+ updatedAt: conv.updatedAt,
173
+ assistantId: conv.assistantId?.toString(),
174
+ avatarUrl,
175
+ } satisfies ConvSidebar;
176
+ })
177
+ )
178
+ ),
179
+ settings: {
180
+ searchEnabled: !!(
181
+ config.SERPAPI_KEY ||
182
+ config.SERPER_API_KEY ||
183
+ config.SERPSTACK_API_KEY ||
184
+ config.SEARCHAPI_KEY ||
185
+ config.YDC_API_KEY ||
186
+ config.USE_LOCAL_WEBSEARCH ||
187
+ config.SEARXNG_QUERY_URL ||
188
+ config.BING_SUBSCRIPTION_KEY
189
+ ),
190
+ ethicsModalAccepted: !!settings?.ethicsModalAcceptedAt,
191
+ ethicsModalAcceptedAt: settings?.ethicsModalAcceptedAt ?? null,
192
+ activeModel: settings?.activeModel ?? DEFAULT_SETTINGS.activeModel,
193
+ hideEmojiOnSidebar: settings?.hideEmojiOnSidebar ?? false,
194
+ shareConversationsWithModelAuthors:
195
+ settings?.shareConversationsWithModelAuthors ??
196
+ DEFAULT_SETTINGS.shareConversationsWithModelAuthors,
197
+ customPrompts: settings?.customPrompts ?? {},
198
+ assistants: userAssistants,
199
+ tools:
200
+ settings?.tools ??
201
+ toolFromConfigs
202
+ .filter((el) => !el.isHidden && el.isOnByDefault)
203
+ .map((el) => el._id.toString()),
204
+ disableStream: settings?.disableStream ?? DEFAULT_SETTINGS.disableStream,
205
+ directPaste: settings?.directPaste ?? DEFAULT_SETTINGS.directPaste,
206
+ },
207
+ models: models.map((model) => ({
208
+ id: model.id,
209
+ name: model.name,
210
+ websiteUrl: model.websiteUrl,
211
+ modelUrl: model.modelUrl,
212
+ tokenizer: model.tokenizer,
213
+ datasetName: model.datasetName,
214
+ datasetUrl: model.datasetUrl,
215
+ displayName: model.displayName,
216
+ description: model.description,
217
+ reasoning: !!model.reasoning,
218
+ logoUrl: model.logoUrl,
219
+ promptExamples: model.promptExamples,
220
+ parameters: model.parameters,
221
+ preprompt: model.preprompt,
222
+ multimodal: model.multimodal,
223
+ multimodalAcceptedMimetypes: model.multimodalAcceptedMimetypes,
224
+ tools: model.tools,
225
+ unlisted: model.unlisted,
226
+ hasInferenceAPI: model.hasInferenceAPI,
227
+ })),
228
+ oldModels,
229
+ tools: [...toolFromConfigs, ...communityTools]
230
+ .filter((tool) => !tool?.isHidden)
231
+ .map(
232
+ (tool) =>
233
+ ({
234
+ _id: tool._id.toString(),
235
+ type: tool.type,
236
+ displayName: tool.displayName,
237
+ name: tool.name,
238
+ description: tool.description,
239
+ mimeTypes: (tool.inputs ?? [])
240
+ .filter((input): input is ToolInputFile => input.type === "file")
241
+ .map((input) => (input as ToolInputFile).mimeTypes)
242
+ .flat(),
243
+ isOnByDefault: tool.isOnByDefault ?? true,
244
+ isLocked: tool.isLocked ?? true,
245
+ timeToUseMS:
246
+ toolUseDuration.find(
247
+ (el) => el.labels.tool === tool._id.toString() && el.labels.quantile === 0.9
248
+ )?.value ?? 15_000,
249
+ color: tool.color,
250
+ icon: tool.icon,
251
+ }) satisfies ToolFront
252
+ ),
253
+ communityToolCount: await collections.tools.countDocuments({
254
+ type: "community",
255
+ review: ReviewStatus.APPROVED,
256
+ }),
257
+ assistants: assistants.then((assistants) =>
258
+ assistants
259
+ .filter((el) => userAssistantsSet.has(el._id.toString()))
260
+ .map((el) => ({
261
+ ...el,
262
+ _id: el._id.toString(),
263
+ createdById: undefined,
264
+ createdByMe:
265
+ el.createdById.toString() === (locals.user?._id ?? locals.sessionId).toString(),
266
+ }))
267
+ ),
268
+ user: locals.user && {
269
+ id: locals.user._id.toString(),
270
+ username: locals.user.username,
271
+ avatarUrl: locals.user.avatarUrl,
272
+ email: locals.user.email,
273
+ logoutDisabled: locals.user.logoutDisabled,
274
+ isEarlyAccess: locals.user.isEarlyAccess ?? false,
275
+ },
276
+ isAdmin: locals.isAdmin,
277
+ assistant: assistant ? JSON.parse(JSON.stringify(assistant)) : null,
278
+ enableAssistants,
279
+ enableAssistantsRAG: config.ENABLE_ASSISTANTS_RAG === "true",
280
+ enableCommunityTools: config.COMMUNITY_TOOLS === "true",
281
+ loginRequired,
282
+ loginEnabled: requiresUser,
283
+ guestMode: requiresUser && messagesBeforeLogin > 0,
284
+ publicConfig: config.getPublicConfig(),
285
+ };
286
+ };
src/routes/+layout.svelte CHANGED
@@ -6,6 +6,8 @@
6
  import { base } from "$app/paths";
7
  import { page } from "$app/stores";
8
 
 
 
9
  import { error } from "$lib/stores/errors";
10
  import { createSettingsStore } from "$lib/stores/settings";
11
 
@@ -21,14 +23,9 @@
21
  import LoginModal from "$lib/components/LoginModal.svelte";
22
  import OverloadedModal from "$lib/components/OverloadedModal.svelte";
23
  import Search from "$lib/components/chat/Search.svelte";
24
- import { setContext } from "svelte";
25
 
26
  let { data = $bindable(), children } = $props();
27
 
28
- setContext("publicConfig", data.publicConfig);
29
-
30
- const publicConfig = data.publicConfig;
31
-
32
  let conversations = $state(data.conversations);
33
  $effect(() => {
34
  data.conversations && untrack(() => (conversations = data.conversations));
@@ -184,6 +181,14 @@
184
  publicConfig.PUBLIC_APP_DISCLAIMER === "1" &&
185
  !($page.data.shared === true)
186
  );
 
 
 
 
 
 
 
 
187
  </script>
188
 
189
  <svelte:head>
@@ -198,13 +203,35 @@
198
  <meta property="og:title" content={publicConfig.PUBLIC_APP_NAME} />
199
  <meta property="og:type" content="website" />
200
  <meta property="og:url" content="{publicConfig.PUBLIC_ORIGIN || $page.url.origin}{base}" />
201
- <meta property="og:image" content="{publicConfig.assetPath}/thumbnail.png" />
 
 
 
 
202
  <meta property="og:description" content={publicConfig.PUBLIC_APP_DESCRIPTION} />
203
  {/if}
204
- <link rel="icon" href="{publicConfig.assetPath}/favicon.ico" sizes="32x32" />
205
- <link rel="icon" href="{publicConfig.assetPath}/icon.svg" type="image/svg+xml" />
206
- <link rel="apple-touch-icon" href="{publicConfig.assetPath}/apple-touch-icon.png" />
207
- <link rel="manifest" href="{publicConfig.assetPath}/manifest.json" />
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
208
 
209
  {#if publicConfig.PUBLIC_PLAUSIBLE_SCRIPT_URL && publicConfig.PUBLIC_ORIGIN}
210
  <script
@@ -254,7 +281,7 @@
254
  <NavMenu
255
  {conversations}
256
  user={data.user}
257
- canLogin={!data.user && data.loginEnabled}
258
  on:shareConversation={(ev) => shareConversation(ev.detail.id, ev.detail.title)}
259
  on:deleteConversation={(ev) => deleteConversation(ev.detail)}
260
  on:editConversationTitle={(ev) => editConversationTitle(ev.detail.id, ev.detail.title)}
@@ -266,7 +293,7 @@
266
  <NavMenu
267
  {conversations}
268
  user={data.user}
269
- canLogin={!data.user && data.loginEnabled}
270
  on:shareConversation={(ev) => shareConversation(ev.detail.id, ev.detail.title)}
271
  on:deleteConversation={(ev) => deleteConversation(ev.detail)}
272
  on:editConversationTitle={(ev) => editConversationTitle(ev.detail.id, ev.detail.title)}
 
6
  import { base } from "$app/paths";
7
  import { page } from "$app/stores";
8
 
9
+ import { publicConfig } from "$lib/utils/PublicConfig.svelte";
10
+
11
  import { error } from "$lib/stores/errors";
12
  import { createSettingsStore } from "$lib/stores/settings";
13
 
 
23
  import LoginModal from "$lib/components/LoginModal.svelte";
24
  import OverloadedModal from "$lib/components/OverloadedModal.svelte";
25
  import Search from "$lib/components/chat/Search.svelte";
 
26
 
27
  let { data = $bindable(), children } = $props();
28
 
 
 
 
 
29
  let conversations = $state(data.conversations);
30
  $effect(() => {
31
  data.conversations && untrack(() => (conversations = data.conversations));
 
181
  publicConfig.PUBLIC_APP_DISCLAIMER === "1" &&
182
  !($page.data.shared === true)
183
  );
184
+
185
+ $effect.pre(() => {
186
+ publicConfig.init(data.publicConfig);
187
+ });
188
+
189
+ onMount(() => {
190
+ publicConfig.init(data.publicConfig);
191
+ });
192
  </script>
193
 
194
  <svelte:head>
 
203
  <meta property="og:title" content={publicConfig.PUBLIC_APP_NAME} />
204
  <meta property="og:type" content="website" />
205
  <meta property="og:url" content="{publicConfig.PUBLIC_ORIGIN || $page.url.origin}{base}" />
206
+ <meta
207
+ property="og:image"
208
+ content="{publicConfig.PUBLIC_ORIGIN ||
209
+ $page.url.origin}{base}/{publicConfig.PUBLIC_APP_ASSETS}/thumbnail.png"
210
+ />
211
  <meta property="og:description" content={publicConfig.PUBLIC_APP_DESCRIPTION} />
212
  {/if}
213
+ <link
214
+ rel="icon"
215
+ href="{publicConfig.PUBLIC_ORIGIN ||
216
+ $page.url.origin}{base}/{publicConfig.PUBLIC_APP_ASSETS}/favicon.ico"
217
+ sizes="32x32"
218
+ />
219
+ <link
220
+ rel="icon"
221
+ href="{publicConfig.PUBLIC_ORIGIN ||
222
+ $page.url.origin}{base}/{publicConfig.PUBLIC_APP_ASSETS}/icon.svg"
223
+ type="image/svg+xml"
224
+ />
225
+ <link
226
+ rel="apple-touch-icon"
227
+ href="{publicConfig.PUBLIC_ORIGIN ||
228
+ $page.url.origin}{base}/{publicConfig.PUBLIC_APP_ASSETS}/apple-touch-icon.png"
229
+ />
230
+ <link
231
+ rel="manifest"
232
+ href="{publicConfig.PUBLIC_ORIGIN ||
233
+ $page.url.origin}{base}/{publicConfig.PUBLIC_APP_ASSETS}/manifest.json"
234
+ />
235
 
236
  {#if publicConfig.PUBLIC_PLAUSIBLE_SCRIPT_URL && publicConfig.PUBLIC_ORIGIN}
237
  <script
 
281
  <NavMenu
282
  {conversations}
283
  user={data.user}
284
+ canLogin={data.user === undefined && data.loginEnabled}
285
  on:shareConversation={(ev) => shareConversation(ev.detail.id, ev.detail.title)}
286
  on:deleteConversation={(ev) => deleteConversation(ev.detail)}
287
  on:editConversationTitle={(ev) => editConversationTitle(ev.detail.id, ev.detail.title)}
 
293
  <NavMenu
294
  {conversations}
295
  user={data.user}
296
+ canLogin={data.user === undefined && data.loginEnabled}
297
  on:shareConversation={(ev) => shareConversation(ev.detail.id, ev.detail.title)}
298
  on:deleteConversation={(ev) => deleteConversation(ev.detail)}
299
  on:editConversationTitle={(ev) => editConversationTitle(ev.detail.id, ev.detail.title)}
src/routes/+layout.ts DELETED
@@ -1,81 +0,0 @@
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 }) => {
8
- depends(UrlDependency.ConversationList);
9
-
10
- const client = useAPIClient({ fetch });
11
-
12
- const settings = await client.user.settings.get().then(throwOnError);
13
- const models = await client.models.get().then(throwOnError);
14
- const defaultModel = models[0];
15
-
16
- // if the active model is not in the list of models, its probably an assistant
17
- // so we fetch it
18
- const assistantActive = !models.map(({ id }) => id).includes(settings?.activeModel ?? "");
19
-
20
- const assistant = assistantActive
21
- ? await client.assistants({ id: settings?.activeModel }).get().then(throwOnErrorNullable)
22
- : null;
23
-
24
- const { conversations, nConversations } = await client.conversations
25
- .get({ query: { p: 0 } })
26
- .then(throwOnError)
27
- .then(({ conversations, nConversations }) => {
28
- return {
29
- nConversations,
30
- conversations: conversations.map((conv) => {
31
- if (settings?.hideEmojiOnSidebar) {
32
- conv.title = conv.title.replace(/\p{Emoji}/gu, "");
33
- }
34
-
35
- // remove invalid unicode and trim whitespaces
36
- conv.title = conv.title.replace(/\uFFFD/gu, "").trimStart();
37
-
38
- return {
39
- id: conv._id.toString(),
40
- title: conv.title,
41
- model: conv.model ?? defaultModel,
42
- updatedAt: new Date(conv.updatedAt),
43
- ...(conv.assistantId
44
- ? {
45
- assistantId: conv.assistantId.toString(),
46
- avatarUrl: client
47
- .assistants({ id: conv.assistantId.toString() })
48
- .get()
49
- .then(throwOnErrorNullable)
50
- .then((assistant) => {
51
- if (!assistant.avatar) {
52
- return undefined;
53
- }
54
- }),
55
- }
56
- : {}),
57
- } satisfies ConvSidebar;
58
- }),
59
- };
60
- });
61
-
62
- return {
63
- nConversations,
64
- conversations,
65
- assistant: assistant ? jsonSerialize(assistant) : undefined,
66
- assistants: await client.user.assistants.get().then(throwOnError),
67
- models: await client.models.get().then(throwOnError),
68
- oldModels: await client.models.old.get().then(throwOnError),
69
- tools: await client.tools.active.get().then(throwOnError),
70
- communityToolCount: await client.tools.count.get().then(throwOnError),
71
- user: await client.user.get().then(throwOnErrorNullable),
72
- settings: {
73
- ...settings,
74
- ethicsModalAcceptedAt: settings.ethicsModalAcceptedAt
75
- ? new Date(settings.ethicsModalAcceptedAt)
76
- : null,
77
- },
78
- publicConfig: getConfigManager(await client["public-config"].get().then(throwOnError)),
79
- ...(await client["feature-flags"].get().then(throwOnError)),
80
- };
81
- };
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
src/routes/+page.svelte CHANGED
@@ -2,9 +2,7 @@
2
  import { goto } from "$app/navigation";
3
  import { base } from "$app/paths";
4
  import { page } from "$app/state";
5
- import { usePublicConfig } from "$lib/utils/PublicConfig.svelte";
6
-
7
- const publicConfig = usePublicConfig();
8
 
9
  import ChatWindow from "$lib/components/chat/ChatWindow.svelte";
10
  import { ERROR_MESSAGES, error } from "$lib/stores/errors";
@@ -33,8 +31,8 @@
33
  if (validModels.includes($settings.activeModel)) {
34
  model = $settings.activeModel;
35
  } else {
36
- if (data.assistant?.modelId && validModels.includes(data.assistant.modelId)) {
37
- model = data.assistant.modelId;
38
  } else {
39
  model = data.models[0].id;
40
  }
 
2
  import { goto } from "$app/navigation";
3
  import { base } from "$app/paths";
4
  import { page } from "$app/state";
5
+ import { publicConfig } from "$lib/utils/PublicConfig.svelte";
 
 
6
 
7
  import ChatWindow from "$lib/components/chat/ChatWindow.svelte";
8
  import { ERROR_MESSAGES, error } from "$lib/stores/errors";
 
31
  if (validModels.includes($settings.activeModel)) {
32
  model = $settings.activeModel;
33
  } else {
34
+ if (validModels.includes(data.assistant?.modelId)) {
35
+ model = data.assistant?.modelId;
36
  } else {
37
  model = data.models[0].id;
38
  }
src/routes/api/assistant/[id]/subscribe/+server.ts CHANGED
@@ -23,7 +23,6 @@ export async function POST({ params, locals }) {
23
 
24
  const result = await collections.settings.updateOne(authCondition(locals), {
25
  $addToSet: { assistants: assistant._id },
26
- $set: { activeModel: assistant._id.toString() },
27
  });
28
 
29
  // reduce count only if push succeeded
 
23
 
24
  const result = await collections.settings.updateOne(authCondition(locals), {
25
  $addToSet: { assistants: assistant._id },
 
26
  });
27
 
28
  // reduce count only if push succeeded
src/routes/api/v2/[...slugs]/+server.ts DELETED
@@ -1,9 +0,0 @@
1
- import { app } from "$api";
2
-
3
- type RequestHandler = (v: { request: Request; locals: App.Locals }) => Response | Promise<Response>;
4
-
5
- export const GET: RequestHandler = ({ request }) => app.handle(request);
6
- export const POST: RequestHandler = ({ request }) => app.handle(request);
7
- export const PUT: RequestHandler = ({ request }) => app.handle(request);
8
- export const PATCH: RequestHandler = ({ request }) => app.handle(request);
9
- export const DELETE: RequestHandler = ({ request }) => app.handle(request);
 
 
 
 
 
 
 
 
 
 
src/routes/assistant/[assistantId]/+page.server.ts ADDED
@@ -0,0 +1,42 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { base } from "$app/paths";
2
+ import { collections } from "$lib/server/database";
3
+ import { redirect } from "@sveltejs/kit";
4
+ import { ObjectId } from "mongodb";
5
+ import { authCondition } from "$lib/server/auth.js";
6
+
7
+ export async function load({ params, locals }) {
8
+ try {
9
+ const assistant = await collections.assistants.findOne({
10
+ _id: new ObjectId(params.assistantId),
11
+ });
12
+
13
+ if (!assistant) {
14
+ redirect(302, `${base}`);
15
+ }
16
+
17
+ if (locals.user?._id ?? locals.sessionId) {
18
+ await collections.settings.updateOne(
19
+ authCondition(locals),
20
+ {
21
+ $set: {
22
+ activeModel: assistant._id.toString(),
23
+ updatedAt: new Date(),
24
+ },
25
+ $push: { assistants: assistant._id },
26
+ $setOnInsert: {
27
+ createdAt: new Date(),
28
+ },
29
+ },
30
+ {
31
+ upsert: true,
32
+ }
33
+ );
34
+ }
35
+
36
+ return {
37
+ assistant: JSON.parse(JSON.stringify(assistant)),
38
+ };
39
+ } catch {
40
+ redirect(302, `${base}`);
41
+ }
42
+ }
src/routes/assistant/[assistantId]/+page.svelte CHANGED
@@ -3,9 +3,7 @@
3
  import { base } from "$app/paths";
4
  import { goto } from "$app/navigation";
5
  import { onMount } from "svelte";
6
- import { usePublicConfig } from "$lib/utils/PublicConfig.svelte";
7
-
8
- const publicConfig = usePublicConfig();
9
 
10
  import ChatWindow from "$lib/components/chat/ChatWindow.svelte";
11
  import { findCurrentModel } from "$lib/utils/models";
 
3
  import { base } from "$app/paths";
4
  import { goto } from "$app/navigation";
5
  import { onMount } from "svelte";
6
+ import { publicConfig } from "$lib/utils/PublicConfig.svelte";
 
 
7
 
8
  import ChatWindow from "$lib/components/chat/ChatWindow.svelte";
9
  import { findCurrentModel } from "$lib/utils/models";
src/routes/assistant/[assistantId]/+page.ts DELETED
@@ -1,16 +0,0 @@
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
-
15
- return { assistant: await data };
16
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
src/routes/assistants/+page.server.ts ADDED
@@ -0,0 +1,83 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { base } from "$app/paths";
2
+ import { config } from "$lib/server/config";
3
+ import { collections } from "$lib/server/database.js";
4
+ import { SortKey, type Assistant } from "$lib/types/Assistant";
5
+ import type { User } from "$lib/types/User";
6
+ import { generateQueryTokens } from "$lib/utils/searchTokens.js";
7
+ import { error, redirect } from "@sveltejs/kit";
8
+ import type { Filter } from "mongodb";
9
+ import { ReviewStatus } from "$lib/types/Review";
10
+ const NUM_PER_PAGE = 24;
11
+
12
+ export const load = async ({ url, locals }) => {
13
+ if (!config.ENABLE_ASSISTANTS) {
14
+ redirect(302, `${base}/`);
15
+ }
16
+
17
+ const modelId = url.searchParams.get("modelId");
18
+ const pageIndex = parseInt(url.searchParams.get("p") ?? "0");
19
+ const username = url.searchParams.get("user");
20
+ const query = url.searchParams.get("q")?.trim() ?? null;
21
+ const sort = url.searchParams.get("sort")?.trim() ?? SortKey.TRENDING;
22
+ const showUnfeatured = url.searchParams.get("showUnfeatured") === "true";
23
+ const createdByCurrentUser = locals.user?.username && locals.user.username === username;
24
+
25
+ let user: Pick<User, "_id"> | null = null;
26
+ if (username) {
27
+ user = await collections.users.findOne<Pick<User, "_id">>(
28
+ { username },
29
+ { projection: { _id: 1 } }
30
+ );
31
+ if (!user) {
32
+ error(404, `User "${username}" doesn't exist`);
33
+ }
34
+ }
35
+
36
+ // if we require featured assistants, that we are not on a user page and we are not an admin who wants to see unfeatured assistants, we show featured assistants
37
+ let shouldBeFeatured = {};
38
+
39
+ if (config.REQUIRE_FEATURED_ASSISTANTS === "true" && !(locals.isAdmin && showUnfeatured)) {
40
+ if (!user) {
41
+ // only show featured assistants on the community page
42
+ shouldBeFeatured = { review: ReviewStatus.APPROVED };
43
+ } else if (!createdByCurrentUser) {
44
+ // on a user page show assistants that have been approved or are pending
45
+ shouldBeFeatured = { review: { $in: [ReviewStatus.APPROVED, ReviewStatus.PENDING] } };
46
+ }
47
+ }
48
+
49
+ const noSpecificSearch = !user && !query;
50
+ // fetch the top assistants sorted by user count from biggest to smallest.
51
+ // filter by model too if modelId is provided or query if query is provided
52
+ // only show assistants that have been used by more than 5 users if no specific search is made
53
+ const filter: Filter<Assistant> = {
54
+ ...(modelId && { modelId }),
55
+ ...(user && { createdById: user._id }),
56
+ ...(query && { searchTokens: { $all: generateQueryTokens(query) } }),
57
+ ...(noSpecificSearch && { userCount: { $gte: 5 } }),
58
+ ...shouldBeFeatured,
59
+ };
60
+
61
+ const assistants = await collections.assistants
62
+ .find(filter)
63
+ .sort({
64
+ ...(sort === SortKey.TRENDING && { last24HoursCount: -1 }),
65
+ userCount: -1,
66
+ _id: 1,
67
+ })
68
+ .skip(NUM_PER_PAGE * pageIndex)
69
+ .limit(NUM_PER_PAGE)
70
+ .toArray();
71
+
72
+ const numTotalItems = await collections.assistants.countDocuments(filter);
73
+
74
+ return {
75
+ assistants: JSON.parse(JSON.stringify(assistants)) as Array<Assistant>,
76
+ selectedModel: modelId ?? "",
77
+ numTotalItems,
78
+ numItemsPerPage: NUM_PER_PAGE,
79
+ query,
80
+ sort,
81
+ showUnfeatured,
82
+ };
83
+ };