nsarrazin commited on
Commit
50aea0a
·
unverified ·
1 Parent(s): e1848d6

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

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

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 +53 -109
  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 +2 -2
  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 +53 -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
@@ -2,20 +2,19 @@ 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,38 @@ export const handle: Handle = async ({ event, resolve }) => {
316
  response.headers.append("Content-Security-Policy", "frame-ancestors 'none';");
317
  }
318
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
319
  return response;
320
  };
 
2
  import type { Handle, HandleServerError, ServerInit } from "@sveltejs/kit";
3
  import { collections } from "$lib/server/database";
4
  import { base } from "$app/paths";
5
+ import { authenticateRequest, refreshSessionCookie, requiresUser } from "$lib/server/auth";
6
  import { ERROR_MESSAGES } from "$lib/stores/errors";
 
7
  import { addWeeks } from "date-fns";
8
  import { checkAndRunMigrations } from "$lib/migrations/migrations";
9
+ import { building, dev } from "$app/environment";
10
  import { logger } from "$lib/server/logger";
11
  import { AbortedGenerations } from "$lib/server/abortedGenerations";
12
  import { MetricsServer } from "$lib/server/metrics";
13
  import { initExitHandler } from "$lib/server/exitHandler";
 
14
  import { refreshAssistantsCounts } from "$lib/jobs/refresh-assistants-counts";
15
  import { refreshConversationStats } from "$lib/jobs/refresh-conversation-stats";
16
  import { adminTokenManager } from "$lib/server/adminToken";
17
+ import { isHostLocalhost } from "$lib/server/isURLLocal";
18
 
19
  export const init: ServerInit = async () => {
20
  // Wait for config to be fully loaded
 
119
  }
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
  };
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,6 @@
1
  import { env as publicEnv } from "$env/dynamic/public";
2
  import { env as serverEnv } from "$env/dynamic/private";
3
- import { publicConfig } from "$lib/utils/PublicConfig.svelte";
4
  import { building } from "$app/environment";
5
  import type { Collection } from "mongodb";
6
  import type { ConfigKey as ConfigKeyType } from "$lib/types/ConfigKey";
@@ -152,7 +152,7 @@ const configManager = new ConfigManager();
152
  export const ready = (async () => {
153
  if (!building) {
154
  await configManager.init().then(() => {
155
- publicConfig.init(configManager.getPublicConfig());
156
  });
157
  }
158
  })();
 
1
  import { env as publicEnv } from "$env/dynamic/public";
2
  import { env as serverEnv } from "$env/dynamic/private";
3
+ import { serverPublicConfig } from "$lib/utils/PublicConfig.svelte";
4
  import { building } from "$app/environment";
5
  import type { Collection } from "mongodb";
6
  import type { ConfigKey as ConfigKeyType } from "$lib/types/ConfigKey";
 
152
  export const ready = (async () => {
153
  if (!building) {
154
  await configManager.init().then(() => {
155
+ serverPublicConfig.init(configManager.getPublicConfig());
156
  });
157
  }
158
  })();
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,53 @@ 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 serverPublicConfig = getConfigManager();
78
+ 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
- };