nsarrazin commited on
Commit
612f16b
·
unverified ·
1 Parent(s): 813f132

refactor: new API & universal load functions (pt. 2) (#1847)

Browse files

* feat(API): refactor API with Elysia

* feat: initial elysia setup

* feat: replace conv/[id] load function with universal

* fix: delete v1 catchall

* wip

* fix: response type

* fix: add cors

* feat: more routes in tools & assistants

* refacto: use normal svelte fetch in `/assistant/[assistantId]`

* refacto: use normal svelte fetch in `conversation/[id]`

* feat: use universal load function for `tools/[toolId]`

* wip: removing more server load function stuff

* feat: more routes to universal load functions

* feat: more routes to universal

* refactor: move tools loading to API endpoint

* refactor(api): implement tools search API endpoint and move load function

* fix: types on tool search

* refactor: update assistant route and remove redundant page load function

* refactor(api): move assistants page load function to api call

* refactor(settings): remove waterfall loading

* refactor: main load function

* fix: types

* feat: improve fetchJSON to handle empty responses

* fix: issues with page loading & assistant avatars

* refactor(api): remove unused Eden fetch utility

* refactor(routes): improve conversation page loading and error handling

* feat(api): migrate login and logout to API routes (#1703)

* feat(auth): migrate login and logout to API routes

- Replaced form-based login/logout with fetch-based API routes
- Updated hooks and components to use new `/api/login` and `/api/logout` endpoints

* fix: invalidate on logout

* refactor: move `/api/login` routes back to `/login` and `/api/logout` to `/logout`

remove breaing change to connected apps

* refactor(api): update import aliases and configuration for API routes

* refactor: update conversation handling to use generic tree structure

- Changed `addChildren` and `addSibling` functions to utilize a generic `Tree` type for better flexibility.
- Updated `buildSubtree` to return a tree node structure.
- Modified conversation response types to use `Serialize<Conversation>` for improved serialization.
- Adjusted related tests to align with the new tree structure and types.

* fix: specify message type in ChatWindow component

- Updated the `messages` prop in the ChatWindow component to explicitly cast `messagesPath` as `Message[]` for improved type safety and clarity.

* feat: make login simpler with GET's

* fix: debug logs

* fix: isAdmin flag

* refactor: remove debug route

* fix: use config manager in api routes

* chores: use latest elysia

* wip

* feat: working with different origin

* refactor: update API routes to throw errors for unimplemented features

* fix: use hook for public config

so we dont use context outside of component lifecycles

* refactor: use api client for user reports in settings load function

* fix: deps

* feat: get rid of last fetchJSON call in load function

* cors setup

* feat: use api client side

* feat: use api for assistant loading

* feat: use api client for assistant search

* fix: lint

* feat: use api client for tool

* feat: rename client hook and use for deleting all convs

* fix: let non-authed user set their model

* feat: bump minor for elysia API

* feat: remove unused serverPublicConfig

* feat: add handleFetch to manage cookie forwarding for localhost SSR requests

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 +7 -2
  3. src/hooks.server.ts +68 -110
  4. src/hooks.ts +6 -0
  5. src/lib/APIClient.ts +55 -0
  6. src/lib/components/AssistantSettings.svelte +3 -1
  7. src/lib/components/DisclaimerModal.svelte +15 -15
  8. src/lib/components/LoginModal.svelte +7 -11
  9. src/lib/components/NavConversationItem.svelte +9 -5
  10. src/lib/components/NavMenu.svelte +36 -26
  11. src/lib/components/ToolsMenu.svelte +3 -1
  12. src/lib/components/chat/AssistantIntroduction.svelte +5 -2
  13. src/lib/components/chat/ChatInput.svelte +3 -1
  14. src/lib/components/chat/ChatIntroduction.svelte +3 -2
  15. src/lib/components/chat/ChatWindow.svelte +3 -2
  16. src/lib/components/icons/Logo.svelte +3 -15
  17. src/lib/server/api/authPlugin.ts +25 -0
  18. src/lib/server/api/index.ts +35 -0
  19. src/lib/server/api/routes/groups/assistants.ts +180 -0
  20. src/lib/server/api/routes/groups/conversations.ts +171 -0
  21. src/lib/server/api/routes/groups/misc.ts +77 -0
  22. src/lib/server/api/routes/groups/models.ts +106 -0
  23. src/lib/server/api/routes/groups/tools.ts +253 -0
  24. src/lib/server/api/routes/groups/user.ts +197 -0
  25. src/lib/server/auth.ts +136 -0
  26. src/lib/server/config.ts +1 -4
  27. src/lib/server/isURLLocal.ts +10 -0
  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 +52 -20
  32. src/lib/utils/fetchJSON.ts +25 -0
  33. src/lib/utils/getShareUrl.ts +3 -2
  34. src/lib/utils/messageUpdates.ts +3 -2
  35. src/lib/utils/serialize.ts +13 -0
  36. src/lib/utils/tree/addChildren.ts +5 -10
  37. src/lib/utils/tree/addSibling.spec.ts +5 -4
  38. src/lib/utils/tree/addSibling.ts +3 -8
  39. src/lib/utils/tree/buildSubtree.ts +2 -6
  40. src/lib/utils/tree/tree.d.ts +14 -0
  41. src/routes/+layout.server.ts +0 -286
  42. src/routes/+layout.svelte +12 -39
  43. src/routes/+layout.ts +81 -0
  44. src/routes/+page.svelte +5 -3
  45. src/routes/api/assistant/[id]/subscribe/+server.ts +1 -0
  46. src/routes/api/v2/[...slugs]/+server.ts +9 -0
  47. src/routes/assistant/[assistantId]/+page.server.ts +0 -42
  48. src/routes/assistant/[assistantId]/+page.svelte +3 -1
  49. src/routes/assistant/[assistantId]/+page.ts +16 -0
  50. src/routes/assistants/+page.server.ts +0 -83
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.9.5",
4
  "private": true,
5
  "packageManager": "npm@9.5.0",
6
  "scripts": {
@@ -18,6 +18,9 @@
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,6 +44,7 @@
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,6 +75,7 @@
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,7 +91,7 @@
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",
 
1
  {
2
  "name": "chat-ui",
3
+ "version": "0.10.0",
4
  "private": true,
5
  "packageManager": "npm@9.5.0",
6
  "scripts": {
 
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
  "@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
  "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
  "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",
src/hooks.server.ts CHANGED
@@ -1,21 +1,20 @@
1
  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 { 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,108 +119,13 @@ export const handle: Handle = async ({ event, resolve }) => {
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,12 +158,16 @@ export const handle: Handle = async ({ event, resolve }) => {
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,6 +217,9 @@ export const handle: Handle = async ({ event, resolve }) => {
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,5 +227,52 @@ export const handle: Handle = async ({ event, resolve }) => {
316
  response.headers.append("Content-Security-Policy", "frame-ancestors 'none';");
317
  }
318
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
319
  return response;
320
  };
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  import { config, ready } from "$lib/server/config";
2
+ import type { Handle, HandleServerError, ServerInit, HandleFetch } 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
  }
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
  }
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
 
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
  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
  };
265
+
266
+ export const handleFetch: HandleFetch = async ({ event, request, fetch }) => {
267
+ if (isHostLocalhost(new URL(request.url).hostname)) {
268
+ const cookieHeader = event.request.headers.get("cookie");
269
+ if (cookieHeader) {
270
+ const headers = new Headers(request.headers);
271
+ headers.set("cookie", cookieHeader);
272
+
273
+ return fetch(new Request(request, { headers }));
274
+ }
275
+ }
276
+
277
+ return fetch(request);
278
+ };
src/hooks.ts ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
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 ADDED
@@ -0,0 +1,55 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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,7 +12,6 @@
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,6 +19,9 @@
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
 
 
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
  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
 
src/lib/components/DisclaimerModal.svelte CHANGED
@@ -1,13 +1,15 @@
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,20 +58,18 @@
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>
 
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
  {/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>
src/lib/components/LoginModal.svelte CHANGED
@@ -1,14 +1,15 @@
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,15 +28,10 @@
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,7 +40,7 @@
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,6 +55,6 @@
59
  Start chatting
60
  </button>
61
  {/if}
62
- </form>
63
  </div>
64
  </Modal>
 
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
  {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
  &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
  Start chatting
56
  </button>
57
  {/if}
58
+ </div>
59
  </div>
60
  </Modal>
src/lib/components/NavConversationItem.svelte CHANGED
@@ -43,11 +43,15 @@
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
 
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
src/lib/components/NavMenu.svelte CHANGED
@@ -13,7 +13,6 @@
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,13 +20,20 @@
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,15 +71,18 @@
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,9 +175,13 @@
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,24 +189,21 @@
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
 
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
  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
 
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
  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
  >{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
src/lib/components/ToolsMenu.svelte CHANGED
@@ -4,10 +4,12 @@
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;
 
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;
src/lib/components/chat/AssistantIntroduction.svelte CHANGED
@@ -15,14 +15,17 @@
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"
 
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"
src/lib/components/chat/ChatInput.svelte CHANGED
@@ -23,6 +23,8 @@
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,7 +33,7 @@
31
  placeholder?: string;
32
  loading?: boolean;
33
  disabled?: boolean;
34
- assistant?: Assistant | undefined;
35
  modelHasTools?: boolean;
36
  modelIsMultimodal?: boolean;
37
  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 type { Serialize } from "$lib/utils/serialize";
27
+
28
  import { isVirtualKeyboard } from "$lib/utils/isVirtualKeyboard";
29
  interface Props {
30
  files?: File[];
 
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;
src/lib/components/chat/ChatIntroduction.svelte CHANGED
@@ -1,6 +1,4 @@
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,6 +7,9 @@
9
  import ModelCardMetadata from "../ModelCardMetadata.svelte";
10
  import { base } from "$app/paths";
11
  import JSON5 from "json5";
 
 
 
12
 
13
  interface Props {
14
  currentModel: Model;
 
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
  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;
src/lib/components/chat/ChatWindow.svelte CHANGED
@@ -37,6 +37,7 @@
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,7 +49,7 @@
48
  shared?: boolean;
49
  currentModel: Model;
50
  models: Model[];
51
- assistant?: Assistant | undefined;
52
  preprompt?: string | undefined;
53
  files?: File[];
54
  }
@@ -259,7 +260,7 @@
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
 
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
  shared?: boolean;
50
  currentModel: Model;
51
  models: Model[];
52
+ assistant?: Serialize<Assistant> | undefined;
53
  preprompt?: string | undefined;
54
  files?: File[];
55
  }
 
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
src/lib/components/icons/Logo.svelte CHANGED
@@ -1,8 +1,7 @@
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,16 +10,6 @@
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,7 +27,6 @@
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}
 
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
  let { classNames = "" }: Props = $props();
11
  </script>
12
 
 
 
 
 
 
 
 
 
 
 
13
  {#if publicConfig.PUBLIC_APP_ASSETS === "chatui"}
14
  <svg
15
  height="30"
 
27
  <img
28
  class={classNames}
29
  alt="{publicConfig.PUBLIC_APP_NAME} logo"
30
+ src="{publicConfig.assetPath}/logo.svg"
 
31
  />
32
  {/if}
src/lib/server/api/authPlugin.ts ADDED
@@ -0,0 +1,25 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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 ADDED
@@ -0,0 +1,35 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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 ADDED
@@ -0,0 +1,180 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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 ADDED
@@ -0,0 +1,171 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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 ADDED
@@ -0,0 +1,77 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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 ADDED
@@ -0,0 +1,106 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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 ADDED
@@ -0,0 +1,253 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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 ADDED
@@ -0,0 +1,197 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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,6 +14,9 @@ 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
 
18
  export interface OIDCSettings {
19
  redirectURI: string;
@@ -79,6 +82,10 @@ export async function findUser(sessionId: 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,6 +172,7 @@ export async function validateAndParseCsrfToken(
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,3 +183,131 @@ export async function validateAndParseCsrfToken(
175
  }
176
  return null;
177
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
  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
  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
  }
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
+ }
src/lib/server/config.ts CHANGED
@@ -1,6 +1,5 @@
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";
@@ -151,9 +150,7 @@ const configManager = new ConfigManager();
151
 
152
  export const ready = (async () => {
153
  if (!building) {
154
- await configManager.init().then(() => {
155
- publicConfig.init(configManager.getPublicConfig());
156
- });
157
  }
158
  })();
159
 
 
1
  import { env as publicEnv } from "$env/dynamic/public";
2
  import { env as serverEnv } from "$env/dynamic/private";
 
3
  import { building } from "$app/environment";
4
  import type { Collection } from "mongodb";
5
  import type { ConfigKey as ConfigKeyType } from "$lib/types/ConfigKey";
 
150
 
151
  export const ready = (async () => {
152
  if (!building) {
153
+ await configManager.init();
 
 
154
  }
155
  })();
156
 
src/lib/server/isURLLocal.ts CHANGED
@@ -1,5 +1,6 @@
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,3 +37,12 @@ export function isURLStringLocal(url: string) {
36
  return true;
37
  }
38
  }
 
 
 
 
 
 
 
 
 
 
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
  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
+ }
src/lib/server/models.ts CHANGED
@@ -13,6 +13,7 @@ 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 { join, dirname } from "path";
17
  import { fileURLToPath } from "url";
18
  import { findRepoRoot } from "./findRepoRoot";
@@ -346,13 +347,12 @@ const addEndpoint = (m: Awaited<ReturnType<typeof processModel>>) => ({
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
  : [];
 
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
  });
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
  : [];
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;
8
  }
 
4
  updatedAt: Date;
5
  model?: string;
6
  assistantId?: string;
7
+ avatarUrl?: string | Promise<string | undefined>;
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",
5
  }
 
1
  /* eslint-disable no-shadow */
2
  export enum UrlDependency {
3
  ConversationList = "conversation:list",
4
+ Conversation = "conversation:id",
5
  }
src/lib/utils/PublicConfig.svelte.ts CHANGED
@@ -1,12 +1,21 @@
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,29 +26,52 @@ class PublicConfigManager {
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;
 
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
  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 usePublicConfig = () => getContext<ConfigProxy>("publicConfig");
 
 
 
 
 
 
 
 
 
 
src/lib/utils/fetchJSON.ts ADDED
@@ -0,0 +1,25 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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,8 +1,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
  }
 
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
  }
src/lib/utils/messageUpdates.ts CHANGED
@@ -15,7 +15,8 @@ import {
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,7 +97,7 @@ export async function fetchMessageUpdates(
96
  throw Error("Body not defined");
97
  }
98
 
99
- if (!(publicConfig.PUBLIC_SMOOTH_UPDATES === "true")) {
100
  return endpointStreamToIterator(response, abortController);
101
  }
102
 
 
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
  throw Error("Body not defined");
98
  }
99
 
100
+ if (!(page.data.publicConfig.PUBLIC_SMOOTH_UPDATES === "true")) {
101
  return endpointStreamToIterator(response, abortController);
102
  }
103
 
src/lib/utils/serialize.ts ADDED
@@ -0,0 +1,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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,12 +1,7 @@
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,7 +10,7 @@ export function addChildren(
15
  ...message,
16
  ancestors: [],
17
  id: messageId,
18
- });
19
  return messageId;
20
  }
21
 
@@ -29,7 +24,7 @@ export function addChildren(
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,7 +34,7 @@ export function addChildren(
39
  ancestors,
40
  id: messageId,
41
  children: [],
42
- });
43
 
44
  const parent = conv.messages.find((m) => m.id === parentId);
45
 
 
 
 
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
  ...message,
11
  ancestors: [],
12
  id: messageId,
13
+ } as TreeNode<T>);
14
  return messageId;
15
  }
16
 
 
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
  ancestors,
35
  id: messageId,
36
  children: [],
37
+ } as TreeNode<T>);
38
 
39
  const parent = conv.messages.find((m) => m.id === parentId);
40
 
src/lib/utils/tree/addSibling.spec.ts CHANGED
@@ -5,10 +5,11 @@ 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
 
9
- const newMessage: Omit<Message, "id"> = {
10
  content: "new message",
11
- from: "user",
12
  };
13
 
14
  Object.freeze(newMessage);
@@ -18,8 +19,8 @@ describe("addSibling", async () => {
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"
 
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
  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"
src/lib/utils/tree/addSibling.ts CHANGED
@@ -1,12 +1,7 @@
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,7 +26,7 @@ export function addSibling(
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);
 
 
 
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
  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);
src/lib/utils/tree/buildSubtree.ts CHANGED
@@ -1,10 +1,6 @@
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
 
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
src/lib/utils/tree/tree.d.ts ADDED
@@ -0,0 +1,14 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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 DELETED
@@ -1,286 +0,0 @@
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,8 +6,6 @@
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,9 +21,14 @@
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,14 +184,6 @@
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,35 +198,13 @@
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,7 +254,7 @@
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,7 +266,7 @@
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)}
 
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
  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
  publicConfig.PUBLIC_APP_DISCLAIMER === "1" &&
185
  !($page.data.shared === true)
186
  );
 
 
 
 
 
 
 
 
187
  </script>
188
 
189
  <svelte:head>
 
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
  <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
  <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)}
src/routes/+layout.ts ADDED
@@ -0,0 +1,81 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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,7 +2,9 @@
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,8 +33,8 @@
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
  }
 
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
  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
  }
src/routes/api/assistant/[id]/subscribe/+server.ts CHANGED
@@ -23,6 +23,7 @@ export async function POST({ params, locals }) {
23
 
24
  const result = await collections.settings.updateOne(authCondition(locals), {
25
  $addToSet: { assistants: assistant._id },
 
26
  });
27
 
28
  // reduce count only if push succeeded
 
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
src/routes/api/v2/[...slugs]/+server.ts ADDED
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
 
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 DELETED
@@ -1,42 +0,0 @@
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,7 +3,9 @@
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";
 
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";
src/routes/assistant/[assistantId]/+page.ts ADDED
@@ -0,0 +1,16 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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 DELETED
@@ -1,83 +0,0 @@
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
- };