From 8ac33f0931274ba544b2ff68cebc1ee9a83a766c Mon Sep 17 00:00:00 2001 From: Nathan Sarrazin Date: Fri, 14 Feb 2025 10:46:06 +0000 Subject: [PATCH 01/34] feat(API): refactor API with Elysia --- package-lock.json | 135 +++++++++++++++++++++++++++ package.json | 3 + src/hooks.server.ts | 112 ++-------------------- src/lib/server/auth.ts | 111 ++++++++++++++++++++++ src/routes/api/[...slugs]/+server.ts | 26 ++++++ 5 files changed, 284 insertions(+), 103 deletions(-) create mode 100644 src/routes/api/[...slugs]/+server.ts diff --git a/package-lock.json b/package-lock.json index 23cd9e569c3..c4cada16fbc 100644 --- a/package-lock.json +++ b/package-lock.json @@ -57,6 +57,8 @@ "zod": "^3.22.3" }, "devDependencies": { + "@elysiajs/eden": "^1.2.0", + "@elysiajs/node": "^1.2.5", "@faker-js/faker": "^8.4.1", "@iconify-json/carbon": "^1.1.16", "@iconify-json/eos-icons": "^1.1.6", @@ -79,6 +81,7 @@ "@typescript-eslint/eslint-plugin": "^6.x", "@typescript-eslint/parser": "^6.x", "dompurify": "^3.1.6", + "elysia": "^1.2.12", "eslint": "^8.28.0", "eslint-config-prettier": "^8.5.0", "eslint-plugin-svelte": "^2.45.1", @@ -1428,6 +1431,38 @@ "@jridgewell/sourcemap-codec": "^1.4.10" } }, + "node_modules/@elysiajs/eden": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@elysiajs/eden/-/eden-1.2.0.tgz", + "integrity": "sha512-MpV45ahuF+iFZUg4tyJbLr9qxzY99m8clJVgQrDrz7Qh6eOKQ8MY6vjYMj3Wh21pTIRHPHzOLhVorRGby1/Owg==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "elysia": ">= 1.2.0" + } + }, + "node_modules/@elysiajs/node": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/@elysiajs/node/-/node-1.2.5.tgz", + "integrity": "sha512-g5iE2csoixsx4KT4Q57BVjQqjcjJPigWF1ruGHguxrwmI/nfTlWNnpKAR6XU+MERhr3Cf7dd6GI826BBNgo0xw==", + "dev": true, + "license": "MIT", + "dependencies": { + "formidable": "^3.5.2", + "ws": "^8.18.0" + }, + "peerDependencies": { + "bufferutil": ">= 4.0.1", + "elysia": ">= 1.2.7", + "formidable": ">= 3.5.2", + "ws": ">= 8.18.0" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + } + } + }, "node_modules/@emnapi/runtime": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.2.0.tgz", @@ -3579,6 +3614,13 @@ "node": ">= 8.0.0" } }, + "node_modules/@sinclair/typebox": { + "version": "0.34.21", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.34.21.tgz", + "integrity": "sha512-G+7YVIZalo+N+h7KapWBxRH2at9yO4yIHq49PkQLTQQkPTM0ZYaPOfPDpeNv/ukWjm8a1GdnO3yDC7bietT23A==", + "dev": true, + "license": "MIT" + }, "node_modules/@smithy/abort-controller": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/@smithy/abort-controller/-/abort-controller-3.1.1.tgz", @@ -5110,6 +5152,13 @@ "node": ">=8" } }, + "node_modules/asap": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz", + "integrity": "sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==", + "dev": true, + "license": "MIT" + }, "node_modules/assertion-error": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", @@ -6138,6 +6187,17 @@ "integrity": "sha512-maua5KUiapvEwiEAe+XnlZ3Rh0GD+qI1J/nb9vrJc3muPXvcF/8gXYTWF76+5DAqHyDUtOIImEuo0YKE9mshVw==", "dev": true }, + "node_modules/dezalgo": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/dezalgo/-/dezalgo-1.0.4.tgz", + "integrity": "sha512-rXSP0bf+5n0Qonsb+SVVfNfIsimO4HEtmnIpPHY8Q1UCzKlQrDMfdobr8nJOOsRgWCyMRqeSBQzmWUMq7zvVig==", + "dev": true, + "license": "ISC", + "dependencies": { + "asap": "^2.0.0", + "wrappy": "1" + } + }, "node_modules/didyoumean": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz", @@ -6284,6 +6344,42 @@ "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.18.tgz", "integrity": "sha512-1OfuVACu+zKlmjsNdcJuVQuVE61sZOLbNM4JAQ1Rvh6EOj0/EUKhMJjRH73InPlXSh8HIJk1cVZ8pyOV/FMdUQ==" }, + "node_modules/elysia": { + "version": "1.2.12", + "resolved": "https://registry.npmjs.org/elysia/-/elysia-1.2.12.tgz", + "integrity": "sha512-X1bZo09qe8/Poa/5tz08Y+sE/77B/wLwnA5xDDENU3FCrsUtYJuBVcy6BPXGRCgnJ1fPQpc0Ov2ZU5MYJXluTg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@sinclair/typebox": "^0.34.15", + "cookie": "^1.0.2", + "memoirist": "^0.3.0", + "openapi-types": "^12.1.3" + }, + "peerDependencies": { + "@sinclair/typebox": ">= 0.34.0", + "openapi-types": ">= 12.0.0", + "typescript": ">= 5.0.0" + }, + "peerDependenciesMeta": { + "openapi-types": { + "optional": true + }, + "typescript": { + "optional": true + } + } + }, + "node_modules/elysia/node_modules/cookie": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.0.2.tgz", + "integrity": "sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, "node_modules/emoji-regex": { "version": "10.4.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.4.0.tgz", @@ -7276,6 +7372,21 @@ "node": ">= 12.20" } }, + "node_modules/formidable": { + "version": "3.5.2", + "resolved": "https://registry.npmjs.org/formidable/-/formidable-3.5.2.tgz", + "integrity": "sha512-Jqc1btCy3QzRbJaICGwKcBfGWuLADRerLzDqi2NwSt/UkXLsHJw2TVResiaoBufHVHy9aSgClOHCeJsSsFLTbg==", + "dev": true, + "license": "MIT", + "dependencies": { + "dezalgo": "^1.0.4", + "hexoid": "^2.0.0", + "once": "^1.4.0" + }, + "funding": { + "url": "https://ko-fi.com/tunnckoCore/commissions" + } + }, "node_modules/forwarded": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", @@ -7669,6 +7780,16 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/hexoid": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/hexoid/-/hexoid-2.0.0.tgz", + "integrity": "sha512-qlspKUK7IlSQv2o+5I7yhUd7TxlOG2Vr5LTa3ve2XSNVKAL/n/u/7KLvKmFNimomDIKvZFXWHv0T12mv7rT8Aw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/highlight.js": { "version": "11.10.0", "resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-11.10.0.tgz", @@ -8908,6 +9029,13 @@ "node": ">= 0.6" } }, + "node_modules/memoirist": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/memoirist/-/memoirist-0.3.0.tgz", + "integrity": "sha512-wR+4chMgVPq+T6OOsk40u9Wlpw1Pjx66NMNiYxCQQ4EUJ7jDs3D9kTCeKdBOkvAiqXlHLVJlvYL01PvIJ1MPNg==", + "dev": true, + "license": "MIT" + }, "node_modules/memory-pager": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/memory-pager/-/memory-pager-1.5.0.tgz", @@ -9799,6 +9927,13 @@ "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", "optional": true }, + "node_modules/openapi-types": { + "version": "12.1.3", + "resolved": "https://registry.npmjs.org/openapi-types/-/openapi-types-12.1.3.tgz", + "integrity": "sha512-N4YtSYJqghVu4iek2ZUvcN/0aqH1kRDuNqzcycDxhOUpg7GdvLa2F3DgS6yBNhInhv2r/6I0Flkn7CqL8+nIcw==", + "dev": true, + "license": "MIT" + }, "node_modules/openid-client": { "version": "5.6.5", "resolved": "https://registry.npmjs.org/openid-client/-/openid-client-5.6.5.tgz", diff --git a/package.json b/package.json index 354ddd6a98f..cd4700d8dc3 100644 --- a/package.json +++ b/package.json @@ -17,6 +17,8 @@ "prepare": "husky" }, "devDependencies": { + "@elysiajs/eden": "^1.2.0", + "@elysiajs/node": "^1.2.5", "@faker-js/faker": "^8.4.1", "@iconify-json/carbon": "^1.1.16", "@iconify-json/eos-icons": "^1.1.6", @@ -39,6 +41,7 @@ "@typescript-eslint/eslint-plugin": "^6.x", "@typescript-eslint/parser": "^6.x", "dompurify": "^3.1.6", + "elysia": "^1.2.12", "eslint": "^8.28.0", "eslint-config-prettier": "^8.5.0", "eslint-plugin-svelte": "^2.45.1", diff --git a/src/hooks.server.ts b/src/hooks.server.ts index 964d53c6254..25dcfff5d22 100644 --- a/src/hooks.server.ts +++ b/src/hooks.server.ts @@ -3,9 +3,8 @@ import { env as envPublic } from "$env/dynamic/public"; import type { Handle, HandleServerError } from "@sveltejs/kit"; import { collections } from "$lib/server/database"; import { base } from "$app/paths"; -import { findUser, refreshSessionCookie, requiresUser } from "$lib/server/auth"; +import { authenticateRequest, refreshSessionCookie, requiresUser } from "$lib/server/auth"; import { ERROR_MESSAGES } from "$lib/stores/errors"; -import { sha256 } from "$lib/utils/sha256"; import { addWeeks } from "date-fns"; import { checkAndRunMigrations } from "$lib/migrations/migrations"; import { building } from "$app/environment"; @@ -13,7 +12,6 @@ import { logger } from "$lib/server/logger"; import { AbortedGenerations } from "$lib/server/abortedGenerations"; import { MetricsServer } from "$lib/server/metrics"; import { initExitHandler } from "$lib/server/exitHandler"; -import { ObjectId } from "mongodb"; import { refreshAssistantsCounts } from "$lib/jobs/refresh-assistants-counts"; import { refreshConversationStats } from "$lib/jobs/refresh-conversation-stats"; @@ -109,105 +107,13 @@ export const handle: Handle = async ({ event, resolve }) => { } } - const token = event.cookies.get(env.COOKIE_NAME); - - // if the trusted email header is set we use it to get the user email - const email = env.TRUSTED_EMAIL_HEADER - ? event.request.headers.get(env.TRUSTED_EMAIL_HEADER) - : null; - - let secretSessionId: string | null = null; - let sessionId: string | null = null; - - if (email) { - secretSessionId = sessionId = await sha256(email); - - event.locals.user = { - // generate id based on email - _id: new ObjectId(sessionId.slice(0, 24)), - name: email, - email, - createdAt: new Date(), - updatedAt: new Date(), - hfUserId: email, - avatarUrl: "", - logoutDisabled: true, - }; - } else if (token) { - secretSessionId = token; - sessionId = await sha256(token); - - const user = await findUser(sessionId); - - if (user) { - event.locals.user = user; - } - } else if (event.url.pathname.startsWith(`${base}/api/`) && env.USE_HF_TOKEN_IN_API === "true") { - // if the request goes to the API and no user is available in the header - // check if a bearer token is available in the Authorization header - - const authorization = event.request.headers.get("Authorization"); - - if (authorization && authorization.startsWith("Bearer ")) { - const token = authorization.slice(7); - - const hash = await sha256(token); - - sessionId = secretSessionId = hash; - - // check if the hash is in the DB and get the user - // else check against https://huggingface.co/api/whoami-v2 - - const cacheHit = await collections.tokenCaches.findOne({ tokenHash: hash }); - - if (cacheHit) { - const user = await collections.users.findOne({ hfUserId: cacheHit.userId }); - - if (!user) { - return errorResponse(500, "User not found"); - } - - event.locals.user = user; - } else { - const response = await fetch("https://huggingface.co/api/whoami-v2", { - headers: { - Authorization: `Bearer ${token}`, - }, - }); - - if (!response.ok) { - return errorResponse(401, "Unauthorized"); - } - - const data = await response.json(); - const user = await collections.users.findOne({ hfUserId: data.id }); - - if (!user) { - return errorResponse(500, "User not found"); - } - - await collections.tokenCaches.insertOne({ - tokenHash: hash, - userId: data.id, - createdAt: new Date(), - updatedAt: new Date(), - }); - - event.locals.user = user; - } - } - } - - if (!sessionId || !secretSessionId) { - secretSessionId = crypto.randomUUID(); - sessionId = await sha256(secretSessionId); - - if (await collections.sessions.findOne({ sessionId })) { - return errorResponse(500, "Session ID collision"); - } - } + const auth = await authenticateRequest( + { type: "svelte", value: event.request.headers }, + { type: "svelte", value: event.cookies } + ); - event.locals.sessionId = sessionId; + event.locals.user = auth.user || undefined; + event.locals.sessionId = auth.sessionId; // CSRF protection const requestContentType = event.request.headers.get("content-type")?.split(";")[0] ?? ""; @@ -239,10 +145,10 @@ export const handle: Handle = async ({ event, resolve }) => { if (event.request.method === "POST") { // if the request is a POST request we refresh the cookie - refreshSessionCookie(event.cookies, secretSessionId); + refreshSessionCookie(event.cookies, auth.secretSessionId); await collections.sessions.updateOne( - { sessionId }, + { sessionId: auth.sessionId }, { $set: { updatedAt: new Date(), expiresAt: addWeeks(new Date(), 2) } } ); } diff --git a/src/lib/server/auth.ts b/src/lib/server/auth.ts index ae170a8bc3f..20baedc88c8 100644 --- a/src/lib/server/auth.ts +++ b/src/lib/server/auth.ts @@ -14,6 +14,8 @@ import type { Cookies } from "@sveltejs/kit"; import { collections } from "$lib/server/database"; import JSON5 from "json5"; import { logger } from "$lib/server/logger"; +import { ObjectId } from "mongodb"; +import type { Cookie } from "elysia"; export interface OIDCSettings { redirectURI: string; @@ -175,3 +177,112 @@ export async function validateAndParseCsrfToken( } return null; } + +type CookieRecord = + | { type: "elysia"; value: Record> } + | { type: "svelte"; value: Cookies }; +type HeaderRecord = + | { type: "elysia"; value: Record } + | { type: "svelte"; value: Headers }; + +export async function authenticateRequest( + headers: HeaderRecord, + cookie: CookieRecord, + isApi?: boolean +) { + const token = + cookie.type === "elysia" + ? cookie.value[env.COOKIE_NAME].value + : cookie.value.get(env.COOKIE_NAME); + + let email = null; + if (env.TRUSTED_EMAIL_HEADER) { + if (headers.type === "elysia") { + email = headers.value[env.TRUSTED_EMAIL_HEADER]; + } else { + email = headers.value.get(env.TRUSTED_EMAIL_HEADER); + } + } + + let secretSessionId: string | null = null; + let sessionId: string | null = null; + + if (email) { + secretSessionId = sessionId = await sha256(email); + return { + user: { + _id: new ObjectId(sessionId.slice(0, 24)), + name: email, + email, + createdAt: new Date(), + updatedAt: new Date(), + hfUserId: email, + avatarUrl: "", + logoutDisabled: true, + }, + sessionId, + secretSessionId, + }; + } + + if (token) { + secretSessionId = token; + sessionId = await sha256(token); + const user = await findUser(sessionId); + return { user, sessionId, secretSessionId }; + } + + if (isApi) { + const authorization = + headers.type === "elysia" + ? headers.value["Authorization"] + : headers.value.get("Authorization"); + if (authorization?.startsWith("Bearer ")) { + const token = authorization.slice(7); + const hash = await sha256(token); + sessionId = secretSessionId = hash; + + const cacheHit = await collections.tokenCaches.findOne({ tokenHash: hash }); + if (cacheHit) { + const user = await collections.users.findOne({ hfUserId: cacheHit.userId }); + if (!user) { + throw new Error("User not found"); + } + return { user, sessionId, secretSessionId }; + } + + const response = await fetch("https://huggingface.co/api/whoami-v2", { + headers: { Authorization: `Bearer ${token}` }, + }); + + if (!response.ok) { + throw new Error("Unauthorized"); + } + + const data = await response.json(); + const user = await collections.users.findOne({ hfUserId: data.id }); + if (!user) { + throw new Error("User not found"); + } + + await collections.tokenCaches.insertOne({ + tokenHash: hash, + userId: data.id, + createdAt: new Date(), + updatedAt: new Date(), + }); + + return { user, sessionId, secretSessionId }; + } + } + + // Generate new session if none exists + secretSessionId = crypto.randomUUID(); + sessionId = await sha256(secretSessionId); + + if (await collections.sessions.findOne({ sessionId })) { + throw new Error("Session ID collision"); + } + + return { user: null, sessionId, secretSessionId }; +} diff --git a/src/routes/api/[...slugs]/+server.ts b/src/routes/api/[...slugs]/+server.ts new file mode 100644 index 00000000000..11c55872577 --- /dev/null +++ b/src/routes/api/[...slugs]/+server.ts @@ -0,0 +1,26 @@ +import { Elysia } from "elysia"; +import { base } from "$app/paths"; +import { authenticateRequest } from "$lib/server/auth"; + +const app = new Elysia({ prefix: `${base}/api` }) + .derive(async ({ headers, cookie }) => ({ + locals: await authenticateRequest( + { type: "elysia", value: headers }, + { type: "elysia", value: cookie }, + true + ), + })) + .get("/foo", ({ locals }) => { + return locals.user?._id; + }) + .get("/bar", ({ locals }) => { + return locals.user?.name; + }); + +type RequestHandler = (v: { request: Request; locals: App.Locals }) => Response | Promise; + +export const GET: RequestHandler = ({ request }) => app.handle(request); +export const POST: RequestHandler = ({ request }) => app.handle(request); +export const PUT: RequestHandler = ({ request }) => app.handle(request); +export const PATCH: RequestHandler = ({ request }) => app.handle(request); +export const DELETE: RequestHandler = ({ request }) => app.handle(request); From 57c6db5412378e23aef3f3d968c3201bd6ae1df7 Mon Sep 17 00:00:00 2001 From: Nathan Sarrazin Date: Thu, 20 Feb 2025 13:09:10 +0000 Subject: [PATCH 02/34] feat: initial elysia setup --- .vscode/settings.json | 3 + package-lock.json | 108 +++++++++++- package.json | 1 + src/hooks.server.ts | 3 + src/lib/server/api/authPlugin.ts | 24 +++ .../server/api/routes/groups/assistants.ts | 45 +++++ .../server/api/routes/groups/conversations.ts | 163 ++++++++++++++++++ src/lib/server/api/routes/groups/tools.ts | 48 ++++++ src/lib/server/api/routes/groups/user.ts | 36 ++++ src/lib/server/api/server.ts | 57 ++++++ src/lib/server/auth.ts | 15 +- src/lib/utils/api.ts | 12 ++ src/routes/api/v2/[...slugs]/+server.ts | 9 + 13 files changed, 513 insertions(+), 11 deletions(-) create mode 100644 src/lib/server/api/authPlugin.ts create mode 100644 src/lib/server/api/routes/groups/assistants.ts create mode 100644 src/lib/server/api/routes/groups/conversations.ts create mode 100644 src/lib/server/api/routes/groups/tools.ts create mode 100644 src/lib/server/api/routes/groups/user.ts create mode 100644 src/lib/server/api/server.ts create mode 100644 src/lib/utils/api.ts create mode 100644 src/routes/api/v2/[...slugs]/+server.ts diff --git a/.vscode/settings.json b/.vscode/settings.json index 4a65d631069..670fc9d919e 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -7,5 +7,8 @@ "eslint.validate": ["javascript", "svelte"], "[svelte]": { "editor.defaultFormatter": "esbenp.prettier-vscode" + }, + "[typescript]": { + "editor.defaultFormatter": "esbenp.prettier-vscode" } } diff --git a/package-lock.json b/package-lock.json index c4cada16fbc..401ba1d6ee2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,6 +10,7 @@ "dependencies": { "@aws-sdk/credential-providers": "^3.592.0", "@cliqz/adblocker-playwright": "^1.27.2", + "@elysiajs/swagger": "^1.2.0", "@gradio/client": "^1.8.0", "@huggingface/hub": "^0.5.1", "@huggingface/inference": "^2.8.1", @@ -1463,6 +1464,21 @@ } } }, + "node_modules/@elysiajs/swagger": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@elysiajs/swagger/-/swagger-1.2.0.tgz", + "integrity": "sha512-OPx93DP6rM2VHjA3D44Xiz5MYm9AYlO2NGWPsnSsdyvaOCiL9wJj529583h7arX4iIEYE5LiLB0/A45unqbopw==", + "license": "MIT", + "dependencies": { + "@scalar/themes": "^0.9.52", + "@scalar/types": "^0.0.12", + "openapi-types": "^12.1.3", + "pathe": "^1.1.2" + }, + "peerDependencies": { + "elysia": ">= 1.2.0" + } + }, "node_modules/@emnapi/runtime": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.2.0.tgz", @@ -3594,6 +3610,62 @@ "win32" ] }, + "node_modules/@scalar/openapi-types": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/@scalar/openapi-types/-/openapi-types-0.1.1.tgz", + "integrity": "sha512-NMy3QNk6ytcCoPUGJH0t4NNr36OWXgZhA3ormr3TvhX1NDgoF95wFyodGVH8xiHeUyn2/FxtETm8UBLbB5xEmg==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@scalar/themes": { + "version": "0.9.66", + "resolved": "https://registry.npmjs.org/@scalar/themes/-/themes-0.9.66.tgz", + "integrity": "sha512-Fm2dUlIQoWCG83yZ2QNdIG7j+3eHgmSQHSnGOfd59+XIC/JxmCVbiOCYyhzfCXl1Zb8YcPlu6Ka2wY++GlrEeQ==", + "license": "MIT", + "dependencies": { + "@scalar/types": "0.0.32" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@scalar/themes/node_modules/@scalar/openapi-types": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/@scalar/openapi-types/-/openapi-types-0.1.7.tgz", + "integrity": "sha512-oOTG3JQifg55U3DhKB7WdNIxFnJzbPJe7rqdyWdio977l8IkxQTVmObftJhdNIMvhV2K+1f/bDoMQGu6yTaD0A==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@scalar/themes/node_modules/@scalar/types": { + "version": "0.0.32", + "resolved": "https://registry.npmjs.org/@scalar/types/-/types-0.0.32.tgz", + "integrity": "sha512-WHMkFQw4cu1mrG4pEiTUXVBBs205kHECdLM/5F7ATI0A7Axv6G1GgofkwbyCAayUjNk82uaCXzSOgPojbq4iGQ==", + "license": "MIT", + "dependencies": { + "@scalar/openapi-types": "0.1.7", + "@unhead/schema": "^1.11.11" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@scalar/types": { + "version": "0.0.12", + "resolved": "https://registry.npmjs.org/@scalar/types/-/types-0.0.12.tgz", + "integrity": "sha512-XYZ36lSEx87i4gDqopQlGCOkdIITHHEvgkuJFrXFATQs9zHARop0PN0g4RZYWj+ZpCUclOcaOjbCt8JGe22mnQ==", + "license": "MIT", + "dependencies": { + "@scalar/openapi-types": "0.1.1", + "@unhead/schema": "^1.9.5" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/@sec-ant/readable-stream": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/@sec-ant/readable-stream/-/readable-stream-0.4.1.tgz", @@ -3618,7 +3690,6 @@ "version": "0.34.21", "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.34.21.tgz", "integrity": "sha512-G+7YVIZalo+N+h7KapWBxRH2at9yO4yIHq49PkQLTQQkPTM0ZYaPOfPDpeNv/ukWjm8a1GdnO3yDC7bietT23A==", - "dev": true, "license": "MIT" }, "node_modules/@smithy/abort-controller": { @@ -4867,6 +4938,19 @@ "integrity": "sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==", "dev": true }, + "node_modules/@unhead/schema": { + "version": "1.11.19", + "resolved": "https://registry.npmjs.org/@unhead/schema/-/schema-1.11.19.tgz", + "integrity": "sha512-7VhYHWK7xHgljdv+C01MepCSYZO2v6OhgsfKWPxRQBDDGfUKCUaChox0XMq3tFvXP6u4zSp6yzcDw2yxCfVMwg==", + "license": "MIT", + "dependencies": { + "hookable": "^5.5.3", + "zhead": "^2.2.4" + }, + "funding": { + "url": "https://github.com/sponsors/harlan-zw" + } + }, "node_modules/@vitest/expect": { "version": "2.1.9", "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-2.1.9.tgz", @@ -6348,7 +6432,6 @@ "version": "1.2.12", "resolved": "https://registry.npmjs.org/elysia/-/elysia-1.2.12.tgz", "integrity": "sha512-X1bZo09qe8/Poa/5tz08Y+sE/77B/wLwnA5xDDENU3FCrsUtYJuBVcy6BPXGRCgnJ1fPQpc0Ov2ZU5MYJXluTg==", - "dev": true, "license": "MIT", "dependencies": { "@sinclair/typebox": "^0.34.15", @@ -6374,7 +6457,6 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.0.2.tgz", "integrity": "sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA==", - "dev": true, "license": "MIT", "engines": { "node": ">=18" @@ -7798,6 +7880,12 @@ "node": ">=12.0.0" } }, + "node_modules/hookable": { + "version": "5.5.3", + "resolved": "https://registry.npmjs.org/hookable/-/hookable-5.5.3.tgz", + "integrity": "sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ==", + "license": "MIT" + }, "node_modules/html-encoding-sniffer": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-3.0.0.tgz", @@ -9033,7 +9121,6 @@ "version": "0.3.0", "resolved": "https://registry.npmjs.org/memoirist/-/memoirist-0.3.0.tgz", "integrity": "sha512-wR+4chMgVPq+T6OOsk40u9Wlpw1Pjx66NMNiYxCQQ4EUJ7jDs3D9kTCeKdBOkvAiqXlHLVJlvYL01PvIJ1MPNg==", - "dev": true, "license": "MIT" }, "node_modules/memory-pager": { @@ -9931,7 +10018,6 @@ "version": "12.1.3", "resolved": "https://registry.npmjs.org/openapi-types/-/openapi-types-12.1.3.tgz", "integrity": "sha512-N4YtSYJqghVu4iek2ZUvcN/0aqH1kRDuNqzcycDxhOUpg7GdvLa2F3DgS6yBNhInhv2r/6I0Flkn7CqL8+nIcw==", - "dev": true, "license": "MIT" }, "node_modules/openid-client": { @@ -10176,8 +10262,7 @@ "node_modules/pathe": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/pathe/-/pathe-1.1.2.tgz", - "integrity": "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==", - "dev": true + "integrity": "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==" }, "node_modules/pathval": { "version": "2.0.0", @@ -14440,6 +14525,15 @@ "resolved": "https://registry.npmjs.org/yoga-wasm-web/-/yoga-wasm-web-0.3.3.tgz", "integrity": "sha512-N+d4UJSJbt/R3wqY7Coqs5pcV0aUj2j9IaQ3rNj9bVCLld8tTGKRa2USARjnvZJWVx1NDmQev8EknoczaOQDOA==" }, + "node_modules/zhead": { + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/zhead/-/zhead-2.2.4.tgz", + "integrity": "sha512-8F0OI5dpWIA5IGG5NHUg9staDwz/ZPxZtvGVf01j7vHqSyZ0raHY+78atOVxRqb73AotX22uV1pXt3gYSstGag==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/harlan-zw" + } + }, "node_modules/zimmerframe": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/zimmerframe/-/zimmerframe-1.1.2.tgz", diff --git a/package.json b/package.json index cd4700d8dc3..3a1b08f48ed 100644 --- a/package.json +++ b/package.json @@ -67,6 +67,7 @@ "dependencies": { "@aws-sdk/credential-providers": "^3.592.0", "@cliqz/adblocker-playwright": "^1.27.2", + "@elysiajs/swagger": "^1.2.0", "@gradio/client": "^1.8.0", "@huggingface/hub": "^0.5.1", "@huggingface/inference": "^2.8.1", diff --git a/src/hooks.server.ts b/src/hooks.server.ts index 25dcfff5d22..f716bfd8083 100644 --- a/src/hooks.server.ts +++ b/src/hooks.server.ts @@ -198,6 +198,9 @@ export const handle: Handle = async ({ event, resolve }) => { return chunk.html.replace("%gaId%", envPublic.PUBLIC_GOOGLE_ANALYTICS_ID); }, + filterSerializedResponseHeaders: (header) => { + return header.includes("content-type"); + }, }); // Add CSP header to disallow framing if ALLOW_IFRAME is not "true" diff --git a/src/lib/server/api/authPlugin.ts b/src/lib/server/api/authPlugin.ts new file mode 100644 index 00000000000..db6dcb7db00 --- /dev/null +++ b/src/lib/server/api/authPlugin.ts @@ -0,0 +1,24 @@ +import Elysia from "elysia"; +import { authenticateRequest } from "../auth"; + +export const authPlugin = new Elysia({ name: "auth" }).derive( + { as: "scoped" }, + async ({ + headers, + cookie, + }): Promise<{ + locals: App.Locals; + }> => { + const auth = await authenticateRequest( + { type: "elysia", value: headers }, + { type: "elysia", value: cookie }, + true + ); + return { + locals: { + user: auth?.user, + sessionId: auth?.sessionId, + }, + }; + } +); diff --git a/src/lib/server/api/routes/groups/assistants.ts b/src/lib/server/api/routes/groups/assistants.ts new file mode 100644 index 00000000000..6a50fd49a91 --- /dev/null +++ b/src/lib/server/api/routes/groups/assistants.ts @@ -0,0 +1,45 @@ +import { Elysia } from "elysia"; +import { authPlugin } from "$lib/server/api/authPlugin"; + +export const assistantGroup = new Elysia().use(authPlugin).group("/assistants", (app) => { + return app + .get("/", () => { + // todo: get assistants + return "aa"; + }) + .post("/", () => { + // todo: post new assistant + return "aa"; + }) + .group("/:id", (app) => { + return app + .get("/", () => { + // todo: get assistant + return "aa"; + }) + .patch("/", () => { + // todo: patch assistant + return "aa"; + }) + .delete("/", () => { + // todo: delete assistant + return "aa"; + }) + .post("/report", () => { + // todo: report assistant + return "aa"; + }) + .patch("/review", () => { + // todo: review assistant + return "aa"; + }) + .post("/subscribe", () => { + // todo: subscribe to assistant + return "aa"; + }) + .delete("/subscribe", () => { + // todo: unsubscribe from assistant + return "aa"; + }); + }); +}); diff --git a/src/lib/server/api/routes/groups/conversations.ts b/src/lib/server/api/routes/groups/conversations.ts new file mode 100644 index 00000000000..ef87d22b5f4 --- /dev/null +++ b/src/lib/server/api/routes/groups/conversations.ts @@ -0,0 +1,163 @@ +import { Elysia, t } from "elysia"; +import { authPlugin } from "$lib/server/api/authPlugin"; +import { collections } from "$lib/server/database"; +import { ObjectId } from "mongodb"; +import { authCondition } from "$lib/server/auth"; +import { models } from "$lib/server/models"; +import { convertLegacyConversation } from "$lib/utils/tree/convertLegacyConversation"; +import type { Conversation } from "$lib/types/Conversation"; +import type { Assistant } from "$lib/types/Assistant"; + +export type GETConversationResponse = Pick< + Conversation, + "messages" | "title" | "model" | "preprompt" | "rootMessageId" | "updatedAt" | "assistantId" +> & { + shared: boolean; + modelTools: boolean; + assistant: Assistant | undefined; + id: string; + modelId: Conversation["model"]; +}; + +export const conversationGroup = new Elysia().use(authPlugin).group("/conversations", (app) => { + return app + .get("", () => { + // todo: get conversations + return "aa"; + }) + .post("", () => { + // todo: post new conversation + return "aa"; + }) + .group( + "/:id", + { + params: t.Object({ + id: t.String(), + }), + }, + (app) => { + return app + .derive(async ({ locals, params, error }) => { + let conversation; + let shared = false; + + // if the conver + if (params.id.length === 7) { + // shared link of length 7 + conversation = await collections.sharedConversations.findOne({ + _id: params.id, + }); + shared = true; + + if (!conversation) { + return error(404, "Conversation not found"); + } + } else { + // todo: add validation on params.id + try { + new ObjectId(params.id); + } catch { + return error(400, "Invalid conversation ID format"); + } + conversation = await collections.conversations.findOne({ + _id: new ObjectId(params.id), + ...authCondition(locals), + }); + + if (!conversation) { + const conversationExists = + (await collections.conversations.countDocuments({ + _id: new ObjectId(params.id), + })) !== 0; + + if (conversationExists) { + return error( + 403, + "You don't have access to this conversation. If someone gave you this link, ask them to use the 'share' feature instead." + ); + } + + return error(404, "Conversation not found."); + } + } + + const convertedConv = { + ...conversation, + ...convertLegacyConversation(conversation), + shared, + }; + + return { conversation: convertedConv }; + }) + .get("", async ({ conversation }) => { + return { + messages: conversation.messages, + title: conversation.title, + model: conversation.model, + preprompt: conversation.preprompt, + rootMessageId: conversation.rootMessageId, + assistant: conversation.assistantId + ? JSON.parse( + JSON.stringify( + await collections.assistants.findOne({ + _id: new ObjectId(conversation.assistantId), + }) + ) + ) + : undefined, + id: conversation._id.toString(), + updatedAt: conversation.updatedAt, + modelId: conversation.model, + assistantId: conversation.assistantId, + modelTools: models.find((m) => m.id == conversation.model)?.tools ?? false, + shared: conversation.shared, + } satisfies GETConversationResponse; + }) + .post("", () => { + // todo: post new message + return "aa"; + }) + .get("/output/:sha256", () => { + // todo: get output + return "aa"; + }) + .post("/share", () => { + // todo: share conversation + return "aa"; + }) + .post("/stop-generating", () => { + // todo: stop generating + return "aa"; + }) + .group( + "messages/:messageId", + { + params: t.Object({ + id: t.String(), + messageId: t.Optional(t.String()), + }), + }, + (app) => { + return app + .get("/", () => { + // todo: get message + return "aa"; + }) + .delete("/", () => { + // todo: delete message + return "aa"; + }) + .get("/prompt", () => { + // todo: get message prompt + return "aa"; + }) + .post("/vote", () => { + // todo: vote on message + return "aa"; + }); + } + ); + } + ); +}); diff --git a/src/lib/server/api/routes/groups/tools.ts b/src/lib/server/api/routes/groups/tools.ts new file mode 100644 index 00000000000..7075efa14e5 --- /dev/null +++ b/src/lib/server/api/routes/groups/tools.ts @@ -0,0 +1,48 @@ +import { Elysia } from "elysia"; +import { authPlugin } from "$lib/server/api/authPlugin"; + +export const toolGroup = new Elysia().use(authPlugin).group("/tools", (app) => { + return app + .get("/", () => { + // todo: get tools + return "aa"; + }) + .get("/search", () => { + // todo: search tools + return "aa"; + }) + .group("/:id", (app) => { + return app + .get("/", () => { + // todo: get tool + return "aa"; + }) + .post("/", () => { + // todo: post new tool + return "aa"; + }) + .group("/:toolId", (app) => { + return app + .get("/", () => { + // todo: get tool + return "aa"; + }) + .patch("/", () => { + // todo: patch tool + return "aa"; + }) + .delete("/", () => { + // todo: delete tool + return "aa"; + }) + .post("/report", () => { + // todo: report tool + return "aa"; + }) + .patch("/review", () => { + // todo: review tool + return "aa"; + }); + }); + }); +}); diff --git a/src/lib/server/api/routes/groups/user.ts b/src/lib/server/api/routes/groups/user.ts new file mode 100644 index 00000000000..f059fa1df3a --- /dev/null +++ b/src/lib/server/api/routes/groups/user.ts @@ -0,0 +1,36 @@ +import { Elysia } from "elysia"; +import { authPlugin } from "$lib/server/api/authPlugin"; + +export const userGroup = new Elysia() + .use(authPlugin) + .post("/login", () => { + // todo: login + return "aa"; + }) + .get("/login/callback", () => { + // todo: login callback + return "aa"; + }) + .post("/logout", () => { + // todo: logout + return "aa"; + }) + .group("/user", (app) => { + return app + .get("/", () => { + // todo: get user + return "aa"; + }) + .get("/settings", () => { + // todo: get user settings + return "aa"; + }) + .patch("/settings", () => { + // todo: patch user settings + return "aa"; + }) + .get("/assistants", () => { + // todo: get user assistants + return "aa"; + }); + }); diff --git a/src/lib/server/api/server.ts b/src/lib/server/api/server.ts new file mode 100644 index 00000000000..a5d6f6c762f --- /dev/null +++ b/src/lib/server/api/server.ts @@ -0,0 +1,57 @@ +import { Elysia } from "elysia"; +import { base } from "$app/paths"; +import { authPlugin } from "$lib/server/api/authPlugin"; +import { conversationGroup } from "$lib/server/api/routes/groups/conversations"; +import { assistantGroup } from "$lib/server/api/routes/groups/assistants"; +import { userGroup } from "$lib/server/api/routes/groups/user"; +import { toolGroup } from "$lib/server/api/routes/groups/tools"; +import { swagger } from "@elysiajs/swagger"; +import { models } from "$lib/server/models"; + +const prefix = `${base}/api/v2` as unknown as ""; + +export const app = new Elysia({ prefix }) + .use( + swagger({ + documentation: { + info: { + title: "Elysia Documentation", + version: "1.0.0", + }, + }, + provider: "swagger-ui", + }) + ) + .use(authPlugin) + .use(conversationGroup) + .use(toolGroup) + .use(assistantGroup) + .use(userGroup) + .get("/models", () => { + return models + .filter((m) => m.unlisted == false) + .map((model) => ({ + id: model.id, + name: model.name, + websiteUrl: model.websiteUrl ?? "https://huggingface.co", + modelUrl: model.modelUrl ?? "https://huggingface.co", + tokenizer: model.tokenizer, + datasetName: model.datasetName, + datasetUrl: model.datasetUrl, + displayName: model.displayName, + description: model.description ?? "", + logoUrl: model.logoUrl, + promptExamples: model.promptExamples ?? [], + preprompt: model.preprompt ?? "", + multimodal: model.multimodal ?? false, + unlisted: model.unlisted ?? false, + tools: model.tools ?? false, + hasInferenceAPI: model.hasInferenceAPI ?? false, + })); + }) + .get("/spaces-config", () => { + // todo: get spaces config + return; + }); + +export type App = typeof app; diff --git a/src/lib/server/auth.ts b/src/lib/server/auth.ts index 20baedc88c8..c935e869b4b 100644 --- a/src/lib/server/auth.ts +++ b/src/lib/server/auth.ts @@ -189,7 +189,10 @@ export async function authenticateRequest( headers: HeaderRecord, cookie: CookieRecord, isApi?: boolean -) { +): Promise { + // once the entire API has been moved to elysia + // we can move this function to authPlugin.ts + // and get rid of the isApi && type: "svelte" options const token = cookie.type === "elysia" ? cookie.value[env.COOKIE_NAME].value @@ -229,7 +232,7 @@ export async function authenticateRequest( secretSessionId = token; sessionId = await sha256(token); const user = await findUser(sessionId); - return { user, sessionId, secretSessionId }; + return { user: user ?? undefined, sessionId, secretSessionId }; } if (isApi) { @@ -272,7 +275,11 @@ export async function authenticateRequest( updatedAt: new Date(), }); - return { user, sessionId, secretSessionId }; + return { + user, + sessionId, + secretSessionId, + }; } } @@ -284,5 +291,5 @@ export async function authenticateRequest( throw new Error("Session ID collision"); } - return { user: null, sessionId, secretSessionId }; + return { user: undefined, sessionId, secretSessionId }; } diff --git a/src/lib/utils/api.ts b/src/lib/utils/api.ts new file mode 100644 index 00000000000..ec5cced9cb4 --- /dev/null +++ b/src/lib/utils/api.ts @@ -0,0 +1,12 @@ +import type { App } from "$lib/server/api/server"; +import { edenFetch } from "@elysiajs/eden"; + +type Fetch = typeof fetch; + +export function useEdenFetch({ fetch }: { fetch: Fetch }) { + const app = edenFetch("http://localhost:5173/chat/api/v2", { + fetcher: fetch, + }); + + return app; +} diff --git a/src/routes/api/v2/[...slugs]/+server.ts b/src/routes/api/v2/[...slugs]/+server.ts new file mode 100644 index 00000000000..23f2c81be38 --- /dev/null +++ b/src/routes/api/v2/[...slugs]/+server.ts @@ -0,0 +1,9 @@ +import { app } from "$lib/server/api/server"; + +type RequestHandler = (v: { request: Request; locals: App.Locals }) => Response | Promise; + +export const GET: RequestHandler = ({ request }) => app.handle(request); +export const POST: RequestHandler = ({ request }) => app.handle(request); +export const PUT: RequestHandler = ({ request }) => app.handle(request); +export const PATCH: RequestHandler = ({ request }) => app.handle(request); +export const DELETE: RequestHandler = ({ request }) => app.handle(request); From 46eec827c1066be25fd3921752335a5459c24a33 Mon Sep 17 00:00:00 2001 From: Nathan Sarrazin Date: Thu, 20 Feb 2025 13:09:36 +0000 Subject: [PATCH 03/34] feat: replace conv/[id] load function with universal --- src/routes/conversation/[id]/+page.server.ts | 68 -------------------- src/routes/conversation/[id]/+page.ts | 21 ++++++ 2 files changed, 21 insertions(+), 68 deletions(-) delete mode 100644 src/routes/conversation/[id]/+page.server.ts create mode 100644 src/routes/conversation/[id]/+page.ts diff --git a/src/routes/conversation/[id]/+page.server.ts b/src/routes/conversation/[id]/+page.server.ts deleted file mode 100644 index bd431a07a63..00000000000 --- a/src/routes/conversation/[id]/+page.server.ts +++ /dev/null @@ -1,68 +0,0 @@ -import { collections } from "$lib/server/database"; -import { ObjectId } from "mongodb"; -import { error } from "@sveltejs/kit"; -import { authCondition } from "$lib/server/auth"; -import { UrlDependency } from "$lib/types/UrlDependency"; -import { convertLegacyConversation } from "$lib/utils/tree/convertLegacyConversation.js"; - -export const load = async ({ params, depends, locals }) => { - let conversation; - let shared = false; - - // if the conver - if (params.id.length === 7) { - // shared link of length 7 - conversation = await collections.sharedConversations.findOne({ - _id: params.id, - }); - shared = true; - - if (!conversation) { - error(404, "Conversation not found"); - } - } else { - // todo: add validation on params.id - conversation = await collections.conversations.findOne({ - _id: new ObjectId(params.id), - ...authCondition(locals), - }); - - depends(UrlDependency.Conversation); - - if (!conversation) { - const conversationExists = - (await collections.conversations.countDocuments({ - _id: new ObjectId(params.id), - })) !== 0; - - if (conversationExists) { - error( - 403, - "You don't have access to this conversation. If someone gave you this link, ask them to use the 'share' feature instead." - ); - } - - error(404, "Conversation not found."); - } - } - - const convertedConv = { ...conversation, ...convertLegacyConversation(conversation) }; - - return { - messages: convertedConv.messages, - title: convertedConv.title, - model: convertedConv.model, - preprompt: convertedConv.preprompt, - rootMessageId: convertedConv.rootMessageId, - assistant: convertedConv.assistantId - ? JSON.parse( - JSON.stringify( - await collections.assistants.findOne({ - _id: new ObjectId(convertedConv.assistantId), - }) - ) - ) - : null, - shared, - }; -}; diff --git a/src/routes/conversation/[id]/+page.ts b/src/routes/conversation/[id]/+page.ts new file mode 100644 index 00000000000..2e90f3d4c97 --- /dev/null +++ b/src/routes/conversation/[id]/+page.ts @@ -0,0 +1,21 @@ +import type { GETConversationResponse } from "$lib/server/api/routes/groups/conversations.js"; +import { UrlDependency } from "$lib/types/UrlDependency"; +import { useEdenFetch } from "$lib/utils/api.js"; +import { error } from "@sveltejs/kit"; +export const load = async ({ params, depends, fetch }) => { + depends(UrlDependency.Conversation); + + const edenFetch = useEdenFetch({ fetch }); + + const { data, error: e } = await edenFetch("/conversations/:id", { + params: { + id: params.id, + }, + }); + + if (e) { + error(e.status, e.message); + } + + return data as GETConversationResponse; +}; From 43eb901bdbae93b93c0f4323d0429fae4a61aaa8 Mon Sep 17 00:00:00 2001 From: Nathan Sarrazin Date: Thu, 20 Feb 2025 14:17:43 +0000 Subject: [PATCH 04/34] fix: delete v1 catchall --- src/routes/api/[...slugs]/+server.ts | 26 -------------------------- 1 file changed, 26 deletions(-) delete mode 100644 src/routes/api/[...slugs]/+server.ts diff --git a/src/routes/api/[...slugs]/+server.ts b/src/routes/api/[...slugs]/+server.ts deleted file mode 100644 index 11c55872577..00000000000 --- a/src/routes/api/[...slugs]/+server.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { Elysia } from "elysia"; -import { base } from "$app/paths"; -import { authenticateRequest } from "$lib/server/auth"; - -const app = new Elysia({ prefix: `${base}/api` }) - .derive(async ({ headers, cookie }) => ({ - locals: await authenticateRequest( - { type: "elysia", value: headers }, - { type: "elysia", value: cookie }, - true - ), - })) - .get("/foo", ({ locals }) => { - return locals.user?._id; - }) - .get("/bar", ({ locals }) => { - return locals.user?.name; - }); - -type RequestHandler = (v: { request: Request; locals: App.Locals }) => Response | Promise; - -export const GET: RequestHandler = ({ request }) => app.handle(request); -export const POST: RequestHandler = ({ request }) => app.handle(request); -export const PUT: RequestHandler = ({ request }) => app.handle(request); -export const PATCH: RequestHandler = ({ request }) => app.handle(request); -export const DELETE: RequestHandler = ({ request }) => app.handle(request); From 8df50a209d9913bab4d64c4385f53f1f0c6788b0 Mon Sep 17 00:00:00 2001 From: Nathan Sarrazin Date: Sun, 23 Feb 2025 22:59:41 +0000 Subject: [PATCH 05/34] wip --- .../chat/AssistantIntroduction.svelte | 4 +- src/lib/components/chat/ChatInput.svelte | 3 +- src/lib/components/chat/ChatWindow.svelte | 3 +- .../server/api/routes/groups/assistants.ts | 55 +++++++++++++++---- .../server/api/routes/groups/conversations.ts | 15 +++-- src/lib/server/api/server.ts | 3 +- src/lib/types/UrlDependency.ts | 2 +- src/lib/utils/serialize.ts | 13 +++++ src/routes/+layout.server.ts | 3 +- src/routes/+page.svelte | 4 +- .../api/assistant/[id]/subscribe/+server.ts | 1 + .../assistant/[assistantId]/+page.server.ts | 42 -------------- src/routes/assistant/[assistantId]/+page.ts | 33 +++++++++++ src/routes/assistants/+page.server.ts | 3 +- src/routes/tools/+page.server.ts | 3 +- src/routes/tools/+page.svelte | 9 ++- 16 files changed, 120 insertions(+), 76 deletions(-) create mode 100644 src/lib/utils/serialize.ts delete mode 100644 src/routes/assistant/[assistantId]/+page.server.ts create mode 100644 src/routes/assistant/[assistantId]/+page.ts diff --git a/src/lib/components/chat/AssistantIntroduction.svelte b/src/lib/components/chat/AssistantIntroduction.svelte index f84188dbcf1..00cbca98e03 100644 --- a/src/lib/components/chat/AssistantIntroduction.svelte +++ b/src/lib/components/chat/AssistantIntroduction.svelte @@ -17,11 +17,11 @@ import { share } from "$lib/utils/share"; import { env as envPublic } from "$env/dynamic/public"; import { page } from "$app/state"; - + import type { Serialize } from "$lib/utils/serialize"; interface Props { models: Model[]; assistant: Pick< - Assistant, + Serialize, | "avatar" | "name" | "rag" diff --git a/src/lib/components/chat/ChatInput.svelte b/src/lib/components/chat/ChatInput.svelte index bf616351128..46b12093251 100644 --- a/src/lib/components/chat/ChatInput.svelte +++ b/src/lib/components/chat/ChatInput.svelte @@ -24,6 +24,7 @@ import { captureScreen } from "$lib/utils/screenshot"; import IconScreenshot from "../icons/IconScreenshot.svelte"; import { loginModalOpen } from "$lib/stores/loginModal"; + import type { Serialize } from "$lib/utils/serialize"; interface Props { files?: File[]; @@ -32,7 +33,7 @@ placeholder?: string; loading?: boolean; disabled?: boolean; - assistant?: Assistant | undefined; + assistant?: Serialize | undefined; modelHasTools?: boolean; modelIsMultimodal?: boolean; children?: import("svelte").Snippet; diff --git a/src/lib/components/chat/ChatWindow.svelte b/src/lib/components/chat/ChatWindow.svelte index b16a9c1a31e..b56bc2d2a2b 100644 --- a/src/lib/components/chat/ChatWindow.svelte +++ b/src/lib/components/chat/ChatWindow.svelte @@ -37,6 +37,7 @@ import { cubicInOut } from "svelte/easing"; import type { ToolFront } from "$lib/types/Tool"; import { loginModalOpen } from "$lib/stores/loginModal"; + import type { Serialize } from "$lib/utils/serialize"; interface Props { messages?: Message[]; @@ -46,7 +47,7 @@ shared?: boolean; currentModel: Model; models: Model[]; - assistant?: Assistant | undefined; + assistant?: Serialize | undefined; preprompt?: string | undefined; files?: File[]; } diff --git a/src/lib/server/api/routes/groups/assistants.ts b/src/lib/server/api/routes/groups/assistants.ts index 6a50fd49a91..832fec95480 100644 --- a/src/lib/server/api/routes/groups/assistants.ts +++ b/src/lib/server/api/routes/groups/assistants.ts @@ -1,5 +1,9 @@ import { Elysia } from "elysia"; import { authPlugin } from "$lib/server/api/authPlugin"; +import { collections } from "$lib/server/database"; +import { ObjectId } from "mongodb"; +import { authCondition } from "$lib/server/auth"; +import { jsonSerialize } from "$lib/utils/serialize"; export const assistantGroup = new Elysia().use(authPlugin).group("/assistants", (app) => { return app @@ -13,11 +17,21 @@ export const assistantGroup = new Elysia().use(authPlugin).group("/assistants", }) .group("/:id", (app) => { return app - .get("/", () => { - // todo: get assistant - return "aa"; + .derive(async ({ params, error }) => { + const assistant = await collections.assistants.findOne({ + _id: new ObjectId(params.id), + }); + + if (!assistant) { + return error(404, "Assistant not found"); + } + + return { assistant }; }) - .patch("/", () => { + .get("", ({ assistant }) => { + return jsonSerialize(assistant); + }) + .patch("", () => { // todo: patch assistant return "aa"; }) @@ -33,13 +47,34 @@ export const assistantGroup = new Elysia().use(authPlugin).group("/assistants", // todo: review assistant return "aa"; }) - .post("/subscribe", () => { - // todo: subscribe to assistant - return "aa"; + .post("/subscribe", async ({ locals, assistant }) => { + const result = await collections.settings.updateOne(authCondition(locals), { + $addToSet: { assistants: assistant._id }, + $set: { activeModel: assistant._id.toString() }, + }); + + if (result.modifiedCount > 0) { + await collections.assistants.updateOne( + { _id: assistant._id }, + { $inc: { userCount: 1 } } + ); + } + + return new Response("Assistant subscribed", { status: 200 }); }) - .delete("/subscribe", () => { - // todo: unsubscribe from assistant - return "aa"; + .delete("/subscribe", async ({ locals, assistant }) => { + const result = await collections.settings.updateOne(authCondition(locals), { + $pull: { assistants: assistant._id }, + }); + + if (result.modifiedCount > 0) { + await collections.assistants.updateOne( + { _id: assistant._id }, + { $inc: { userCount: -1 } } + ); + } + + return new Response("Assistant unsubscribed", { status: 200 }); }); }); }); diff --git a/src/lib/server/api/routes/groups/conversations.ts b/src/lib/server/api/routes/groups/conversations.ts index ef87d22b5f4..e9302052d47 100644 --- a/src/lib/server/api/routes/groups/conversations.ts +++ b/src/lib/server/api/routes/groups/conversations.ts @@ -7,14 +7,15 @@ import { models } from "$lib/server/models"; import { convertLegacyConversation } from "$lib/utils/tree/convertLegacyConversation"; import type { Conversation } from "$lib/types/Conversation"; import type { Assistant } from "$lib/types/Assistant"; - +import type { Serialize } from "$lib/utils/serialize"; +import { jsonSerialize } from "$lib/utils/serialize"; export type GETConversationResponse = Pick< Conversation, "messages" | "title" | "model" | "preprompt" | "rootMessageId" | "updatedAt" | "assistantId" > & { shared: boolean; modelTools: boolean; - assistant: Assistant | undefined; + assistant: Serialize | undefined; id: string; modelId: Conversation["model"]; }; @@ -98,12 +99,10 @@ export const conversationGroup = new Elysia().use(authPlugin).group("/conversati preprompt: conversation.preprompt, rootMessageId: conversation.rootMessageId, assistant: conversation.assistantId - ? JSON.parse( - JSON.stringify( - await collections.assistants.findOne({ - _id: new ObjectId(conversation.assistantId), - }) - ) + ? jsonSerialize( + (await collections.assistants.findOne({ + _id: new ObjectId(conversation.assistantId), + })) ?? undefined ) : undefined, id: conversation._id.toString(), diff --git a/src/lib/server/api/server.ts b/src/lib/server/api/server.ts index a5d6f6c762f..ac69883f49e 100644 --- a/src/lib/server/api/server.ts +++ b/src/lib/server/api/server.ts @@ -7,10 +7,11 @@ import { userGroup } from "$lib/server/api/routes/groups/user"; import { toolGroup } from "$lib/server/api/routes/groups/tools"; import { swagger } from "@elysiajs/swagger"; import { models } from "$lib/server/models"; +import { node } from "@elysiajs/node"; const prefix = `${base}/api/v2` as unknown as ""; -export const app = new Elysia({ prefix }) +export const app = new Elysia({ prefix, adapter: node() }) .use( swagger({ documentation: { diff --git a/src/lib/types/UrlDependency.ts b/src/lib/types/UrlDependency.ts index dca26f87f49..c8b901f2ef5 100644 --- a/src/lib/types/UrlDependency.ts +++ b/src/lib/types/UrlDependency.ts @@ -1,5 +1,5 @@ /* eslint-disable no-shadow */ export enum UrlDependency { ConversationList = "conversation:list", - Conversation = "conversation", + Conversation = "conversation:id", } diff --git a/src/lib/utils/serialize.ts b/src/lib/utils/serialize.ts new file mode 100644 index 00000000000..20e5250f2cc --- /dev/null +++ b/src/lib/utils/serialize.ts @@ -0,0 +1,13 @@ +import { ObjectId } from "mongodb"; + +export type Serialize = T extends ObjectId | Date + ? string + : T extends Array + ? Array> + : T extends object + ? { [K in keyof T]: Serialize } + : T; + +export function jsonSerialize(data: T): Serialize { + return JSON.parse(JSON.stringify(data)) as Serialize; +} diff --git a/src/routes/+layout.server.ts b/src/routes/+layout.server.ts index f2caa374834..c54dea29cdb 100644 --- a/src/routes/+layout.server.ts +++ b/src/routes/+layout.server.ts @@ -13,6 +13,7 @@ import { MetricsServer } from "$lib/server/metrics"; import type { ToolFront, ToolInputFile } from "$lib/types/Tool"; import { ReviewStatus } from "$lib/types/Review"; import { base } from "$app/paths"; +import { jsonSerialize } from "../lib/utils/serialize"; export const load: LayoutServerLoad = async ({ locals, depends, fetch }) => { depends(UrlDependency.ConversationList); @@ -273,7 +274,7 @@ export const load: LayoutServerLoad = async ({ locals, depends, fetch }) => { isAdmin: locals.user.isAdmin ?? false, isEarlyAccess: locals.user.isEarlyAccess ?? false, }, - assistant: assistant ? JSON.parse(JSON.stringify(assistant)) : null, + assistant: assistant ? jsonSerialize(assistant) : undefined, enableAssistants, enableAssistantsRAG: env.ENABLE_ASSISTANTS_RAG === "true", enableCommunityTools: env.COMMUNITY_TOOLS === "true", diff --git a/src/routes/+page.svelte b/src/routes/+page.svelte index 2e1f106febd..42c3ffdc9c5 100644 --- a/src/routes/+page.svelte +++ b/src/routes/+page.svelte @@ -30,8 +30,8 @@ if (validModels.includes($settings.activeModel)) { model = $settings.activeModel; } else { - if (validModels.includes(data.assistant?.modelId)) { - model = data.assistant?.modelId; + if (data.assistant?.modelId && validModels.includes(data.assistant.modelId)) { + model = data.assistant.modelId; } else { model = data.models[0].id; } diff --git a/src/routes/api/assistant/[id]/subscribe/+server.ts b/src/routes/api/assistant/[id]/subscribe/+server.ts index fe67acf644d..ffc7ec60265 100644 --- a/src/routes/api/assistant/[id]/subscribe/+server.ts +++ b/src/routes/api/assistant/[id]/subscribe/+server.ts @@ -23,6 +23,7 @@ export async function POST({ params, locals }) { const result = await collections.settings.updateOne(authCondition(locals), { $addToSet: { assistants: assistant._id }, + $set: { activeModel: assistant._id.toString() }, }); // reduce count only if push succeeded diff --git a/src/routes/assistant/[assistantId]/+page.server.ts b/src/routes/assistant/[assistantId]/+page.server.ts deleted file mode 100644 index 072b3516c8e..00000000000 --- a/src/routes/assistant/[assistantId]/+page.server.ts +++ /dev/null @@ -1,42 +0,0 @@ -import { base } from "$app/paths"; -import { collections } from "$lib/server/database"; -import { redirect } from "@sveltejs/kit"; -import { ObjectId } from "mongodb"; -import { authCondition } from "$lib/server/auth.js"; - -export async function load({ params, locals }) { - try { - const assistant = await collections.assistants.findOne({ - _id: new ObjectId(params.assistantId), - }); - - if (!assistant) { - redirect(302, `${base}`); - } - - if (locals.user?._id ?? locals.sessionId) { - await collections.settings.updateOne( - authCondition(locals), - { - $set: { - activeModel: assistant._id.toString(), - updatedAt: new Date(), - }, - $push: { assistants: assistant._id }, - $setOnInsert: { - createdAt: new Date(), - }, - }, - { - upsert: true, - } - ); - } - - return { - assistant: JSON.parse(JSON.stringify(assistant)), - }; - } catch { - redirect(302, `${base}`); - } -} diff --git a/src/routes/assistant/[assistantId]/+page.ts b/src/routes/assistant/[assistantId]/+page.ts new file mode 100644 index 00000000000..78540097aed --- /dev/null +++ b/src/routes/assistant/[assistantId]/+page.ts @@ -0,0 +1,33 @@ +import { useEdenFetch } from "$lib/utils/api"; +import { error } from "@sveltejs/kit"; +import type { Assistant } from "$lib/types/Assistant"; +import type { Serialize } from "$lib/utils/serialize"; + +export async function load({ fetch, params }) { + const edenFetch = useEdenFetch({ fetch }); + + const { data, error: e } = await edenFetch("/assistants/:id", { + method: "GET", + params: { + id: params.assistantId, + }, + }); + + if (e) { + error(e.status, e.message); + } + + const { error: subscribeError } = await edenFetch("/assistants/:id/subscribe", { + method: "POST", + params: { + id: params.assistantId, + }, + }); + + if (subscribeError) { + console.error(subscribeError); + error(subscribeError.status, subscribeError.message); + } + + return { assistant: data as Serialize }; +} diff --git a/src/routes/assistants/+page.server.ts b/src/routes/assistants/+page.server.ts index cc5ab3ab195..d74701e63b6 100644 --- a/src/routes/assistants/+page.server.ts +++ b/src/routes/assistants/+page.server.ts @@ -7,6 +7,7 @@ import { generateQueryTokens } from "$lib/utils/searchTokens.js"; import { error, redirect } from "@sveltejs/kit"; import type { Filter } from "mongodb"; import { ReviewStatus } from "$lib/types/Review"; +import { jsonSerialize } from "$lib/utils/serialize"; const NUM_PER_PAGE = 24; export const load = async ({ url, locals }) => { @@ -75,7 +76,7 @@ export const load = async ({ url, locals }) => { .assistants.countDocuments(filter); return { - assistants: JSON.parse(JSON.stringify(assistants)) as Array, + assistants: jsonSerialize(assistants), selectedModel: modelId ?? "", numTotalItems, numItemsPerPage: NUM_PER_PAGE, diff --git a/src/routes/tools/+page.server.ts b/src/routes/tools/+page.server.ts index 7aca65796ec..b704a889930 100644 --- a/src/routes/tools/+page.server.ts +++ b/src/routes/tools/+page.server.ts @@ -7,6 +7,7 @@ import { ReviewStatus } from "$lib/types/Review"; import type { CommunityToolDB } from "$lib/types/Tool.js"; import type { User } from "$lib/types/User.js"; import { generateQueryTokens, generateSearchTokens } from "$lib/utils/searchTokens.js"; +import { jsonSerialize } from "$lib/utils/serialize"; import { error } from "@sveltejs/kit"; import { ObjectId, type Filter } from "mongodb"; @@ -89,7 +90,7 @@ export const load = async ({ url, locals }) => { toolFromConfigs.length; return { - tools: JSON.parse(JSON.stringify(tools)) as CommunityToolDB[], + tools: jsonSerialize(tools), numTotalItems, numItemsPerPage: NUM_PER_PAGE, query, diff --git a/src/routes/tools/+page.svelte b/src/routes/tools/+page.svelte index 920fb10dd79..16e98e49570 100644 --- a/src/routes/tools/+page.svelte +++ b/src/routes/tools/+page.svelte @@ -266,15 +266,14 @@
{#each tools as tool} {@const isActive = (page.data.settings?.tools ?? []).includes(tool._id.toString())} - {@const isOfficial = !tool.createdByName} + {@const isOfficial = tool.type === "config"}
goto(`${base}/tools/${tool._id.toString()}`)} onkeydown={(e) => e.key === "Enter" && goto(`${base}/tools/${tool._id.toString()}`)} role="button" tabindex="0" - class="relative flex flex-row items-center gap-4 overflow-hidden text-balance rounded-xl border bg-gray-50/50 px-4 text-center shadow hover:bg-gray-50 hover:shadow-inner dark:bg-gray-950/20 dark:hover:bg-gray-950/40 max-sm:px-4 sm:h-24 {!( - tool.review === ReviewStatus.APPROVED - ) && !isOfficial + class="relative flex flex-row items-center gap-4 overflow-hidden text-balance rounded-xl border bg-gray-50/50 px-4 text-center shadow hover:bg-gray-50 hover:shadow-inner dark:bg-gray-950/20 dark:hover:bg-gray-950/40 max-sm:px-4 sm:h-24 {!isOfficial && + tool.review !== ReviewStatus.APPROVED ? ' border-red-500/30' : 'dark:border-gray-800/70'}" class:!border-blue-600={isActive} @@ -302,7 +301,7 @@ {tool.description}

- {#if !isOfficial} + {#if !isOfficial && tool.type === "community"}

Added by Date: Mon, 24 Feb 2025 13:29:27 +0000 Subject: [PATCH 06/34] fix: response type --- src/lib/server/api/routes/groups/assistants.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/lib/server/api/routes/groups/assistants.ts b/src/lib/server/api/routes/groups/assistants.ts index 832fec95480..e4ec1085c4d 100644 --- a/src/lib/server/api/routes/groups/assistants.ts +++ b/src/lib/server/api/routes/groups/assistants.ts @@ -60,7 +60,7 @@ export const assistantGroup = new Elysia().use(authPlugin).group("/assistants", ); } - return new Response("Assistant subscribed", { status: 200 }); + return { message: "Assistant subscribed" }; }) .delete("/subscribe", async ({ locals, assistant }) => { const result = await collections.settings.updateOne(authCondition(locals), { @@ -74,7 +74,7 @@ export const assistantGroup = new Elysia().use(authPlugin).group("/assistants", ); } - return new Response("Assistant unsubscribed", { status: 200 }); + return { message: "Assistant unsubscribed" }; }); }); }); From e87e88aa0fd899e2b85e918658aba5a6a23dd0cb Mon Sep 17 00:00:00 2001 From: Nathan Sarrazin Date: Fri, 28 Feb 2025 09:26:50 +0000 Subject: [PATCH 07/34] fix: add cors --- package-lock.json | 11 +++++++++++ package.json | 3 ++- src/lib/server/api/server.ts | 5 +++-- 3 files changed, 16 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 81b74cd65ae..28bd72e0d06 100644 --- a/package-lock.json +++ b/package-lock.json @@ -58,6 +58,7 @@ "zod": "^3.22.3" }, "devDependencies": { + "@elysiajs/cors": "^1.2.0", "@elysiajs/eden": "^1.2.0", "@elysiajs/node": "^1.2.5", "@faker-js/faker": "^8.4.1", @@ -1433,6 +1434,16 @@ "@jridgewell/sourcemap-codec": "^1.4.10" } }, + "node_modules/@elysiajs/cors": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@elysiajs/cors/-/cors-1.2.0.tgz", + "integrity": "sha512-qsJwDAg6WfdQRMfj6uSMcDPSpXvm/zQFeAX1uuJXhIgazH8itSfcDxcH9pMuXVRX1yQNi2pPwNQLJmAcw5mzvw==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "elysia": ">= 1.2.0" + } + }, "node_modules/@elysiajs/eden": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/@elysiajs/eden/-/eden-1.2.0.tgz", diff --git a/package.json b/package.json index 1ddeae48431..88f5a43bac0 100644 --- a/package.json +++ b/package.json @@ -17,6 +17,7 @@ "prepare": "husky" }, "devDependencies": { + "@elysiajs/cors": "^1.2.0", "@elysiajs/eden": "^1.2.0", "@elysiajs/node": "^1.2.5", "@faker-js/faker": "^8.4.1", @@ -40,8 +41,8 @@ "@types/uuid": "^9.0.8", "@typescript-eslint/eslint-plugin": "^6.x", "@typescript-eslint/parser": "^6.x", - "elysia": "^1.2.12", "dompurify": "^3.2.4", + "elysia": "^1.2.12", "eslint": "^8.28.0", "eslint-config-prettier": "^8.5.0", "eslint-plugin-svelte": "^2.45.1", diff --git a/src/lib/server/api/server.ts b/src/lib/server/api/server.ts index ac69883f49e..fa3d264b056 100644 --- a/src/lib/server/api/server.ts +++ b/src/lib/server/api/server.ts @@ -7,11 +7,11 @@ import { userGroup } from "$lib/server/api/routes/groups/user"; import { toolGroup } from "$lib/server/api/routes/groups/tools"; import { swagger } from "@elysiajs/swagger"; import { models } from "$lib/server/models"; -import { node } from "@elysiajs/node"; +import { cors } from "@elysiajs/cors"; const prefix = `${base}/api/v2` as unknown as ""; -export const app = new Elysia({ prefix, adapter: node() }) +export const app = new Elysia({ prefix }) .use( swagger({ documentation: { @@ -23,6 +23,7 @@ export const app = new Elysia({ prefix, adapter: node() }) provider: "swagger-ui", }) ) + .use(cors()) .use(authPlugin) .use(conversationGroup) .use(toolGroup) From fbeeb84cbce96a91aac6048a16e9ed5e2ddf1f01 Mon Sep 17 00:00:00 2001 From: Nathan Sarrazin Date: Fri, 28 Feb 2025 09:27:17 +0000 Subject: [PATCH 08/34] feat: more routes in tools & assistants --- .../server/api/routes/groups/assistants.ts | 3 +- src/lib/server/api/routes/groups/tools.ts | 49 +++++++++++++++++-- 2 files changed, 47 insertions(+), 5 deletions(-) diff --git a/src/lib/server/api/routes/groups/assistants.ts b/src/lib/server/api/routes/groups/assistants.ts index e4ec1085c4d..7cf2520fd76 100644 --- a/src/lib/server/api/routes/groups/assistants.ts +++ b/src/lib/server/api/routes/groups/assistants.ts @@ -3,7 +3,6 @@ import { authPlugin } from "$lib/server/api/authPlugin"; import { collections } from "$lib/server/database"; import { ObjectId } from "mongodb"; import { authCondition } from "$lib/server/auth"; -import { jsonSerialize } from "$lib/utils/serialize"; export const assistantGroup = new Elysia().use(authPlugin).group("/assistants", (app) => { return app @@ -29,7 +28,7 @@ export const assistantGroup = new Elysia().use(authPlugin).group("/assistants", return { assistant }; }) .get("", ({ assistant }) => { - return jsonSerialize(assistant); + return assistant; }) .patch("", () => { // todo: patch assistant diff --git a/src/lib/server/api/routes/groups/tools.ts b/src/lib/server/api/routes/groups/tools.ts index 7075efa14e5..372d62f4884 100644 --- a/src/lib/server/api/routes/groups/tools.ts +++ b/src/lib/server/api/routes/groups/tools.ts @@ -1,5 +1,9 @@ import { Elysia } from "elysia"; import { authPlugin } from "$lib/server/api/authPlugin"; +import { ReviewStatus } from "$lib/types/Review"; +import { toolFromConfigs } from "$lib/server/tools"; +import { collections } from "$lib/server/database"; +import { ObjectId } from "mongodb"; export const toolGroup = new Elysia().use(authPlugin).group("/tools", (app) => { return app @@ -13,9 +17,48 @@ export const toolGroup = new Elysia().use(authPlugin).group("/tools", (app) => { }) .group("/:id", (app) => { return app - .get("/", () => { - // todo: get tool - return "aa"; + .derive(async ({ params, error, locals }) => { + const tool = await collections.tools.findOne({ _id: new ObjectId(params.id) }); + + if (!tool) { + const tool = toolFromConfigs.find((el) => el._id.toString() === params.id); + if (!tool) { + throw error(404, "Tool not found"); + } else { + return { + tool: { + ...tool, + _id: tool._id.toString(), + call: undefined, + createdById: null, + createdByName: null, + createdByMe: false, + reported: false, + review: ReviewStatus.APPROVED, + }, + }; + } + } else { + const reported = await collections.reports.findOne({ + contentId: tool._id, + object: "tool", + }); + + return { + tool: { + ...tool, + _id: tool._id.toString(), + call: undefined, + createdById: tool.createdById.toString(), + createdByMe: + tool.createdById.toString() === (locals.user?._id ?? locals.sessionId).toString(), + reported: !!reported, + }, + }; + } + }) + .get("", ({ tool }) => { + return tool; }) .post("/", () => { // todo: post new tool From ae1a5a954ff612e0bf92a99110f8da08d402bd60 Mon Sep 17 00:00:00 2001 From: Nathan Sarrazin Date: Fri, 28 Feb 2025 09:27:46 +0000 Subject: [PATCH 09/34] refacto: use normal svelte fetch in `/assistant/[assistantId]` --- src/routes/assistant/[assistantId]/+page.ts | 27 +++++++-------------- 1 file changed, 9 insertions(+), 18 deletions(-) diff --git a/src/routes/assistant/[assistantId]/+page.ts b/src/routes/assistant/[assistantId]/+page.ts index 78540097aed..a72ac388f96 100644 --- a/src/routes/assistant/[assistantId]/+page.ts +++ b/src/routes/assistant/[assistantId]/+page.ts @@ -1,32 +1,23 @@ -import { useEdenFetch } from "$lib/utils/api"; import { error } from "@sveltejs/kit"; import type { Assistant } from "$lib/types/Assistant"; import type { Serialize } from "$lib/utils/serialize"; +import { base } from "$app/paths"; export async function load({ fetch, params }) { - const edenFetch = useEdenFetch({ fetch }); + const r = await fetch(`${base}/api/v2/assistants/${params.assistantId}`); - const { data, error: e } = await edenFetch("/assistants/:id", { - method: "GET", - params: { - id: params.assistantId, - }, - }); - - if (e) { - error(e.status, e.message); + if (!r.ok) { + error(r.status, r.statusText); } - const { error: subscribeError } = await edenFetch("/assistants/:id/subscribe", { + const data = await r.json(); + + const r2 = await fetch(`${base}/api/v2/assistants/${params.assistantId}/subscribe`, { method: "POST", - params: { - id: params.assistantId, - }, }); - if (subscribeError) { - console.error(subscribeError); - error(subscribeError.status, subscribeError.message); + if (!r2.ok) { + error(r2.status, r2.statusText); } return { assistant: data as Serialize }; From 8818fc8d56d999368767099f89e0a5e3126de62e Mon Sep 17 00:00:00 2001 From: Nathan Sarrazin Date: Fri, 28 Feb 2025 09:27:59 +0000 Subject: [PATCH 10/34] refacto: use normal svelte fetch in `conversation/[id]` --- src/routes/conversation/[id]/+page.ts | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/src/routes/conversation/[id]/+page.ts b/src/routes/conversation/[id]/+page.ts index 2e90f3d4c97..54e1ac535e0 100644 --- a/src/routes/conversation/[id]/+page.ts +++ b/src/routes/conversation/[id]/+page.ts @@ -1,21 +1,19 @@ +import { base } from "$app/paths"; import type { GETConversationResponse } from "$lib/server/api/routes/groups/conversations.js"; import { UrlDependency } from "$lib/types/UrlDependency"; -import { useEdenFetch } from "$lib/utils/api.js"; import { error } from "@sveltejs/kit"; + export const load = async ({ params, depends, fetch }) => { depends(UrlDependency.Conversation); - const edenFetch = useEdenFetch({ fetch }); - - const { data, error: e } = await edenFetch("/conversations/:id", { - params: { - id: params.id, - }, - }); + const r = await fetch(`${base}/api/v2/conversations/${params.id}`); - if (e) { - error(e.status, e.message); + if (!r.ok) { + console.log({ r }); + error(r.status, "Failed to fetch conversation"); } + const data = await r.json(); + return data as GETConversationResponse; }; From ac607228930c7fad83bdfcd76c4f4820beeb5fe8 Mon Sep 17 00:00:00 2001 From: Nathan Sarrazin Date: Fri, 28 Feb 2025 09:28:19 +0000 Subject: [PATCH 11/34] feat: use universal load function for `tools/[toolId]` --- src/routes/tools/[toolId]/+layout.server.ts | 46 --------------------- src/routes/tools/[toolId]/+layout.ts | 26 ++++++++++++ 2 files changed, 26 insertions(+), 46 deletions(-) delete mode 100644 src/routes/tools/[toolId]/+layout.server.ts create mode 100644 src/routes/tools/[toolId]/+layout.ts diff --git a/src/routes/tools/[toolId]/+layout.server.ts b/src/routes/tools/[toolId]/+layout.server.ts deleted file mode 100644 index 3fa4ed30503..00000000000 --- a/src/routes/tools/[toolId]/+layout.server.ts +++ /dev/null @@ -1,46 +0,0 @@ -import { base } from "$app/paths"; -import { collections } from "$lib/server/database.js"; -import { toolFromConfigs } from "$lib/server/tools/index.js"; -import { ReviewStatus } from "$lib/types/Review.js"; -import { redirect } from "@sveltejs/kit"; -import { ObjectId } from "mongodb"; - -export const load = async ({ params, locals }) => { - const tool = await collections.tools.findOne({ _id: new ObjectId(params.toolId) }); - - if (!tool) { - const tool = toolFromConfigs.find((el) => el._id.toString() === params.toolId); - if (!tool) { - redirect(302, `${base}/tools`); - } - return { - tool: { - ...tool, - _id: tool._id.toString(), - call: undefined, - createdById: null, - createdByName: null, - createdByMe: false, - reported: false, - review: ReviewStatus.APPROVED, - }, - }; - } - - const reported = await collections.reports.findOne({ - contentId: tool._id, - object: "tool", - }); - - return { - tool: { - ...tool, - _id: tool._id.toString(), - call: undefined, - createdById: tool.createdById.toString(), - createdByMe: - tool.createdById.toString() === (locals.user?._id ?? locals.sessionId).toString(), - reported: !!reported, - }, - }; -}; diff --git a/src/routes/tools/[toolId]/+layout.ts b/src/routes/tools/[toolId]/+layout.ts new file mode 100644 index 00000000000..4efd11a8d8d --- /dev/null +++ b/src/routes/tools/[toolId]/+layout.ts @@ -0,0 +1,26 @@ +import { base } from "$app/paths"; +import type { ReviewStatus } from "$lib/types/Review"; +import type { Tool } from "$lib/types/Tool.js"; +import type { Serialize } from "$lib/utils/serialize"; +import { error } from "@sveltejs/kit"; + +export const load = async ({ params, fetch }) => { + const r = await fetch(`${base}/api/v2/tools/${params.toolId}`); + + if (!r.ok) { + throw error(r.status, r.statusText); + } + + const data = await r.json(); + + return { + tool: data as Serialize< + Tool & { + createdById: string | null; + createdByMe: boolean; + reported: boolean; + review: ReviewStatus; + } + >, + }; +}; From f8731b39470cb8fb5365d145570c3ebfdf0444db Mon Sep 17 00:00:00 2001 From: Nathan Sarrazin Date: Tue, 4 Mar 2025 00:33:35 +0000 Subject: [PATCH 12/34] wip: removing more server load function stuff --- src/lib/components/NavMenu.svelte | 2 +- .../server/api/routes/groups/conversations.ts | 63 ++++++- src/lib/server/api/routes/groups/misc.ts | 129 ++++++++++++++ src/lib/server/api/routes/groups/tools.ts | 62 ++++++- src/lib/server/api/routes/groups/user.ts | 167 +++++++++++++++++- src/lib/server/api/server.ts | 30 +--- src/routes/+layout.server.ts | 152 +++------------- .../settings/(nav)/application/+page.svelte | 2 +- 8 files changed, 427 insertions(+), 180 deletions(-) create mode 100644 src/lib/server/api/routes/groups/misc.ts diff --git a/src/lib/components/NavMenu.svelte b/src/lib/components/NavMenu.svelte index 114653e47e6..e0be47f189a 100644 --- a/src/lib/components/NavMenu.svelte +++ b/src/lib/components/NavMenu.svelte @@ -57,7 +57,7 @@ async function handleVisible() { p++; - const newConvs = await fetch(`${base}/api/conversations?p=${p}`) + const newConvs = await fetch(`${base}/api/v2/conversations?p=${p}`) .then((res) => res.json()) .then((convs) => convs.map( diff --git a/src/lib/server/api/routes/groups/conversations.ts b/src/lib/server/api/routes/groups/conversations.ts index e9302052d47..169b54bdec7 100644 --- a/src/lib/server/api/routes/groups/conversations.ts +++ b/src/lib/server/api/routes/groups/conversations.ts @@ -9,6 +9,8 @@ import type { Conversation } from "$lib/types/Conversation"; import type { Assistant } from "$lib/types/Assistant"; import type { Serialize } from "$lib/utils/serialize"; import { jsonSerialize } from "$lib/utils/serialize"; +import { CONV_NUM_PER_PAGE } from "$lib/constants/pagination"; + export type GETConversationResponse = Pick< Conversation, "messages" | "title" | "model" | "preprompt" | "rootMessageId" | "updatedAt" | "assistantId" @@ -22,14 +24,50 @@ export type GETConversationResponse = Pick< export const conversationGroup = new Elysia().use(authPlugin).group("/conversations", (app) => { return app - .get("", () => { - // todo: get conversations - return "aa"; - }) - .post("", () => { - // todo: post new conversation - return "aa"; - }) + .get( + "", + async ({ locals, query }) => { + if (locals.user?._id || locals.sessionId) { + const convs = await collections.conversations + .find({ + ...authCondition(locals), + }) + .project>({ + title: 1, + updatedAt: 1, + model: 1, + assistantId: 1, + }) + .sort({ updatedAt: -1 }) + .skip((query.p ?? 0) * CONV_NUM_PER_PAGE) + .limit(CONV_NUM_PER_PAGE) + .toArray(); + + if (convs.length === 0) { + return Response.json([]); + } + + const res = convs.map((conv) => ({ + _id: conv._id, + id: conv._id, // legacy param iOS + title: conv.title, + updatedAt: conv.updatedAt, + model: conv.model, + modelId: conv.model, // legacy param iOS + assistantId: conv.assistantId, + modelTools: models.find((m) => m.id == conv.model)?.tools ?? false, + })); + return Response.json(res); + } else { + return Response.json({ message: "Must have session cookie" }, { status: 401 }); + } + }, + { + query: t.Object({ + p: t.Optional(t.Number()), + }), + } + ) .group( "/:id", { @@ -117,6 +155,15 @@ export const conversationGroup = new Elysia().use(authPlugin).group("/conversati // todo: post new message return "aa"; }) + .delete("", async ({ locals }) => { + if (locals.user?._id || locals.sessionId) { + const res = await collections.conversations.deleteMany({ + ...authCondition(locals), + }); + return res.deletedCount; + } + return 0; + }) .get("/output/:sha256", () => { // todo: get output return "aa"; diff --git a/src/lib/server/api/routes/groups/misc.ts b/src/lib/server/api/routes/groups/misc.ts new file mode 100644 index 00000000000..19ce402d100 --- /dev/null +++ b/src/lib/server/api/routes/groups/misc.ts @@ -0,0 +1,129 @@ +import { Elysia, env } from "elysia"; +import { authPlugin } from "../../authPlugin"; +import { requiresUser } from "$lib/server/auth"; +import { collections } from "$lib/server/database"; +import { authCondition } from "$lib/server/auth"; +import { models, oldModels, type BackendModel } from "$lib/server/models"; + +export interface FeatureFlags { + searchEnabled: boolean; + enableAssistants: boolean; + enableAssistantsRAG: boolean; + enableCommunityTools: boolean; + loginEnabled: boolean; + loginRequired: boolean; + guestMode: boolean; +} + +export type GETModelsResponse = Array<{ + id: string; + name: string; + websiteUrl?: string; + modelUrl?: string; + tokenizer?: string | { tokenizerUrl: string; tokenizerConfigUrl: string }; + datasetName?: string; + datasetUrl?: string; + displayName: string; + description?: string; + reasoning: boolean; + logoUrl?: string; + promptExamples?: { title: string; prompt: string }[]; + parameters: BackendModel["parameters"]; + preprompt?: string; + multimodal: boolean; + multimodalAcceptedMimetypes?: string[]; + tools: boolean; + unlisted: boolean; + hasInferenceAPI: boolean; +}>; + +export type GETOldModelsResponse = Array<{ + id: string; + name: string; + displayName: string; + transferTo?: string; +}>; + +export const misc = new Elysia() + .use(authPlugin) + .get("/feature-flags", async ({ locals }) => { + let loginRequired = false; + const messagesBeforeLogin = env.MESSAGES_BEFORE_LOGIN ? parseInt(env.MESSAGES_BEFORE_LOGIN) : 0; + const nConversations = await collections.conversations.countDocuments(authCondition(locals)); + + if (requiresUser && !locals.user) { + if (messagesBeforeLogin === 0) { + loginRequired = true; + } else if (nConversations >= messagesBeforeLogin) { + loginRequired = true; + } else { + // get the number of messages where `from === "assistant"` across all conversations. + const totalMessages = + ( + await collections.conversations + .aggregate([ + { $match: { ...authCondition(locals), "messages.from": "assistant" } }, + { $project: { messages: 1 } }, + { $limit: messagesBeforeLogin + 1 }, + { $unwind: "$messages" }, + { $match: { "messages.from": "assistant" } }, + { $count: "messages" }, + ]) + .toArray() + )[0]?.messages ?? 0; + + loginRequired = totalMessages >= messagesBeforeLogin; + } + } + + return { + searchEnabled: !!( + env.SERPAPI_KEY || + env.SERPER_API_KEY || + env.SERPSTACK_API_KEY || + env.SEARCHAPI_KEY || + env.YDC_API_KEY || + env.USE_LOCAL_WEBSEARCH || + env.SEARXNG_QUERY_URL || + env.BING_SUBSCRIPTION_KEY + ), + enableAssistants: env.ENABLE_ASSISTANTS === "true", + enableAssistantsRAG: env.ENABLE_ASSISTANTS_RAG === "true", + enableCommunityTools: env.COMMUNITY_TOOLS === "true", + loginEnabled: requiresUser, // misnomer, this is actually whether the feature is available, not required + loginRequired, + guestMode: requiresUser && messagesBeforeLogin > 0, + } satisfies FeatureFlags; + }) + .get("/models", () => { + return models + .filter((m) => m.unlisted == false) + .map((model) => ({ + id: model.id, + name: model.name, + websiteUrl: model.websiteUrl, + modelUrl: model.modelUrl, + tokenizer: model.tokenizer, + datasetName: model.datasetName, + datasetUrl: model.datasetUrl, + displayName: model.displayName, + description: model.description, + reasoning: !!model.reasoning, + logoUrl: model.logoUrl, + promptExamples: model.promptExamples, + parameters: model.parameters, + preprompt: model.preprompt, + multimodal: model.multimodal, + multimodalAcceptedMimetypes: model.multimodalAcceptedMimetypes, + tools: model.tools, + unlisted: model.unlisted, + hasInferenceAPI: model.hasInferenceAPI, + })) satisfies GETModelsResponse; + }) + .get("/models/old", () => { + return oldModels satisfies GETOldModelsResponse; + }) + .get("/spaces-config", () => { + // todo: get spaces config + return; + }); diff --git a/src/lib/server/api/routes/groups/tools.ts b/src/lib/server/api/routes/groups/tools.ts index 372d62f4884..3cbf045f4dc 100644 --- a/src/lib/server/api/routes/groups/tools.ts +++ b/src/lib/server/api/routes/groups/tools.ts @@ -4,17 +4,73 @@ import { ReviewStatus } from "$lib/types/Review"; import { toolFromConfigs } from "$lib/server/tools"; import { collections } from "$lib/server/database"; import { ObjectId } from "mongodb"; +import type { ToolFront, ToolInputFile } from "$lib/types/Tool"; +import { MetricsServer } from "$lib/server/metrics"; +import { authCondition } from "$lib/server/auth"; + +export type GETToolsResponse = Array; export const toolGroup = new Elysia().use(authPlugin).group("/tools", (app) => { return app - .get("/", () => { - // todo: get tools - return "aa"; + .get("/config", async () => { + const toolUseDuration = (await MetricsServer.getMetrics().tool.toolUseDuration.get()).values; + + return toolFromConfigs + .filter((tool) => !tool?.isHidden) + .map( + (tool) => + ({ + _id: tool._id.toString(), + type: tool.type, + displayName: tool.displayName, + name: tool.name, + description: tool.description, + mimeTypes: (tool.inputs ?? []) + .filter((input): input is ToolInputFile => input.type === "file") + .map((input) => (input as ToolInputFile).mimeTypes) + .flat(), + isOnByDefault: tool.isOnByDefault ?? true, + isLocked: tool.isLocked ?? true, + timeToUseMS: + toolUseDuration.find( + (el) => el.labels.tool === tool._id.toString() && el.labels.quantile === 0.9 + )?.value ?? 15_000, + color: tool.color, + icon: tool.icon, + }) satisfies ToolFront + ); + }) + .get("/active", async ({ locals }) => { + const settings = await collections.settings.findOne(authCondition(locals)); + + if (!settings) { + return []; + } + + const activeCommunityToolIds = settings.tools ?? []; + + const communityTools = await collections.tools + .find({ _id: { $in: activeCommunityToolIds.map((el) => new ObjectId(el)) } }) + .toArray() + .then((tools) => + tools.map((tool) => ({ + ...tool, + isHidden: false, + isOnByDefault: true, + isLocked: true, + })) + ); + + return communityTools; }) .get("/search", () => { // todo: search tools return "aa"; }) + .get("/count", () => { + // return community tool count + return collections.tools.countDocuments({ type: "community", review: ReviewStatus.APPROVED }); + }) .group("/:id", (app) => { return app .derive(async ({ params, error, locals }) => { diff --git a/src/lib/server/api/routes/groups/user.ts b/src/lib/server/api/routes/groups/user.ts index f059fa1df3a..369ada4224a 100644 --- a/src/lib/server/api/routes/groups/user.ts +++ b/src/lib/server/api/routes/groups/user.ts @@ -1,5 +1,36 @@ import { Elysia } from "elysia"; import { authPlugin } from "$lib/server/api/authPlugin"; +import { defaultModel } from "$lib/server/models"; +import { collections } from "$lib/server/database"; +import { authCondition } from "$lib/server/auth"; +import { models, validateModel } from "$lib/server/models"; +import { DEFAULT_SETTINGS, type SettingsEditable } from "$lib/types/Settings"; +import { toolFromConfigs } from "$lib/server/tools"; +import { ObjectId } from "mongodb"; +import { z } from "zod"; + +export type UserGETFront = { + id: string; + username?: string; + avatarUrl?: string; + email?: string; + logoutDisabled?: boolean; + isAdmin: boolean; + isEarlyAccess: boolean; +} | null; + +export type UserGETSettings = { + ethicsModalAccepted: boolean; + ethicsModalAcceptedAt: Date | null; + activeModel: string; + hideEmojiOnSidebar: boolean; + disableStream: boolean; + directPaste: boolean; + shareConversationsWithModelAuthors: boolean; + customPrompts: Record; + assistants: string[]; + tools: string[]; +}; export const userGroup = new Elysia() .use(authPlugin) @@ -17,20 +48,138 @@ export const userGroup = new Elysia() }) .group("/user", (app) => { return app - .get("/", () => { - // todo: get user - return "aa"; + .get("/", ({ locals }) => { + return ( + locals.user + ? { + id: locals.user._id.toString(), + username: locals.user.username, + avatarUrl: locals.user.avatarUrl, + email: locals.user.email, + logoutDisabled: locals.user.logoutDisabled, + isAdmin: locals.user.isAdmin ?? false, + isEarlyAccess: locals.user.isEarlyAccess ?? false, + } + : null + ) satisfies UserGETFront; }) - .get("/settings", () => { + .get("/settings", async ({ locals }) => { + const settings = await collections.settings.findOne(authCondition(locals)); + + if ( + settings && + !validateModel(models).safeParse(settings?.activeModel).success && + !settings.assistants?.map((el) => el.toString())?.includes(settings?.activeModel) + ) { + settings.activeModel = defaultModel.id; + await collections.settings.updateOne(authCondition(locals), { + $set: { activeModel: defaultModel.id }, + }); + } + + // if the model is unlisted, set the active model to the default model + if ( + settings?.activeModel && + models.find((m) => m.id === settings?.activeModel)?.unlisted === true + ) { + settings.activeModel = defaultModel.id; + await collections.settings.updateOne(authCondition(locals), { + $set: { activeModel: defaultModel.id }, + }); + } + // todo: get user settings - return "aa"; + return { + ethicsModalAccepted: !!settings?.ethicsModalAcceptedAt, + ethicsModalAcceptedAt: settings?.ethicsModalAcceptedAt ?? null, + + activeModel: settings?.activeModel ?? DEFAULT_SETTINGS.activeModel, + hideEmojiOnSidebar: settings?.hideEmojiOnSidebar ?? DEFAULT_SETTINGS.hideEmojiOnSidebar, + disableStream: settings?.disableStream ?? DEFAULT_SETTINGS.disableStream, + directPaste: settings?.directPaste ?? DEFAULT_SETTINGS.directPaste, + shareConversationsWithModelAuthors: + settings?.shareConversationsWithModelAuthors ?? + DEFAULT_SETTINGS.shareConversationsWithModelAuthors, + + customPrompts: settings?.customPrompts ?? {}, + assistants: settings?.assistants?.map((assistantId) => assistantId.toString()) ?? [], + tools: + settings?.tools ?? + toolFromConfigs + .filter((el) => !el.isHidden && el.isOnByDefault) + .map((el) => el._id.toString()), + } satisfies UserGETSettings; }) - .patch("/settings", () => { - // todo: patch user settings - return "aa"; + .post("/settings", async ({ locals, request }) => { + const body = await request.json(); + + const { ethicsModalAccepted, ...settings } = z + .object({ + shareConversationsWithModelAuthors: z + .boolean() + .default(DEFAULT_SETTINGS.shareConversationsWithModelAuthors), + hideEmojiOnSidebar: z.boolean().default(DEFAULT_SETTINGS.hideEmojiOnSidebar), + ethicsModalAccepted: z.boolean().optional(), + activeModel: z.string().default(DEFAULT_SETTINGS.activeModel), + customPrompts: z.record(z.string()).default({}), + tools: z.array(z.string()).optional(), + disableStream: z.boolean().default(false), + directPaste: z.boolean().default(false), + }) + .parse(body) satisfies SettingsEditable; + + // make sure all tools exist + // either in db or in config + if (settings.tools) { + const newTools = [ + ...(await collections.tools + .find({ _id: { $in: settings.tools.map((toolId) => new ObjectId(toolId)) } }) + .project({ _id: 1 }) + .toArray() + .then((tools) => tools.map((tool) => tool._id.toString()))), + ...toolFromConfigs + .filter((el) => (settings?.tools ?? []).includes(el._id.toString())) + .map((el) => el._id.toString()), + ]; + + settings.tools = newTools; + } + + await collections.settings.updateOne( + authCondition(locals), + { + $set: { + ...settings, + ...(ethicsModalAccepted && { ethicsModalAcceptedAt: new Date() }), + updatedAt: new Date(), + }, + $setOnInsert: { + createdAt: new Date(), + }, + }, + { + upsert: true, + } + ); + // return ok response + return new Response(); + }) + .get("/assistant/active", async ({ locals }) => { + const settings = await collections.settings.findOne(authCondition(locals)); + + if (!settings) { + return null; + } + + if (settings.assistants?.map((el) => el.toString())?.includes(settings?.activeModel)) { + return await collections.assistants.findOne({ + _id: new ObjectId(settings.activeModel), + }); + } + + return null; }) .get("/assistants", () => { // todo: get user assistants - return "aa"; }); }); diff --git a/src/lib/server/api/server.ts b/src/lib/server/api/server.ts index fa3d264b056..74ffdf7db0d 100644 --- a/src/lib/server/api/server.ts +++ b/src/lib/server/api/server.ts @@ -6,9 +6,8 @@ import { assistantGroup } from "$lib/server/api/routes/groups/assistants"; import { userGroup } from "$lib/server/api/routes/groups/user"; import { toolGroup } from "$lib/server/api/routes/groups/tools"; import { swagger } from "@elysiajs/swagger"; -import { models } from "$lib/server/models"; import { cors } from "@elysiajs/cors"; - +import { misc } from "$lib/server/api/routes/groups/misc"; const prefix = `${base}/api/v2` as unknown as ""; export const app = new Elysia({ prefix }) @@ -29,31 +28,6 @@ export const app = new Elysia({ prefix }) .use(toolGroup) .use(assistantGroup) .use(userGroup) - .get("/models", () => { - return models - .filter((m) => m.unlisted == false) - .map((model) => ({ - id: model.id, - name: model.name, - websiteUrl: model.websiteUrl ?? "https://huggingface.co", - modelUrl: model.modelUrl ?? "https://huggingface.co", - tokenizer: model.tokenizer, - datasetName: model.datasetName, - datasetUrl: model.datasetUrl, - displayName: model.displayName, - description: model.description ?? "", - logoUrl: model.logoUrl, - promptExamples: model.promptExamples ?? [], - preprompt: model.preprompt ?? "", - multimodal: model.multimodal ?? false, - unlisted: model.unlisted ?? false, - tools: model.tools ?? false, - hasInferenceAPI: model.hasInferenceAPI ?? false, - })); - }) - .get("/spaces-config", () => { - // todo: get spaces config - return; - }); + .use(misc); export type App = typeof app; diff --git a/src/routes/+layout.server.ts b/src/routes/+layout.server.ts index c54dea29cdb..d62623124e1 100644 --- a/src/routes/+layout.server.ts +++ b/src/routes/+layout.server.ts @@ -2,49 +2,23 @@ import type { LayoutServerLoad } from "./$types"; import { collections } from "$lib/server/database"; import type { Conversation } from "$lib/types/Conversation"; import { UrlDependency } from "$lib/types/UrlDependency"; -import { defaultModel, models, oldModels, validateModel } from "$lib/server/models"; -import { authCondition, requiresUser } from "$lib/server/auth"; -import { DEFAULT_SETTINGS } from "$lib/types/Settings"; -import { env } from "$env/dynamic/private"; +import { defaultModel, models } from "$lib/server/models"; +import { authCondition } from "$lib/server/auth"; import { ObjectId } from "mongodb"; import type { ConvSidebar } from "$lib/types/ConvSidebar"; import { toolFromConfigs } from "$lib/server/tools"; import { MetricsServer } from "$lib/server/metrics"; import type { ToolFront, ToolInputFile } from "$lib/types/Tool"; -import { ReviewStatus } from "$lib/types/Review"; import { base } from "$app/paths"; import { jsonSerialize } from "../lib/utils/serialize"; - +import type { FeatureFlags, GETModelsResponse } from "$lib/server/api/routes/groups/misc"; +import type { UserGETFront, UserGETSettings } from "$lib/server/api/routes/groups/user"; +import type { GETOldModelsResponse } from "$lib/server/api/routes/groups/misc"; export const load: LayoutServerLoad = async ({ locals, depends, fetch }) => { depends(UrlDependency.ConversationList); const settings = await collections.settings.findOne(authCondition(locals)); - // If the active model in settings is not valid, set it to the default model. This can happen if model was disabled. - if ( - settings && - !validateModel(models).safeParse(settings?.activeModel).success && - !settings.assistants?.map((el) => el.toString())?.includes(settings?.activeModel) - ) { - settings.activeModel = defaultModel.id; - await collections.settings.updateOne(authCondition(locals), { - $set: { activeModel: defaultModel.id }, - }); - } - - // if the model is unlisted, set the active model to the default model - if ( - settings?.activeModel && - models.find((m) => m.id === settings?.activeModel)?.unlisted === true - ) { - settings.activeModel = defaultModel.id; - await collections.settings.updateOne(authCondition(locals), { - $set: { activeModel: defaultModel.id }, - }); - } - - const enableAssistants = env.ENABLE_ASSISTANTS === "true"; - const assistantActive = !models.map(({ id }) => id).includes(settings?.activeModel ?? ""); const assistant = assistantActive @@ -58,7 +32,7 @@ export const load: LayoutServerLoad = async ({ locals, depends, fetch }) => { const conversations = nConversations === 0 ? Promise.resolve([]) - : fetch(`${base}/api/conversations`) + : fetch(`${base}/api/v2/conversations`) .then((res) => res.json()) .then( ( @@ -86,35 +60,6 @@ export const load: LayoutServerLoad = async ({ locals, depends, fetch }) => { .toArray() ); - const messagesBeforeLogin = env.MESSAGES_BEFORE_LOGIN ? parseInt(env.MESSAGES_BEFORE_LOGIN) : 0; - - let loginRequired = false; - - if (requiresUser && !locals.user) { - if (messagesBeforeLogin === 0) { - loginRequired = true; - } else if (nConversations >= messagesBeforeLogin) { - loginRequired = true; - } else { - // get the number of messages where `from === "assistant"` across all conversations. - const totalMessages = - ( - await collections.conversations - .aggregate([ - { $match: { ...authCondition(locals), "messages.from": "assistant" } }, - { $project: { messages: 1 } }, - { $limit: messagesBeforeLogin + 1 }, - { $unwind: "$messages" }, - { $match: { "messages.from": "assistant" } }, - { $count: "messages" }, - ]) - .toArray() - )[0]?.messages ?? 0; - - loginRequired = totalMessages >= messagesBeforeLogin; - } - } - const toolUseDuration = (await MetricsServer.getMetrics().tool.toolUseDuration.get()).values; const configToolIds = toolFromConfigs.map((el) => el._id.toString()); @@ -176,56 +121,12 @@ export const load: LayoutServerLoad = async ({ locals, depends, fetch }) => { }) ) ), - settings: { - searchEnabled: !!( - env.SERPAPI_KEY || - env.SERPER_API_KEY || - env.SERPSTACK_API_KEY || - env.SEARCHAPI_KEY || - env.YDC_API_KEY || - env.USE_LOCAL_WEBSEARCH || - env.SEARXNG_QUERY_URL || - env.BING_SUBSCRIPTION_KEY - ), - ethicsModalAccepted: !!settings?.ethicsModalAcceptedAt, - ethicsModalAcceptedAt: settings?.ethicsModalAcceptedAt ?? null, - activeModel: settings?.activeModel ?? DEFAULT_SETTINGS.activeModel, - hideEmojiOnSidebar: settings?.hideEmojiOnSidebar ?? false, - shareConversationsWithModelAuthors: - settings?.shareConversationsWithModelAuthors ?? - DEFAULT_SETTINGS.shareConversationsWithModelAuthors, - customPrompts: settings?.customPrompts ?? {}, - assistants: userAssistants, - tools: - settings?.tools ?? - toolFromConfigs - .filter((el) => !el.isHidden && el.isOnByDefault) - .map((el) => el._id.toString()), - disableStream: settings?.disableStream ?? DEFAULT_SETTINGS.disableStream, - directPaste: settings?.directPaste ?? DEFAULT_SETTINGS.directPaste, - }, - models: models.map((model) => ({ - id: model.id, - name: model.name, - websiteUrl: model.websiteUrl, - modelUrl: model.modelUrl, - tokenizer: model.tokenizer, - datasetName: model.datasetName, - datasetUrl: model.datasetUrl, - displayName: model.displayName, - description: model.description, - reasoning: !!model.reasoning, - logoUrl: model.logoUrl, - promptExamples: model.promptExamples, - parameters: model.parameters, - preprompt: model.preprompt, - multimodal: model.multimodal, - multimodalAcceptedMimetypes: model.multimodalAcceptedMimetypes, - tools: model.tools, - unlisted: model.unlisted, - hasInferenceAPI: model.hasInferenceAPI, - })), - oldModels, + models: await fetch(`${base}/api/v2/models`).then( + (res) => res.json() as Promise + ), + oldModels: await fetch(`${base}/api/v2/models/old`).then( + (res) => res.json() as Promise + ), tools: [...toolFromConfigs, ...communityTools] .filter((tool) => !tool?.isHidden) .map( @@ -250,10 +151,9 @@ export const load: LayoutServerLoad = async ({ locals, depends, fetch }) => { icon: tool.icon, }) satisfies ToolFront ), - communityToolCount: await collections.tools.countDocuments({ - type: "community", - review: ReviewStatus.APPROVED, - }), + communityToolCount: await fetch(`${base}/api/v2/tools/count`).then( + (res) => res.json() as Promise + ), assistants: assistants.then((assistants) => assistants .filter((el) => userAssistantsSet.has(el._id.toString())) @@ -265,21 +165,13 @@ export const load: LayoutServerLoad = async ({ locals, depends, fetch }) => { el.createdById.toString() === (locals.user?._id ?? locals.sessionId).toString(), })) ), - user: locals.user && { - id: locals.user._id.toString(), - username: locals.user.username, - avatarUrl: locals.user.avatarUrl, - email: locals.user.email, - logoutDisabled: locals.user.logoutDisabled, - isAdmin: locals.user.isAdmin ?? false, - isEarlyAccess: locals.user.isEarlyAccess ?? false, - }, assistant: assistant ? jsonSerialize(assistant) : undefined, - enableAssistants, - enableAssistantsRAG: env.ENABLE_ASSISTANTS_RAG === "true", - enableCommunityTools: env.COMMUNITY_TOOLS === "true", - loginRequired, - loginEnabled: requiresUser, - guestMode: requiresUser && messagesBeforeLogin > 0, + user: await fetch(`${base}/api/v2/user`).then((res) => res.json() as Promise), + settings: await fetch(`${base}/api/v2/user/settings`).then( + (res) => res.json() as Promise + ), + ...(await fetch(`${base}/api/v2/feature-flags`).then( + (res) => res.json() as Promise + )), }; }; diff --git a/src/routes/settings/(nav)/application/+page.svelte b/src/routes/settings/(nav)/application/+page.svelte index 590892fbf0f..db396302290 100644 --- a/src/routes/settings/(nav)/application/+page.svelte +++ b/src/routes/settings/(nav)/application/+page.svelte @@ -85,7 +85,7 @@ e.preventDefault(); confirm("Are you sure you want to delete all conversations?") && - (await fetch(`${base}/api/conversations`, { + (await fetch(`${base}/api/v2/conversations`, { method: "DELETE", }) .then(async () => { From eb3550c28e6c8d2f3666d35bb63ad8e49bee1594 Mon Sep 17 00:00:00 2001 From: Nathan Sarrazin Date: Tue, 4 Mar 2025 07:21:03 +0000 Subject: [PATCH 13/34] feat: more routes to universal load functions --- src/lib/server/api/routes/groups/user.ts | 12 ++++++++++++ src/lib/server/auth.ts | 4 ++++ src/routes/settings/+layout.server.ts | 25 ------------------------ src/routes/settings/+layout.ts | 18 +++++++++++++++++ 4 files changed, 34 insertions(+), 25 deletions(-) delete mode 100644 src/routes/settings/+layout.server.ts create mode 100644 src/routes/settings/+layout.ts diff --git a/src/lib/server/api/routes/groups/user.ts b/src/lib/server/api/routes/groups/user.ts index 369ada4224a..cefb991d76f 100644 --- a/src/lib/server/api/routes/groups/user.ts +++ b/src/lib/server/api/routes/groups/user.ts @@ -164,6 +164,18 @@ export const userGroup = new Elysia() // return ok response return new Response(); }) + .get("/reports", async ({ locals }) => { + if (!locals.user || !locals.sessionId) { + return []; + } + + const reports = await collections.reports + .find({ + createdBy: locals.user?._id ?? locals.sessionId, + }) + .toArray(); + return reports; + }) .get("/assistant/active", async ({ locals }) => { const settings = await collections.settings.findOne(authCondition(locals)); diff --git a/src/lib/server/auth.ts b/src/lib/server/auth.ts index c935e869b4b..7f22d1b668b 100644 --- a/src/lib/server/auth.ts +++ b/src/lib/server/auth.ts @@ -81,6 +81,10 @@ export async function findUser(sessionId: string) { return await collections.users.findOne({ _id: session.userId }); } export const authCondition = (locals: App.Locals) => { + if (!locals.user && !locals.sessionId) { + throw new Error("User or sessionId is required"); + } + return locals.user ? { userId: locals.user._id } : { sessionId: locals.sessionId, userId: { $exists: false } }; diff --git a/src/routes/settings/+layout.server.ts b/src/routes/settings/+layout.server.ts deleted file mode 100644 index 17119bc8c8c..00000000000 --- a/src/routes/settings/+layout.server.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { collections } from "$lib/server/database"; -import type { LayoutServerLoad } from "./$types"; -import type { Report } from "$lib/types/Report"; - -export const load = (async ({ locals, parent }) => { - const { assistants } = await parent(); - - let reportsByUser: string[] = []; - const createdBy = locals.user?._id ?? locals.sessionId; - if (createdBy) { - const reports = await collections.reports - .find< - Pick - >({ createdBy, object: "assistant" }, { projection: { _id: 0, contentId: 1 } }) - .toArray(); - reportsByUser = reports.map((r) => r.contentId.toString()); - } - - return { - assistants: (await assistants).map((el) => ({ - ...el, - reported: reportsByUser.includes(el._id), - })), - }; -}) satisfies LayoutServerLoad; diff --git a/src/routes/settings/+layout.ts b/src/routes/settings/+layout.ts new file mode 100644 index 00000000000..22f77aea863 --- /dev/null +++ b/src/routes/settings/+layout.ts @@ -0,0 +1,18 @@ +import type { Report } from "$lib/types/Report"; +import { base } from "$app/paths"; +import type { Serialize } from "$lib/utils/serialize"; + +export const load = async ({ parent }) => { + const { assistants } = await parent(); + + const reports = await fetch(`${base}/api/v2/user/reports`) + .then((res) => res.json() as Promise[]>) + .catch(() => []); + + return { + assistants: (await assistants).map((el) => ({ + ...el, + reported: reports.some((r) => r.contentId === el._id && r.object === "assistant"), + })), + }; +}; From 16fb59aa6229317a5dff22930cd83b5c3a48c5c0 Mon Sep 17 00:00:00 2001 From: Nathan Sarrazin Date: Tue, 4 Mar 2025 07:53:51 +0000 Subject: [PATCH 14/34] feat: more routes to universal --- src/lib/server/api/routes/groups/misc.ts | 58 ---------- src/lib/server/api/routes/groups/models.ts | 106 +++++++++++++++++++ src/lib/server/api/server.ts | 3 + src/routes/+layout.server.ts | 5 +- src/routes/models/[...model]/+page.server.ts | 39 ------- src/routes/models/[...model]/+page.ts | 19 ++++ 6 files changed, 131 insertions(+), 99 deletions(-) create mode 100644 src/lib/server/api/routes/groups/models.ts delete mode 100644 src/routes/models/[...model]/+page.server.ts create mode 100644 src/routes/models/[...model]/+page.ts diff --git a/src/lib/server/api/routes/groups/misc.ts b/src/lib/server/api/routes/groups/misc.ts index 19ce402d100..6fa9c936aaf 100644 --- a/src/lib/server/api/routes/groups/misc.ts +++ b/src/lib/server/api/routes/groups/misc.ts @@ -3,7 +3,6 @@ import { authPlugin } from "../../authPlugin"; import { requiresUser } from "$lib/server/auth"; import { collections } from "$lib/server/database"; import { authCondition } from "$lib/server/auth"; -import { models, oldModels, type BackendModel } from "$lib/server/models"; export interface FeatureFlags { searchEnabled: boolean; @@ -15,35 +14,6 @@ export interface FeatureFlags { guestMode: boolean; } -export type GETModelsResponse = Array<{ - id: string; - name: string; - websiteUrl?: string; - modelUrl?: string; - tokenizer?: string | { tokenizerUrl: string; tokenizerConfigUrl: string }; - datasetName?: string; - datasetUrl?: string; - displayName: string; - description?: string; - reasoning: boolean; - logoUrl?: string; - promptExamples?: { title: string; prompt: string }[]; - parameters: BackendModel["parameters"]; - preprompt?: string; - multimodal: boolean; - multimodalAcceptedMimetypes?: string[]; - tools: boolean; - unlisted: boolean; - hasInferenceAPI: boolean; -}>; - -export type GETOldModelsResponse = Array<{ - id: string; - name: string; - displayName: string; - transferTo?: string; -}>; - export const misc = new Elysia() .use(authPlugin) .get("/feature-flags", async ({ locals }) => { @@ -95,34 +65,6 @@ export const misc = new Elysia() guestMode: requiresUser && messagesBeforeLogin > 0, } satisfies FeatureFlags; }) - .get("/models", () => { - return models - .filter((m) => m.unlisted == false) - .map((model) => ({ - id: model.id, - name: model.name, - websiteUrl: model.websiteUrl, - modelUrl: model.modelUrl, - tokenizer: model.tokenizer, - datasetName: model.datasetName, - datasetUrl: model.datasetUrl, - displayName: model.displayName, - description: model.description, - reasoning: !!model.reasoning, - logoUrl: model.logoUrl, - promptExamples: model.promptExamples, - parameters: model.parameters, - preprompt: model.preprompt, - multimodal: model.multimodal, - multimodalAcceptedMimetypes: model.multimodalAcceptedMimetypes, - tools: model.tools, - unlisted: model.unlisted, - hasInferenceAPI: model.hasInferenceAPI, - })) satisfies GETModelsResponse; - }) - .get("/models/old", () => { - return oldModels satisfies GETOldModelsResponse; - }) .get("/spaces-config", () => { // todo: get spaces config return; diff --git a/src/lib/server/api/routes/groups/models.ts b/src/lib/server/api/routes/groups/models.ts new file mode 100644 index 00000000000..50e3ea00a7c --- /dev/null +++ b/src/lib/server/api/routes/groups/models.ts @@ -0,0 +1,106 @@ +import { Elysia } from "elysia"; +import { models, oldModels, type BackendModel } from "$lib/server/models"; +import { authPlugin } from "../../authPlugin"; +import { authCondition } from "$lib/server/auth"; +import { collections } from "$lib/server/database"; + +export type GETModelsResponse = Array<{ + id: string; + name: string; + websiteUrl?: string; + modelUrl?: string; + tokenizer?: string | { tokenizerUrl: string; tokenizerConfigUrl: string }; + datasetName?: string; + datasetUrl?: string; + displayName: string; + description?: string; + reasoning: boolean; + logoUrl?: string; + promptExamples?: { title: string; prompt: string }[]; + parameters: BackendModel["parameters"]; + preprompt?: string; + multimodal: boolean; + multimodalAcceptedMimetypes?: string[]; + tools: boolean; + unlisted: boolean; + hasInferenceAPI: boolean; +}>; + +export type GETOldModelsResponse = Array<{ + id: string; + name: string; + displayName: string; + transferTo?: string; +}>; + +export const modelGroup = new Elysia().group("/models", (app) => + app + .get("/", () => { + return models + .filter((m) => m.unlisted == false) + .map((model) => ({ + id: model.id, + name: model.name, + websiteUrl: model.websiteUrl, + modelUrl: model.modelUrl, + tokenizer: model.tokenizer, + datasetName: model.datasetName, + datasetUrl: model.datasetUrl, + displayName: model.displayName, + description: model.description, + reasoning: !!model.reasoning, + logoUrl: model.logoUrl, + promptExamples: model.promptExamples, + parameters: model.parameters, + preprompt: model.preprompt, + multimodal: model.multimodal, + multimodalAcceptedMimetypes: model.multimodalAcceptedMimetypes, + tools: model.tools, + unlisted: model.unlisted, + hasInferenceAPI: model.hasInferenceAPI, + })) satisfies GETModelsResponse; + }) + .get("/old", () => { + return oldModels satisfies GETOldModelsResponse; + }) + .group("/:namespace/:model?", (app) => + app + .derive(async ({ params, error }) => { + let modelId: string = params.namespace; + if (params.model) { + modelId += "/" + params.model; + } + const model = models.find((m) => m.id === modelId); + if (!model || model.unlisted) { + return error(404, "Model not found"); + } + return { model }; + }) + .get("/", ({ model }) => { + return model; + }) + .use(authPlugin) + .post("/subscribe", async ({ locals, model, error }) => { + if (!locals.user || !locals.sessionId) { + return error(401, "Unauthorized"); + } + await collections.settings.updateOne( + authCondition(locals), + { + $set: { + activeModel: model.id, + updatedAt: new Date(), + }, + $setOnInsert: { + createdAt: new Date(), + }, + }, + { + upsert: true, + } + ); + + return new Response(); + }) + ) +); diff --git a/src/lib/server/api/server.ts b/src/lib/server/api/server.ts index 74ffdf7db0d..b5a77c30174 100644 --- a/src/lib/server/api/server.ts +++ b/src/lib/server/api/server.ts @@ -8,6 +8,8 @@ import { toolGroup } from "$lib/server/api/routes/groups/tools"; import { swagger } from "@elysiajs/swagger"; import { cors } from "@elysiajs/cors"; import { misc } from "$lib/server/api/routes/groups/misc"; +import { modelGroup } from "$lib/server/api/routes/groups/models"; + const prefix = `${base}/api/v2` as unknown as ""; export const app = new Elysia({ prefix }) @@ -28,6 +30,7 @@ export const app = new Elysia({ prefix }) .use(toolGroup) .use(assistantGroup) .use(userGroup) + .use(modelGroup) .use(misc); export type App = typeof app; diff --git a/src/routes/+layout.server.ts b/src/routes/+layout.server.ts index d62623124e1..5897fe455fd 100644 --- a/src/routes/+layout.server.ts +++ b/src/routes/+layout.server.ts @@ -11,9 +11,10 @@ import { MetricsServer } from "$lib/server/metrics"; import type { ToolFront, ToolInputFile } from "$lib/types/Tool"; import { base } from "$app/paths"; import { jsonSerialize } from "../lib/utils/serialize"; -import type { FeatureFlags, GETModelsResponse } from "$lib/server/api/routes/groups/misc"; +import type { FeatureFlags } from "$lib/server/api/routes/groups/misc"; import type { UserGETFront, UserGETSettings } from "$lib/server/api/routes/groups/user"; -import type { GETOldModelsResponse } from "$lib/server/api/routes/groups/misc"; +import type { GETModelsResponse, GETOldModelsResponse } from "$lib/server/api/routes/groups/models"; + export const load: LayoutServerLoad = async ({ locals, depends, fetch }) => { depends(UrlDependency.ConversationList); diff --git a/src/routes/models/[...model]/+page.server.ts b/src/routes/models/[...model]/+page.server.ts deleted file mode 100644 index 4e385ece615..00000000000 --- a/src/routes/models/[...model]/+page.server.ts +++ /dev/null @@ -1,39 +0,0 @@ -import { base } from "$app/paths"; -import { authCondition } from "$lib/server/auth.js"; -import { collections } from "$lib/server/database"; -import { models } from "$lib/server/models"; -import { redirect } from "@sveltejs/kit"; - -export async function load({ params, locals, parent }) { - const model = models.find(({ id }) => id === params.model); - const data = await parent(); - - if (!model || model.unlisted) { - redirect(302, `${base}/`); - } - - if (locals.user?._id ?? locals.sessionId) { - await collections.settings.updateOne( - authCondition(locals), - { - $set: { - activeModel: model.id, - updatedAt: new Date(), - }, - $setOnInsert: { - createdAt: new Date(), - }, - }, - { - upsert: true, - } - ); - } - - return { - settings: { - ...data.settings, - activeModel: model.id, - }, - }; -} diff --git a/src/routes/models/[...model]/+page.ts b/src/routes/models/[...model]/+page.ts new file mode 100644 index 00000000000..a3eefaa311d --- /dev/null +++ b/src/routes/models/[...model]/+page.ts @@ -0,0 +1,19 @@ +import { base } from "$app/paths"; +import { redirect } from "@sveltejs/kit"; + +export async function load({ params, parent, fetch }) { + const r = await fetch(`${base}/api/v2/models/${params.model}/subscribe`, { + method: "POST", + }); + + if (!r.ok) { + redirect(302, base + "/"); + } + + return { + settings: await parent().then((data) => ({ + ...data.settings, + activeModel: params.model, + })), + }; +} From e89fd95593b2255f9e28acf876624e66b957cbf6 Mon Sep 17 00:00:00 2001 From: Nathan Sarrazin Date: Tue, 4 Mar 2025 08:17:41 +0000 Subject: [PATCH 15/34] refactor: move tools loading to API endpoint --- src/lib/server/api/routes/groups/tools.ts | 51 +++++++++++---------- src/routes/+layout.server.ts | 55 ++--------------------- 2 files changed, 29 insertions(+), 77 deletions(-) diff --git a/src/lib/server/api/routes/groups/tools.ts b/src/lib/server/api/routes/groups/tools.ts index 3cbf045f4dc..42945aec902 100644 --- a/src/lib/server/api/routes/groups/tools.ts +++ b/src/lib/server/api/routes/groups/tools.ts @@ -12,11 +12,33 @@ export type GETToolsResponse = Array; export const toolGroup = new Elysia().use(authPlugin).group("/tools", (app) => { return app - .get("/config", async () => { + .get("/active", async ({ locals }) => { + const settings = await collections.settings.findOne(authCondition(locals)); + + if (!settings) { + return []; + } + const toolUseDuration = (await MetricsServer.getMetrics().tool.toolUseDuration.get()).values; - return toolFromConfigs - .filter((tool) => !tool?.isHidden) + const activeCommunityToolIds = settings.tools ?? []; + + const communityTools = await collections.tools + .find({ _id: { $in: activeCommunityToolIds.map((el) => new ObjectId(el)) } }) + .toArray() + .then((tools) => + tools.map((tool) => ({ + ...tool, + isHidden: false, + isOnByDefault: true, + isLocked: true, + })) + ); + + const fullTools = [...communityTools, ...toolFromConfigs]; + + return fullTools + .filter((tool) => !tool.isHidden) .map( (tool) => ({ @@ -40,29 +62,6 @@ export const toolGroup = new Elysia().use(authPlugin).group("/tools", (app) => { }) satisfies ToolFront ); }) - .get("/active", async ({ locals }) => { - const settings = await collections.settings.findOne(authCondition(locals)); - - if (!settings) { - return []; - } - - const activeCommunityToolIds = settings.tools ?? []; - - const communityTools = await collections.tools - .find({ _id: { $in: activeCommunityToolIds.map((el) => new ObjectId(el)) } }) - .toArray() - .then((tools) => - tools.map((tool) => ({ - ...tool, - isHidden: false, - isOnByDefault: true, - isLocked: true, - })) - ); - - return communityTools; - }) .get("/search", () => { // todo: search tools return "aa"; diff --git a/src/routes/+layout.server.ts b/src/routes/+layout.server.ts index 5897fe455fd..a3e32fda485 100644 --- a/src/routes/+layout.server.ts +++ b/src/routes/+layout.server.ts @@ -6,9 +6,7 @@ import { defaultModel, models } from "$lib/server/models"; import { authCondition } from "$lib/server/auth"; import { ObjectId } from "mongodb"; import type { ConvSidebar } from "$lib/types/ConvSidebar"; -import { toolFromConfigs } from "$lib/server/tools"; -import { MetricsServer } from "$lib/server/metrics"; -import type { ToolFront, ToolInputFile } from "$lib/types/Tool"; +import type { ToolFront } from "$lib/types/Tool"; import { base } from "$app/paths"; import { jsonSerialize } from "../lib/utils/serialize"; import type { FeatureFlags } from "$lib/server/api/routes/groups/misc"; @@ -61,30 +59,6 @@ export const load: LayoutServerLoad = async ({ locals, depends, fetch }) => { .toArray() ); - const toolUseDuration = (await MetricsServer.getMetrics().tool.toolUseDuration.get()).values; - - const configToolIds = toolFromConfigs.map((el) => el._id.toString()); - - let activeCommunityToolIds = (settings?.tools ?? []).filter( - (key) => !configToolIds.includes(key) - ); - - if (assistant) { - activeCommunityToolIds = [...activeCommunityToolIds, ...(assistant.tools ?? [])]; - } - - const communityTools = await collections.tools - .find({ _id: { $in: activeCommunityToolIds.map((el) => new ObjectId(el)) } }) - .toArray() - .then((tools) => - tools.map((tool) => ({ - ...tool, - isHidden: false, - isOnByDefault: true, - isLocked: true, - })) - ); - return { nConversations, conversations: await conversations.then( @@ -128,30 +102,9 @@ export const load: LayoutServerLoad = async ({ locals, depends, fetch }) => { oldModels: await fetch(`${base}/api/v2/models/old`).then( (res) => res.json() as Promise ), - tools: [...toolFromConfigs, ...communityTools] - .filter((tool) => !tool?.isHidden) - .map( - (tool) => - ({ - _id: tool._id.toString(), - type: tool.type, - displayName: tool.displayName, - name: tool.name, - description: tool.description, - mimeTypes: (tool.inputs ?? []) - .filter((input): input is ToolInputFile => input.type === "file") - .map((input) => (input as ToolInputFile).mimeTypes) - .flat(), - isOnByDefault: tool.isOnByDefault ?? true, - isLocked: tool.isLocked ?? true, - timeToUseMS: - toolUseDuration.find( - (el) => el.labels.tool === tool._id.toString() && el.labels.quantile === 0.9 - )?.value ?? 15_000, - color: tool.color, - icon: tool.icon, - }) satisfies ToolFront - ), + tools: await fetch(`${base}/api/v2/tools/active`).then( + (res) => res.json() as Promise + ), communityToolCount: await fetch(`${base}/api/v2/tools/count`).then( (res) => res.json() as Promise ), From 1b513bfe31890c777b538345de23b1d011637263 Mon Sep 17 00:00:00 2001 From: Nathan Sarrazin Date: Tue, 4 Mar 2025 09:20:39 +0000 Subject: [PATCH 16/34] refactor(api): implement tools search API endpoint and move load function --- src/lib/server/api/routes/groups/tools.ts | 121 ++++++++++++++++++++-- src/routes/tools/+page.server.ts | 100 ------------------ src/routes/tools/+page.ts | 15 +++ 3 files changed, 129 insertions(+), 107 deletions(-) delete mode 100644 src/routes/tools/+page.server.ts create mode 100644 src/routes/tools/+page.ts diff --git a/src/lib/server/api/routes/groups/tools.ts b/src/lib/server/api/routes/groups/tools.ts index 42945aec902..bbba67fe199 100644 --- a/src/lib/server/api/routes/groups/tools.ts +++ b/src/lib/server/api/routes/groups/tools.ts @@ -1,14 +1,29 @@ -import { Elysia } from "elysia"; +import { Elysia, t } from "elysia"; import { authPlugin } from "$lib/server/api/authPlugin"; import { ReviewStatus } from "$lib/types/Review"; import { toolFromConfigs } from "$lib/server/tools"; import { collections } from "$lib/server/database"; -import { ObjectId } from "mongodb"; -import type { ToolFront, ToolInputFile } from "$lib/types/Tool"; +import { ObjectId, type Filter } from "mongodb"; +import type { CommunityToolDB, Tool, ToolFront, ToolInputFile } from "$lib/types/Tool"; import { MetricsServer } from "$lib/server/metrics"; import { authCondition } from "$lib/server/auth"; +import { SortKey } from "$lib/types/Assistant"; +import type { User } from "$lib/types/User"; +import { generateQueryTokens, generateSearchTokens } from "$lib/utils/searchTokens"; +import { env } from "$env/dynamic/private"; +import { jsonSerialize, type Serialize } from "$lib/utils/serialize"; + +const NUM_PER_PAGE = 16; export type GETToolsResponse = Array; +export type GETToolsSearchResponse = { + tools: Array>>; + numTotalItems: number; + numItemsPerPage: number; + query: string | null; + sort: SortKey; + showUnfeatured: boolean; +}; export const toolGroup = new Elysia().use(authPlugin).group("/tools", (app) => { return app @@ -62,10 +77,102 @@ export const toolGroup = new Elysia().use(authPlugin).group("/tools", (app) => { }) satisfies ToolFront ); }) - .get("/search", () => { - // todo: search tools - return "aa"; - }) + .get( + "/search", + async ({ query, locals, error }) => { + if (env.COMMUNITY_TOOLS !== "true") { + error(403, "Community tools are not enabled"); + } + + const username = query.user; + const search = query.q?.trim() ?? null; + + const pageIndex = query.p ?? 0; + const sort = query.sort ?? SortKey.TRENDING; + const createdByCurrentUser = locals.user?.username && locals.user.username === username; + const activeOnly = query.active ?? false; + const showUnfeatured = query.showUnfeatured ?? false; + + let user: Pick | null = null; + if (username) { + user = await collections.users.findOne>( + { username }, + { projection: { _id: 1 } } + ); + if (!user) { + error(404, `User "${username}" doesn't exist`); + } + } + + const settings = await collections.settings.findOne(authCondition(locals)); + + if (!settings && activeOnly) { + error(404, "No user settings found"); + } + + const queryTokens = !!search && generateQueryTokens(search); + + const filter: Filter = { + ...(!createdByCurrentUser && + !activeOnly && + !(locals.user?.isAdmin && showUnfeatured) && { review: ReviewStatus.APPROVED }), + ...(user && { createdById: user._id }), + ...(queryTokens && { searchTokens: { $all: queryTokens } }), + ...(activeOnly && { + _id: { + $in: (settings?.tools ?? []).map((key) => { + return new ObjectId(key); + }), + }, + }), + }; + + const communityTools = await collections.tools + .find(filter) + .skip(NUM_PER_PAGE * pageIndex) + .sort({ + ...(sort === SortKey.TRENDING && { last24HoursUseCount: -1 }), + useCount: -1, + }) + .limit(NUM_PER_PAGE) + .toArray(); + + const configTools = toolFromConfigs + .filter((tool) => !tool?.isHidden) + .filter((tool) => { + if (queryTokens) { + return generateSearchTokens(tool.displayName).some((token) => + queryTokens.some((queryToken) => queryToken.test(token)) + ); + } + return true; + }); + + const tools = [...(pageIndex == 0 && !username ? configTools : []), ...communityTools]; + + const numTotalItems = + (await collections.tools.countDocuments(filter)) + toolFromConfigs.length; + + return { + tools: jsonSerialize(tools), + numTotalItems, + numItemsPerPage: NUM_PER_PAGE, + query: search, + sort, + showUnfeatured, + } satisfies GETToolsSearchResponse; + }, + { + query: t.Object({ + user: t.Optional(t.String()), + q: t.Optional(t.String()), + sort: t.Optional(t.Enum(SortKey)), + p: t.Optional(t.Numeric()), + showUnfeatured: t.Optional(t.Boolean()), + active: t.Optional(t.Boolean()), + }), + } + ) .get("/count", () => { // return community tool count return collections.tools.countDocuments({ type: "community", review: ReviewStatus.APPROVED }); diff --git a/src/routes/tools/+page.server.ts b/src/routes/tools/+page.server.ts deleted file mode 100644 index b704a889930..00000000000 --- a/src/routes/tools/+page.server.ts +++ /dev/null @@ -1,100 +0,0 @@ -import { env } from "$env/dynamic/private"; -import { authCondition } from "$lib/server/auth.js"; -import { Database, collections } from "$lib/server/database.js"; -import { toolFromConfigs } from "$lib/server/tools/index.js"; -import { SortKey } from "$lib/types/Assistant.js"; -import { ReviewStatus } from "$lib/types/Review"; -import type { CommunityToolDB } from "$lib/types/Tool.js"; -import type { User } from "$lib/types/User.js"; -import { generateQueryTokens, generateSearchTokens } from "$lib/utils/searchTokens.js"; -import { jsonSerialize } from "$lib/utils/serialize"; -import { error } from "@sveltejs/kit"; -import { ObjectId, type Filter } from "mongodb"; - -const NUM_PER_PAGE = 16; - -export const load = async ({ url, locals }) => { - if (env.COMMUNITY_TOOLS !== "true") { - error(403, "Community tools are not enabled"); - } - - const username = url.searchParams.get("user"); - const query = url.searchParams.get("q")?.trim() ?? null; - - const pageIndex = parseInt(url.searchParams.get("p") ?? "0"); - const sort = url.searchParams.get("sort")?.trim() ?? SortKey.TRENDING; - const createdByCurrentUser = locals.user?.username && locals.user.username === username; - const activeOnly = url.searchParams.get("active") === "true"; - const showUnfeatured = url.searchParams.get("showUnfeatured") === "true"; - - let user: Pick | null = null; - if (username) { - user = await collections.users.findOne>( - { username }, - { projection: { _id: 1 } } - ); - if (!user) { - error(404, `User "${username}" doesn't exist`); - } - } - - const settings = await collections.settings.findOne(authCondition(locals)); - - if (!settings && activeOnly) { - error(404, "No user settings found"); - } - - const queryTokens = !!query && generateQueryTokens(query); - - const filter: Filter = { - ...(!createdByCurrentUser && - !activeOnly && - !(locals.user?.isAdmin && showUnfeatured) && { review: ReviewStatus.APPROVED }), - ...(user && { createdById: user._id }), - ...(queryTokens && { searchTokens: { $all: queryTokens } }), - ...(activeOnly && { - _id: { - $in: (settings?.tools ?? []).map((key) => { - return new ObjectId(key); - }), - }, - }), - }; - - const communityTools = await Database.getInstance() - .getCollections() - .tools.find(filter) - .skip(NUM_PER_PAGE * pageIndex) - .sort({ - ...(sort === SortKey.TRENDING && { last24HoursUseCount: -1 }), - useCount: -1, - }) - .limit(NUM_PER_PAGE) - .toArray(); - - const configTools = toolFromConfigs - .filter((tool) => !tool?.isHidden) - .filter((tool) => { - if (queryTokens) { - return generateSearchTokens(tool.displayName).some((token) => - queryTokens.some((queryToken) => queryToken.test(token)) - ); - } - return true; - }); - - const tools = [...(pageIndex == 0 && !username ? configTools : []), ...communityTools]; - - const numTotalItems = - (await Database.getInstance().getCollections().tools.countDocuments(filter)) + - toolFromConfigs.length; - - return { - tools: jsonSerialize(tools), - numTotalItems, - numItemsPerPage: NUM_PER_PAGE, - query, - sort, - showUnfeatured, - }; -}; diff --git a/src/routes/tools/+page.ts b/src/routes/tools/+page.ts new file mode 100644 index 00000000000..01b7ecf2790 --- /dev/null +++ b/src/routes/tools/+page.ts @@ -0,0 +1,15 @@ +import { base } from "$app/paths"; +import type { GETToolsSearchResponse } from "$lib/server/api/routes/groups/tools"; +import { error } from "@sveltejs/kit"; + +export const load = async ({ url, fetch }) => { + const r = await fetch(`${base}/api/v2/tools/search?${url.searchParams.toString()}`); + + if (!r.ok) { + throw error(r.status, "Failed to fetch tools"); + } + + const data = (await r.json()) as GETToolsSearchResponse; + + return data; +}; From 089c1eb7f957faa966f724586371a28d68ca760e Mon Sep 17 00:00:00 2001 From: Nathan Sarrazin Date: Tue, 4 Mar 2025 09:36:19 +0000 Subject: [PATCH 17/34] fix: types on tool search --- src/lib/server/api/routes/groups/tools.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/lib/server/api/routes/groups/tools.ts b/src/lib/server/api/routes/groups/tools.ts index bbba67fe199..659b572e961 100644 --- a/src/lib/server/api/routes/groups/tools.ts +++ b/src/lib/server/api/routes/groups/tools.ts @@ -4,7 +4,7 @@ import { ReviewStatus } from "$lib/types/Review"; import { toolFromConfigs } from "$lib/server/tools"; import { collections } from "$lib/server/database"; import { ObjectId, type Filter } from "mongodb"; -import type { CommunityToolDB, Tool, ToolFront, ToolInputFile } from "$lib/types/Tool"; +import type { CommunityToolDB, ConfigTool, ToolFront, ToolInputFile } from "$lib/types/Tool"; import { MetricsServer } from "$lib/server/metrics"; import { authCondition } from "$lib/server/auth"; import { SortKey } from "$lib/types/Assistant"; @@ -17,7 +17,7 @@ const NUM_PER_PAGE = 16; export type GETToolsResponse = Array; export type GETToolsSearchResponse = { - tools: Array>>; + tools: Array>; numTotalItems: number; numItemsPerPage: number; query: string | null; From 91d15f2d586b4424aacfcf04c5c20730633e2832 Mon Sep 17 00:00:00 2001 From: Nathan Sarrazin Date: Tue, 4 Mar 2025 10:15:09 +0000 Subject: [PATCH 18/34] refactor: update assistant route and remove redundant page load function --- src/lib/components/chat/ChatWindow.svelte | 2 +- .../(nav)/assistants/[assistantId]/+page.ts | 14 -------------- 2 files changed, 1 insertion(+), 15 deletions(-) delete mode 100644 src/routes/settings/(nav)/assistants/[assistantId]/+page.ts diff --git a/src/lib/components/chat/ChatWindow.svelte b/src/lib/components/chat/ChatWindow.svelte index b56bc2d2a2b..34b273750ca 100644 --- a/src/lib/components/chat/ChatWindow.svelte +++ b/src/lib/components/chat/ChatWindow.svelte @@ -253,7 +253,7 @@ {#if assistant && !!messages.length} {#if assistant.avatar} id === params.assistantId); - - if (!assistant) { - redirect(302, `${base}/assistant/${params.assistantId}`); - } - - return data; -} From 585d411875b0b6791294eecea596e2f6b5776a5c Mon Sep 17 00:00:00 2001 From: Nathan Sarrazin Date: Tue, 4 Mar 2025 10:26:09 +0000 Subject: [PATCH 19/34] refactor(api): move assistants page load function to api call --- .../server/api/routes/groups/assistants.ts | 108 +++++++++++++++++- src/routes/assistants/+page.server.ts | 87 -------------- src/routes/assistants/+page.ts | 15 +++ 3 files changed, 121 insertions(+), 89 deletions(-) delete mode 100644 src/routes/assistants/+page.server.ts create mode 100644 src/routes/assistants/+page.ts diff --git a/src/lib/server/api/routes/groups/assistants.ts b/src/lib/server/api/routes/groups/assistants.ts index 7cf2520fd76..817707cef99 100644 --- a/src/lib/server/api/routes/groups/assistants.ts +++ b/src/lib/server/api/routes/groups/assistants.ts @@ -1,8 +1,26 @@ -import { Elysia } from "elysia"; +import { Elysia, t } from "elysia"; import { authPlugin } from "$lib/server/api/authPlugin"; import { collections } from "$lib/server/database"; -import { ObjectId } from "mongodb"; +import { ObjectId, type Filter } from "mongodb"; import { authCondition } from "$lib/server/auth"; +import { SortKey, type Assistant } from "$lib/types/Assistant"; +import { env } from "$env/dynamic/private"; +import type { User } from "$lib/types/User"; +import { ReviewStatus } from "$lib/types/Review"; +import { generateQueryTokens } from "$lib/utils/searchTokens"; +import { jsonSerialize, type Serialize } from "$lib/utils/serialize"; + +export type GETAssistantsSearchResponse = { + assistants: Array>; + selectedModel: string; + numTotalItems: number; + numItemsPerPage: number; + query: string | null; + sort: SortKey; + showUnfeatured: boolean; +}; + +const NUM_PER_PAGE = 24; export const assistantGroup = new Elysia().use(authPlugin).group("/assistants", (app) => { return app @@ -14,6 +32,92 @@ export const assistantGroup = new Elysia().use(authPlugin).group("/assistants", // todo: post new assistant return "aa"; }) + .get( + "/search", + async ({ query, locals, error }) => { + if (!env.ENABLE_ASSISTANTS) { + error(403, "Assistants are not enabled"); + } + const modelId = query.modelId; + const pageIndex = query.p ?? 0; + const username = query.user; + const search = query.q?.trim() ?? null; + const sort = query.sort ?? SortKey.TRENDING; + const showUnfeatured = query.showUnfeatured ?? false; + const createdByCurrentUser = locals.user?.username && locals.user.username === username; + + let user: Pick | null = null; + if (username) { + user = await collections.users.findOne>( + { username }, + { projection: { _id: 1 } } + ); + if (!user) { + error(404, `User "${username}" doesn't exist`); + } + } + // 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 + let shouldBeFeatured = {}; + + if ( + env.REQUIRE_FEATURED_ASSISTANTS === "true" && + !(locals.user?.isAdmin && showUnfeatured) + ) { + if (!user) { + // only show featured assistants on the community page + shouldBeFeatured = { review: ReviewStatus.APPROVED }; + } else if (!createdByCurrentUser) { + // on a user page show assistants that have been approved or are pending + shouldBeFeatured = { review: { $in: [ReviewStatus.APPROVED, ReviewStatus.PENDING] } }; + } + } + + const noSpecificSearch = !user && !search; + // fetch the top assistants sorted by user count from biggest to smallest. + // filter by model too if modelId is provided or query if query is provided + // only show assistants that have been used by more than 5 users if no specific search is made + const filter: Filter = { + ...(modelId && { modelId }), + ...(user && { createdById: user._id }), + ...(search && { searchTokens: { $all: generateQueryTokens(search) } }), + ...(noSpecificSearch && { userCount: { $gte: 5 } }), + ...shouldBeFeatured, + }; + + const assistants = await collections.assistants + .find(filter) + .sort({ + ...(sort === SortKey.TRENDING && { last24HoursCount: -1 }), + userCount: -1, + _id: 1, + }) + .skip(NUM_PER_PAGE * pageIndex) + .limit(NUM_PER_PAGE) + .toArray(); + + const numTotalItems = await collections.assistants.countDocuments(filter); + + return { + assistants: jsonSerialize(assistants), + selectedModel: modelId ?? "", + numTotalItems, + numItemsPerPage: NUM_PER_PAGE, + query: search, + sort, + showUnfeatured, + }; + }, + { + query: t.Object({ + user: t.Optional(t.String()), + q: t.Optional(t.String()), + sort: t.Optional(t.Enum(SortKey)), + p: t.Optional(t.Numeric()), + showUnfeatured: t.Optional(t.Boolean()), + modelId: t.Optional(t.String()), + }), + } + ) .group("/:id", (app) => { return app .derive(async ({ params, error }) => { diff --git a/src/routes/assistants/+page.server.ts b/src/routes/assistants/+page.server.ts deleted file mode 100644 index d74701e63b6..00000000000 --- a/src/routes/assistants/+page.server.ts +++ /dev/null @@ -1,87 +0,0 @@ -import { base } from "$app/paths"; -import { env } from "$env/dynamic/private"; -import { Database, collections } from "$lib/server/database.js"; -import { SortKey, type Assistant } from "$lib/types/Assistant"; -import type { User } from "$lib/types/User"; -import { generateQueryTokens } from "$lib/utils/searchTokens.js"; -import { error, redirect } from "@sveltejs/kit"; -import type { Filter } from "mongodb"; -import { ReviewStatus } from "$lib/types/Review"; -import { jsonSerialize } from "$lib/utils/serialize"; -const NUM_PER_PAGE = 24; - -export const load = async ({ url, locals }) => { - if (!env.ENABLE_ASSISTANTS) { - redirect(302, `${base}/`); - } - - const modelId = url.searchParams.get("modelId"); - const pageIndex = parseInt(url.searchParams.get("p") ?? "0"); - const username = url.searchParams.get("user"); - const query = url.searchParams.get("q")?.trim() ?? null; - const sort = url.searchParams.get("sort")?.trim() ?? SortKey.TRENDING; - const showUnfeatured = url.searchParams.get("showUnfeatured") === "true"; - const createdByCurrentUser = locals.user?.username && locals.user.username === username; - - let user: Pick | null = null; - if (username) { - user = await collections.users.findOne>( - { username }, - { projection: { _id: 1 } } - ); - if (!user) { - error(404, `User "${username}" doesn't exist`); - } - } - - // 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 - let shouldBeFeatured = {}; - - if (env.REQUIRE_FEATURED_ASSISTANTS === "true" && !(locals.user?.isAdmin && showUnfeatured)) { - if (!user) { - // only show featured assistants on the community page - shouldBeFeatured = { review: ReviewStatus.APPROVED }; - } else if (!createdByCurrentUser) { - // on a user page show assistants that have been approved or are pending - shouldBeFeatured = { review: { $in: [ReviewStatus.APPROVED, ReviewStatus.PENDING] } }; - } - } - - const noSpecificSearch = !user && !query; - // fetch the top assistants sorted by user count from biggest to smallest. - // filter by model too if modelId is provided or query if query is provided - // only show assistants that have been used by more than 5 users if no specific search is made - const filter: Filter = { - ...(modelId && { modelId }), - ...(user && { createdById: user._id }), - ...(query && { searchTokens: { $all: generateQueryTokens(query) } }), - ...(noSpecificSearch && { userCount: { $gte: 5 } }), - ...shouldBeFeatured, - }; - - const assistants = await Database.getInstance() - .getCollections() - .assistants.find(filter) - .sort({ - ...(sort === SortKey.TRENDING && { last24HoursCount: -1 }), - userCount: -1, - _id: 1, - }) - .skip(NUM_PER_PAGE * pageIndex) - .limit(NUM_PER_PAGE) - .toArray(); - - const numTotalItems = await Database.getInstance() - .getCollections() - .assistants.countDocuments(filter); - - return { - assistants: jsonSerialize(assistants), - selectedModel: modelId ?? "", - numTotalItems, - numItemsPerPage: NUM_PER_PAGE, - query, - sort, - showUnfeatured, - }; -}; diff --git a/src/routes/assistants/+page.ts b/src/routes/assistants/+page.ts new file mode 100644 index 00000000000..3405142773b --- /dev/null +++ b/src/routes/assistants/+page.ts @@ -0,0 +1,15 @@ +import { base } from "$app/paths"; +import type { GETAssistantsSearchResponse } from "$lib/server/api/routes/groups/assistants"; +import { error } from "@sveltejs/kit"; + +export const load = async ({ url, fetch }) => { + const r = await fetch(`${base}/api/v2/assistants/search?${url.searchParams.toString()}`); + + if (!r.ok) { + throw error(r.status, "Failed to fetch assistants"); + } + + const data = (await r.json()) as GETAssistantsSearchResponse; + + return data; +}; From 8b3ab353fb0cf01570ab8c911aee5b86f991f190 Mon Sep 17 00:00:00 2001 From: Nathan Sarrazin Date: Tue, 4 Mar 2025 13:14:14 +0000 Subject: [PATCH 20/34] refactor(settings): remove waterfall loading --- src/routes/settings/+layout.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/routes/settings/+layout.ts b/src/routes/settings/+layout.ts index 22f77aea863..03a86778c99 100644 --- a/src/routes/settings/+layout.ts +++ b/src/routes/settings/+layout.ts @@ -3,14 +3,12 @@ import { base } from "$app/paths"; import type { Serialize } from "$lib/utils/serialize"; export const load = async ({ parent }) => { - const { assistants } = await parent(); - const reports = await fetch(`${base}/api/v2/user/reports`) .then((res) => res.json() as Promise[]>) .catch(() => []); return { - assistants: (await assistants).map((el) => ({ + assistants: (await parent().then((data) => data.assistants)).map((el) => ({ ...el, reported: reports.some((r) => r.contentId === el._id && r.object === "assistant"), })), From d522f94bdd16b2c121e4d437417c89716eed9d1b Mon Sep 17 00:00:00 2001 From: Nathan Sarrazin Date: Tue, 4 Mar 2025 23:10:42 +0000 Subject: [PATCH 21/34] refactor: main load function --- src/lib/components/NavConversationItem.svelte | 12 +- .../server/api/routes/groups/conversations.ts | 87 ++++++------ src/lib/server/api/routes/groups/user.ts | 32 ++++- src/lib/server/models.ts | 10 +- src/lib/types/ConvSidebar.ts | 2 +- src/lib/utils/fetchJSON.ts | 10 ++ src/routes/+layout.server.ts | 131 ------------------ src/routes/+layout.ts | 82 +++++++++++ 8 files changed, 183 insertions(+), 183 deletions(-) create mode 100644 src/lib/utils/fetchJSON.ts delete mode 100644 src/routes/+layout.server.ts create mode 100644 src/routes/+layout.ts diff --git a/src/lib/components/NavConversationItem.svelte b/src/lib/components/NavConversationItem.svelte index b129a3a7f1c..9b8e4e5b0fb 100644 --- a/src/lib/components/NavConversationItem.svelte +++ b/src/lib/components/NavConversationItem.svelte @@ -39,11 +39,13 @@ Delete {/if} {#if conv.avatarUrl} - Assistant avatar + {#await conv.avatarUrl then avatarUrl} + Assistant avatar + {/await} {conv.title.replace(/\p{Emoji}/gu, "")} {:else if conv.assistantId}

>; + nConversations: number; +}; + export type GETConversationResponse = Pick< Conversation, "messages" | "title" | "model" | "preprompt" | "rootMessageId" | "updatedAt" | "assistantId" @@ -24,43 +29,50 @@ export type GETConversationResponse = Pick< export const conversationGroup = new Elysia().use(authPlugin).group("/conversations", (app) => { return app + .guard({ + as: "scoped", + beforeHandle: async ({ locals }) => { + if (!locals.user?._id && !locals.sessionId) { + return error(401, "Must have a valid session or user"); + } + }, + }) .get( "", async ({ locals, query }) => { - if (locals.user?._id || locals.sessionId) { - const convs = await collections.conversations - .find({ - ...authCondition(locals), - }) - .project>({ - title: 1, - updatedAt: 1, - model: 1, - assistantId: 1, - }) - .sort({ updatedAt: -1 }) - .skip((query.p ?? 0) * CONV_NUM_PER_PAGE) - .limit(CONV_NUM_PER_PAGE) - .toArray(); + const convs = await collections.conversations + .find(authCondition(locals)) + .project>({ + title: 1, + updatedAt: 1, + model: 1, + assistantId: 1, + }) + .sort({ updatedAt: -1 }) + .skip((query.p ?? 0) * CONV_NUM_PER_PAGE) + .limit(CONV_NUM_PER_PAGE) + .toArray(); - if (convs.length === 0) { - return Response.json([]); - } + const nConversations = await collections.conversations.countDocuments( + authCondition(locals) + ); - const res = convs.map((conv) => ({ - _id: conv._id, - id: conv._id, // legacy param iOS - title: conv.title, - updatedAt: conv.updatedAt, - model: conv.model, - modelId: conv.model, // legacy param iOS - assistantId: conv.assistantId, - modelTools: models.find((m) => m.id == conv.model)?.tools ?? false, - })); - return Response.json(res); - } else { - return Response.json({ message: "Must have session cookie" }, { status: 401 }); + if (convs.length === 0) { + return Response.json([]); } + + const res = convs.map((conv) => ({ + _id: conv._id, + id: conv._id, // legacy param iOS + title: conv.title, + updatedAt: conv.updatedAt, + model: conv.model, + modelId: conv.model, // legacy param iOS + assistantId: conv.assistantId, + modelTools: models.find((m) => m.id == conv.model)?.tools ?? false, + })); + + return { conversations: res, nConversations } satisfies GETConversationsResponse; }, { query: t.Object({ @@ -156,13 +168,10 @@ export const conversationGroup = new Elysia().use(authPlugin).group("/conversati return "aa"; }) .delete("", async ({ locals }) => { - if (locals.user?._id || locals.sessionId) { - const res = await collections.conversations.deleteMany({ - ...authCondition(locals), - }); - return res.deletedCount; - } - return 0; + const res = await collections.conversations.deleteMany({ + ...authCondition(locals), + }); + return res.deletedCount; }) .get("/output/:sha256", () => { // todo: get output diff --git a/src/lib/server/api/routes/groups/user.ts b/src/lib/server/api/routes/groups/user.ts index cefb991d76f..c5ec5447ffa 100644 --- a/src/lib/server/api/routes/groups/user.ts +++ b/src/lib/server/api/routes/groups/user.ts @@ -8,6 +8,7 @@ import { DEFAULT_SETTINGS, type SettingsEditable } from "$lib/types/Settings"; import { toolFromConfigs } from "$lib/server/tools"; import { ObjectId } from "mongodb"; import { z } from "zod"; +import type { Assistant } from "$lib/types/Assistant"; export type UserGETFront = { id: string; @@ -32,6 +33,10 @@ export type UserGETSettings = { tools: string[]; }; +export type UserGETAssistants = Array< + Assistant & { _id: string; createdById: string; createdByMe: boolean } +>; + export const userGroup = new Elysia() .use(authPlugin) .post("/login", () => { @@ -191,7 +196,30 @@ export const userGroup = new Elysia() return null; }) - .get("/assistants", () => { - // todo: get user assistants + .get("/assistants", async ({ locals }) => { + const settings = await collections.settings.findOne(authCondition(locals)); + + if (!settings) { + return []; + } + + const userAssistants = + settings?.assistants?.map((assistantId) => assistantId.toString()) ?? []; + + const assistants = await collections.assistants + .find({ + _id: { + $in: [...userAssistants.map((el) => new ObjectId(el))], + }, + }) + .toArray(); + + return assistants.map((el) => ({ + ...el, + _id: el._id.toString(), + createdById: undefined, + createdByMe: + el.createdById.toString() === (locals.user?._id ?? locals.sessionId).toString(), + })); }); }); diff --git a/src/lib/server/models.ts b/src/lib/server/models.ts index be657d43ed2..0d825f73e44 100644 --- a/src/lib/server/models.ts +++ b/src/lib/server/models.ts @@ -14,6 +14,7 @@ import { getTokenizer } from "$lib/utils/getTokenizer"; import { logger } from "$lib/server/logger"; import { ToolResultStatus, type ToolInput } from "$lib/types/Tool"; import { isHuggingChat } from "$lib/utils/isHuggingChat"; +import { fetchJSON } from "$lib/utils/fetchJSON"; type Optional = Pick, K> & Omit; @@ -342,13 +343,12 @@ const addEndpoint = (m: Awaited>) => ({ }); const inferenceApiIds = isHuggingChat - ? await fetch( + ? await fetchJSON<{ id: string }[]>( "https://huggingface.co/api/models?pipeline_tag=text-generation&inference=warm&filter=conversational" ) - .then((r) => r.json()) - .then((json) => json.map((r: { id: string }) => r.id)) - .catch((err) => { - logger.error(err, "Failed to fetch inference API ids"); + .then((arr) => arr.map((r) => r.id)) + .catch(() => { + logger.error("Failed to fetch inference API ids"); return []; }) : []; diff --git a/src/lib/types/ConvSidebar.ts b/src/lib/types/ConvSidebar.ts index 679a41ba641..bf2ec6e2fb5 100644 --- a/src/lib/types/ConvSidebar.ts +++ b/src/lib/types/ConvSidebar.ts @@ -4,5 +4,5 @@ export interface ConvSidebar { updatedAt: Date; model?: string; assistantId?: string; - avatarUrl?: string; + avatarUrl?: string | Promise; } diff --git a/src/lib/utils/fetchJSON.ts b/src/lib/utils/fetchJSON.ts new file mode 100644 index 00000000000..fba3c51e34e --- /dev/null +++ b/src/lib/utils/fetchJSON.ts @@ -0,0 +1,10 @@ +export async function fetchJSON( + url: string, + options?: { fetch?: typeof window.fetch } +): Promise { + const response = await (options?.fetch ?? fetch)(url); + if (!response.ok) { + throw new Error(`Failed to fetch ${url}: ${response.status} ${response.statusText}`); + } + return response.json(); +} diff --git a/src/routes/+layout.server.ts b/src/routes/+layout.server.ts deleted file mode 100644 index a3e32fda485..00000000000 --- a/src/routes/+layout.server.ts +++ /dev/null @@ -1,131 +0,0 @@ -import type { LayoutServerLoad } from "./$types"; -import { collections } from "$lib/server/database"; -import type { Conversation } from "$lib/types/Conversation"; -import { UrlDependency } from "$lib/types/UrlDependency"; -import { defaultModel, models } from "$lib/server/models"; -import { authCondition } from "$lib/server/auth"; -import { ObjectId } from "mongodb"; -import type { ConvSidebar } from "$lib/types/ConvSidebar"; -import type { ToolFront } from "$lib/types/Tool"; -import { base } from "$app/paths"; -import { jsonSerialize } from "../lib/utils/serialize"; -import type { FeatureFlags } from "$lib/server/api/routes/groups/misc"; -import type { UserGETFront, UserGETSettings } from "$lib/server/api/routes/groups/user"; -import type { GETModelsResponse, GETOldModelsResponse } from "$lib/server/api/routes/groups/models"; - -export const load: LayoutServerLoad = async ({ locals, depends, fetch }) => { - depends(UrlDependency.ConversationList); - - const settings = await collections.settings.findOne(authCondition(locals)); - - const assistantActive = !models.map(({ id }) => id).includes(settings?.activeModel ?? ""); - - const assistant = assistantActive - ? await collections.assistants.findOne({ - _id: new ObjectId(settings?.activeModel), - }) - : null; - - const nConversations = await collections.conversations.countDocuments(authCondition(locals)); - - const conversations = - nConversations === 0 - ? Promise.resolve([]) - : fetch(`${base}/api/v2/conversations`) - .then((res) => res.json()) - .then( - ( - convs: Pick[] - ) => - convs.map((conv) => ({ - ...conv, - updatedAt: new Date(conv.updatedAt), - })) - ); - - const userAssistants = settings?.assistants?.map((assistantId) => assistantId.toString()) ?? []; - const userAssistantsSet = new Set(userAssistants); - - const assistants = conversations.then((conversations) => - collections.assistants - .find({ - _id: { - $in: [ - ...userAssistants.map((el) => new ObjectId(el)), - ...(conversations.map((conv) => conv.assistantId).filter((el) => !!el) as ObjectId[]), - ], - }, - }) - .toArray() - ); - - return { - nConversations, - conversations: await conversations.then( - async (convs) => - await Promise.all( - convs.map(async (conv) => { - if (settings?.hideEmojiOnSidebar) { - conv.title = conv.title.replace(/\p{Emoji}/gu, ""); - } - - // remove invalid unicode and trim whitespaces - conv.title = conv.title.replace(/\uFFFD/gu, "").trimStart(); - - let avatarUrl: string | undefined = undefined; - - if (conv.assistantId) { - const hash = ( - await collections.assistants.findOne({ - _id: new ObjectId(conv.assistantId), - }) - )?.avatar; - if (hash) { - avatarUrl = `/settings/assistants/${conv.assistantId}/avatar.jpg?hash=${hash}`; - } - } - - return { - id: conv._id.toString(), - title: conv.title, - model: conv.model ?? defaultModel, - updatedAt: conv.updatedAt, - assistantId: conv.assistantId?.toString(), - avatarUrl, - } satisfies ConvSidebar; - }) - ) - ), - models: await fetch(`${base}/api/v2/models`).then( - (res) => res.json() as Promise - ), - oldModels: await fetch(`${base}/api/v2/models/old`).then( - (res) => res.json() as Promise - ), - tools: await fetch(`${base}/api/v2/tools/active`).then( - (res) => res.json() as Promise - ), - communityToolCount: await fetch(`${base}/api/v2/tools/count`).then( - (res) => res.json() as Promise - ), - assistants: assistants.then((assistants) => - assistants - .filter((el) => userAssistantsSet.has(el._id.toString())) - .map((el) => ({ - ...el, - _id: el._id.toString(), - createdById: undefined, - createdByMe: - el.createdById.toString() === (locals.user?._id ?? locals.sessionId).toString(), - })) - ), - assistant: assistant ? jsonSerialize(assistant) : undefined, - user: await fetch(`${base}/api/v2/user`).then((res) => res.json() as Promise), - settings: await fetch(`${base}/api/v2/user/settings`).then( - (res) => res.json() as Promise - ), - ...(await fetch(`${base}/api/v2/feature-flags`).then( - (res) => res.json() as Promise - )), - }; -}; diff --git a/src/routes/+layout.ts b/src/routes/+layout.ts new file mode 100644 index 00000000000..54ab812b564 --- /dev/null +++ b/src/routes/+layout.ts @@ -0,0 +1,82 @@ +import { UrlDependency } from "$lib/types/UrlDependency"; +import type { ConvSidebar } from "$lib/types/ConvSidebar"; +import type { ToolFront } from "$lib/types/Tool"; +import { base } from "$app/paths"; +import { jsonSerialize, type Serialize } from "../lib/utils/serialize"; +import type { FeatureFlags } from "$lib/server/api/routes/groups/misc"; +import type { + UserGETAssistants, + UserGETFront, + UserGETSettings, +} from "$lib/server/api/routes/groups/user"; +import type { GETModelsResponse, GETOldModelsResponse } from "$lib/server/api/routes/groups/models"; +import type { Assistant } from "$lib/types/Assistant"; +import type { GETConversationsResponse } from "$lib/server/api/routes/groups/conversations"; +import { fetchJSON } from "$lib/utils/fetchJSON"; + +export const load = async ({ depends, fetch }) => { + depends(UrlDependency.ConversationList); + + const settings = await fetchJSON(`${base}/api/v2/user/settings`, { fetch }); + const models = await fetchJSON(`${base}/api/v2/models`, { fetch }); + const defaultModel = models[0]; + + // if the active model is not in the list of models, its probably an assistant + // so we fetch it + const assistantActive = !models.map(({ id }) => id).includes(settings?.activeModel ?? ""); + + const assistant = assistantActive + ? await fetchJSON>(`${base}/api/v2/assistants/${settings?.activeModel}`, { + fetch, + }) + : null; + + const { conversations, nConversations } = await fetchJSON( + `${base}/api/v2/conversations`, + { fetch } + ).then(({ conversations, nConversations }) => { + return { + nConversations, + conversations: (conversations ?? []).map((conv) => { + if (settings?.hideEmojiOnSidebar) { + conv.title = conv.title.replace(/\p{Emoji}/gu, ""); + } + + // remove invalid unicode and trim whitespaces + conv.title = conv.title.replace(/\uFFFD/gu, "").trimStart(); + + return { + id: conv._id.toString(), + title: conv.title, + model: conv.model ?? defaultModel, + updatedAt: conv.updatedAt, + ...(conv.assistantId + ? { + assistantId: conv.assistantId.toString(), + avatarUrl: fetch(`${base}/api/v2/assistants/${conv.assistantId}`) + .then((res) => res.json() as Promise>) + .then( + (assistant) => + `/settings/assistants/${conv.assistantId}/avatar.jpg?hash=${assistant.avatar}` + ), + } + : {}), + } satisfies ConvSidebar; + }), + }; + }); + + return { + nConversations, + conversations, + assistant: assistant ? jsonSerialize(assistant) : undefined, + assistants: await fetchJSON(`${base}/api/v2/user/assistants`, { fetch }), + models: await fetchJSON(`${base}/api/v2/models`, { fetch }), + oldModels: await fetchJSON(`${base}/api/v2/models/old`, { fetch }), + tools: await fetchJSON(`${base}/api/v2/tools/active`, { fetch }), + communityToolCount: await fetchJSON(`${base}/api/v2/tools/count`, { fetch }), + user: await fetchJSON(`${base}/api/v2/user`, { fetch }), + settings, + ...(await fetchJSON(`${base}/api/v2/feature-flags`, { fetch })), + }; +}; From 3caaff4164899f495bbd2ed7a7ce199d0aebecfc Mon Sep 17 00:00:00 2001 From: Nathan Sarrazin Date: Tue, 4 Mar 2025 23:57:50 +0000 Subject: [PATCH 22/34] fix: types --- src/lib/utils/fetchJSON.ts | 4 +++- src/routes/+layout.ts | 9 +++++++-- .../(nav)/assistants/[assistantId]/edit/+page.svelte | 11 ++++++++++- 3 files changed, 20 insertions(+), 4 deletions(-) diff --git a/src/lib/utils/fetchJSON.ts b/src/lib/utils/fetchJSON.ts index fba3c51e34e..54441b65b0c 100644 --- a/src/lib/utils/fetchJSON.ts +++ b/src/lib/utils/fetchJSON.ts @@ -1,7 +1,9 @@ +import type { Serialize } from "./serialize"; + export async function fetchJSON( url: string, options?: { fetch?: typeof window.fetch } -): Promise { +): Promise> { const response = await (options?.fetch ?? fetch)(url); if (!response.ok) { throw new Error(`Failed to fetch ${url}: ${response.status} ${response.statusText}`); diff --git a/src/routes/+layout.ts b/src/routes/+layout.ts index 54ab812b564..0f7e8acf645 100644 --- a/src/routes/+layout.ts +++ b/src/routes/+layout.ts @@ -49,7 +49,7 @@ export const load = async ({ depends, fetch }) => { id: conv._id.toString(), title: conv.title, model: conv.model ?? defaultModel, - updatedAt: conv.updatedAt, + updatedAt: new Date(conv.updatedAt), ...(conv.assistantId ? { assistantId: conv.assistantId.toString(), @@ -76,7 +76,12 @@ export const load = async ({ depends, fetch }) => { tools: await fetchJSON(`${base}/api/v2/tools/active`, { fetch }), communityToolCount: await fetchJSON(`${base}/api/v2/tools/count`, { fetch }), user: await fetchJSON(`${base}/api/v2/user`, { fetch }), - settings, + settings: { + ...settings, + ethicsModalAcceptedAt: settings.ethicsModalAcceptedAt + ? new Date(settings.ethicsModalAcceptedAt) + : null, + }, ...(await fetchJSON(`${base}/api/v2/feature-flags`, { fetch })), }; }; diff --git a/src/routes/settings/(nav)/assistants/[assistantId]/edit/+page.svelte b/src/routes/settings/(nav)/assistants/[assistantId]/edit/+page.svelte index 1b0ac21b3a1..20ce27030c7 100644 --- a/src/routes/settings/(nav)/assistants/[assistantId]/edit/+page.svelte +++ b/src/routes/settings/(nav)/assistants/[assistantId]/edit/+page.svelte @@ -12,4 +12,13 @@ let assistant = data.assistants.find((el) => el._id.toString() === page.params.assistantId); - + From c3682cfd5531eac43f1ec57d7f799d00cc33a311 Mon Sep 17 00:00:00 2001 From: Nathan Sarrazin Date: Wed, 5 Mar 2025 11:10:31 +0000 Subject: [PATCH 23/34] feat: improve fetchJSON to handle empty responses --- src/lib/server/models.ts | 5 +++-- src/lib/utils/fetchJSON.ts | 17 +++++++++++++++-- src/routes/+layout.ts | 3 ++- 3 files changed, 20 insertions(+), 5 deletions(-) diff --git a/src/lib/server/models.ts b/src/lib/server/models.ts index 0d825f73e44..97438d4062a 100644 --- a/src/lib/server/models.ts +++ b/src/lib/server/models.ts @@ -344,9 +344,10 @@ const addEndpoint = (m: Awaited>) => ({ const inferenceApiIds = isHuggingChat ? await fetchJSON<{ id: string }[]>( - "https://huggingface.co/api/models?pipeline_tag=text-generation&inference=warm&filter=conversational" + "https://huggingface.co/api/models?pipeline_tag=text-generation&inference=warm&filter=conversational", + { allowNull: true } ) - .then((arr) => arr.map((r) => r.id)) + .then((arr) => arr?.map((r) => r.id) || []) .catch(() => { logger.error("Failed to fetch inference API ids"); return []; diff --git a/src/lib/utils/fetchJSON.ts b/src/lib/utils/fetchJSON.ts index 54441b65b0c..b3b0de4c7aa 100644 --- a/src/lib/utils/fetchJSON.ts +++ b/src/lib/utils/fetchJSON.ts @@ -2,11 +2,24 @@ import type { Serialize } from "./serialize"; export async function fetchJSON( url: string, - options?: { fetch?: typeof window.fetch } + options?: { + fetch?: typeof window.fetch; + allowNull?: boolean; + } ): Promise> { const response = await (options?.fetch ?? fetch)(url); if (!response.ok) { throw new Error(`Failed to fetch ${url}: ${response.status} ${response.statusText}`); } - return response.json(); + + // Handle empty responses (which parse to null) + const text = await response.text(); + if (!text || text.trim() === "") { + if (options?.allowNull) { + return null as Serialize; + } + throw new Error(`Received empty response from ${url} but allowNull is not set to true`); + } + + return JSON.parse(text); } diff --git a/src/routes/+layout.ts b/src/routes/+layout.ts index 0f7e8acf645..fdd3444c199 100644 --- a/src/routes/+layout.ts +++ b/src/routes/+layout.ts @@ -28,6 +28,7 @@ export const load = async ({ depends, fetch }) => { const assistant = assistantActive ? await fetchJSON>(`${base}/api/v2/assistants/${settings?.activeModel}`, { fetch, + allowNull: true, }) : null; @@ -75,7 +76,7 @@ export const load = async ({ depends, fetch }) => { oldModels: await fetchJSON(`${base}/api/v2/models/old`, { fetch }), tools: await fetchJSON(`${base}/api/v2/tools/active`, { fetch }), communityToolCount: await fetchJSON(`${base}/api/v2/tools/count`, { fetch }), - user: await fetchJSON(`${base}/api/v2/user`, { fetch }), + user: await fetchJSON(`${base}/api/v2/user`, { fetch, allowNull: true }), settings: { ...settings, ethicsModalAcceptedAt: settings.ethicsModalAcceptedAt From 8a9930430fbba599787743970023affcb235678d Mon Sep 17 00:00:00 2001 From: Nathan Sarrazin Date: Wed, 5 Mar 2025 13:32:47 +0000 Subject: [PATCH 24/34] fix: issues with page loading & assistant avatars --- src/lib/components/NavConversationItem.svelte | 12 +++++++----- src/lib/types/ConvSidebar.ts | 2 +- src/lib/utils/serialize.ts | 2 +- src/routes/+layout.ts | 12 ++++++++---- 4 files changed, 17 insertions(+), 11 deletions(-) diff --git a/src/lib/components/NavConversationItem.svelte b/src/lib/components/NavConversationItem.svelte index 9b8e4e5b0fb..c8c69d16c64 100644 --- a/src/lib/components/NavConversationItem.svelte +++ b/src/lib/components/NavConversationItem.svelte @@ -40,11 +40,13 @@ {/if} {#if conv.avatarUrl} {#await conv.avatarUrl then avatarUrl} - Assistant avatar + {#if avatarUrl} + Assistant avatar + {/if} {/await} {conv.title.replace(/\p{Emoji}/gu, "")} {:else if conv.assistantId} diff --git a/src/lib/types/ConvSidebar.ts b/src/lib/types/ConvSidebar.ts index bf2ec6e2fb5..e3c91fd489f 100644 --- a/src/lib/types/ConvSidebar.ts +++ b/src/lib/types/ConvSidebar.ts @@ -4,5 +4,5 @@ export interface ConvSidebar { updatedAt: Date; model?: string; assistantId?: string; - avatarUrl?: string | Promise; + avatarUrl?: string | Promise; } diff --git a/src/lib/utils/serialize.ts b/src/lib/utils/serialize.ts index 20e5250f2cc..4fb4135f428 100644 --- a/src/lib/utils/serialize.ts +++ b/src/lib/utils/serialize.ts @@ -1,4 +1,4 @@ -import { ObjectId } from "mongodb"; +import type { ObjectId } from "mongodb"; export type Serialize = T extends ObjectId | Date ? string diff --git a/src/routes/+layout.ts b/src/routes/+layout.ts index fdd3444c199..cfd78491a4a 100644 --- a/src/routes/+layout.ts +++ b/src/routes/+layout.ts @@ -56,10 +56,14 @@ export const load = async ({ depends, fetch }) => { assistantId: conv.assistantId.toString(), avatarUrl: fetch(`${base}/api/v2/assistants/${conv.assistantId}`) .then((res) => res.json() as Promise>) - .then( - (assistant) => - `/settings/assistants/${conv.assistantId}/avatar.jpg?hash=${assistant.avatar}` - ), + .then((assistant) => { + console.log(assistant); + if (!assistant.avatar) { + return undefined; + } + + return `/settings/assistants/${conv.assistantId}/avatar.jpg?hash=${assistant.avatar}`; + }), } : {}), } satisfies ConvSidebar; From 244941c2c1fe41ee5892a34e42ca226713b35f95 Mon Sep 17 00:00:00 2001 From: Nathan Sarrazin Date: Wed, 5 Mar 2025 13:54:49 +0000 Subject: [PATCH 25/34] refactor(api): remove unused Eden fetch utility --- src/lib/utils/api.ts | 12 ------------ 1 file changed, 12 deletions(-) delete mode 100644 src/lib/utils/api.ts diff --git a/src/lib/utils/api.ts b/src/lib/utils/api.ts deleted file mode 100644 index ec5cced9cb4..00000000000 --- a/src/lib/utils/api.ts +++ /dev/null @@ -1,12 +0,0 @@ -import type { App } from "$lib/server/api/server"; -import { edenFetch } from "@elysiajs/eden"; - -type Fetch = typeof fetch; - -export function useEdenFetch({ fetch }: { fetch: Fetch }) { - const app = edenFetch("http://localhost:5173/chat/api/v2", { - fetcher: fetch, - }); - - return app; -} From d48431d6ecd12fe457868b58efbf8b529c5d29c6 Mon Sep 17 00:00:00 2001 From: Nathan Sarrazin Date: Wed, 5 Mar 2025 14:55:07 +0000 Subject: [PATCH 26/34] refactor(routes): improve conversation page loading and error handling --- src/routes/+layout.ts | 1 - src/routes/conversation/[id]/+page.ts | 18 ++++++++---------- 2 files changed, 8 insertions(+), 11 deletions(-) diff --git a/src/routes/+layout.ts b/src/routes/+layout.ts index cfd78491a4a..beacb5d4bd1 100644 --- a/src/routes/+layout.ts +++ b/src/routes/+layout.ts @@ -57,7 +57,6 @@ export const load = async ({ depends, fetch }) => { avatarUrl: fetch(`${base}/api/v2/assistants/${conv.assistantId}`) .then((res) => res.json() as Promise>) .then((assistant) => { - console.log(assistant); if (!assistant.avatar) { return undefined; } diff --git a/src/routes/conversation/[id]/+page.ts b/src/routes/conversation/[id]/+page.ts index 54e1ac535e0..ed4567081f6 100644 --- a/src/routes/conversation/[id]/+page.ts +++ b/src/routes/conversation/[id]/+page.ts @@ -1,19 +1,17 @@ import { base } from "$app/paths"; import type { GETConversationResponse } from "$lib/server/api/routes/groups/conversations.js"; import { UrlDependency } from "$lib/types/UrlDependency"; -import { error } from "@sveltejs/kit"; +import { fetchJSON } from "$lib/utils/fetchJSON.js"; +import { redirect } from "@sveltejs/kit"; export const load = async ({ params, depends, fetch }) => { depends(UrlDependency.Conversation); - const r = await fetch(`${base}/api/v2/conversations/${params.id}`); - - if (!r.ok) { - console.log({ r }); - error(r.status, "Failed to fetch conversation"); + try { + return await fetchJSON(`${base}/api/v2/conversations/${params.id}`, { + fetch, + }); + } catch { + redirect(302, "/"); } - - const data = await r.json(); - - return data as GETConversationResponse; }; From 9a02c6f9994b5b4d040900374013d8f9c21385dc Mon Sep 17 00:00:00 2001 From: Nathan Sarrazin Date: Thu, 6 Mar 2025 01:35:59 +0100 Subject: [PATCH 27/34] 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 --- src/lib/components/DisclaimerModal.svelte | 33 ++++++++------- src/lib/components/LoginModal.svelte | 11 +++-- src/lib/components/NavMenu.svelte | 40 ++++++++++++------- src/routes/login/+page.server.ts | 27 ------------- src/routes/login/+server.ts | 28 +++++++++++++ .../callback/{+page.server.ts => +server.ts} | 18 ++++----- src/routes/logout/+page.server.ts | 20 ---------- src/routes/logout/+server.ts | 18 +++++++++ 8 files changed, 107 insertions(+), 88 deletions(-) delete mode 100644 src/routes/login/+page.server.ts create mode 100644 src/routes/login/+server.ts rename src/routes/login/callback/{+page.server.ts => +server.ts} (78%) delete mode 100644 src/routes/logout/+page.server.ts create mode 100644 src/routes/logout/+server.ts diff --git a/src/lib/components/DisclaimerModal.svelte b/src/lib/components/DisclaimerModal.svelte index 830b3162817..f8170d7ab5d 100644 --- a/src/lib/components/DisclaimerModal.svelte +++ b/src/lib/components/DisclaimerModal.svelte @@ -55,20 +55,25 @@ {/if} {#if page.data.loginEnabled} -
- -
+ {/if}
diff --git a/src/lib/components/LoginModal.svelte b/src/lib/components/LoginModal.svelte index f78188fa68f..8f46878e654 100644 --- a/src/lib/components/LoginModal.svelte +++ b/src/lib/components/LoginModal.svelte @@ -27,10 +27,15 @@

{ + const response = await fetch(`${base}/login`, { + method: "POST", + }); + if (response.ok) { + window.location.href = await response.text(); + } + }} > {#if page.data.loginRequired} + {/if} -
+ {/if} {#if canLogin} -
- -
+ {/if} {#if page.data.loginEnabled} -
+ {/if}
diff --git a/src/lib/components/LoginModal.svelte b/src/lib/components/LoginModal.svelte index 8f46878e654..ba22c16989d 100644 --- a/src/lib/components/LoginModal.svelte +++ b/src/lib/components/LoginModal.svelte @@ -26,20 +26,10 @@ {envPublic.PUBLIC_APP_GUEST_MESSAGE}

-
{ - const response = await fetch(`${base}/login`, { - method: "POST", - }); - if (response.ok) { - window.location.href = await response.text(); - } - }} - > +
{#if page.data.loginRequired} - + {:else}
diff --git a/src/lib/components/NavMenu.svelte b/src/lib/components/NavMenu.svelte index 8790dab5fc9..cac29d69830 100644 --- a/src/lib/components/NavMenu.svelte +++ b/src/lib/components/NavMenu.svelte @@ -165,20 +165,12 @@ {/if} {#if canLogin} - + {/if}