Skip to content

refactor: new API & universal load functions #1743

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 72 commits into from
Jun 5, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
72 commits
Select commit Hold shift + click to select a range
8ac33f0
feat(API): refactor API with Elysia
nsarrazin Feb 14, 2025
db7070a
Merge branch 'main' into feat/api_elysia_setup
nsarrazin Feb 14, 2025
2dfa176
Merge branch 'main' into feat/api_elysia_setup
nsarrazin Feb 17, 2025
9276111
Merge branch 'main' into feat/api_elysia_setup
nsarrazin Feb 17, 2025
57c6db5
feat: initial elysia setup
nsarrazin Feb 20, 2025
46eec82
feat: replace conv/[id] load function with universal
nsarrazin Feb 20, 2025
43eb901
fix: delete v1 catchall
nsarrazin Feb 20, 2025
8df50a2
wip
nsarrazin Feb 23, 2025
5b73d6a
Merge branch 'main' into feat/api_elysia_setup
nsarrazin Feb 24, 2025
7bb2650
fix: response type
nsarrazin Feb 24, 2025
e53d2a6
Merge branch 'main' into feat/api_elysia_setup
nsarrazin Feb 24, 2025
461b864
Merge branch 'main' into feat/api_elysia_setup
nsarrazin Feb 26, 2025
e87e88a
fix: add cors
nsarrazin Feb 28, 2025
fbeeb84
feat: more routes in tools & assistants
nsarrazin Feb 28, 2025
ae1a5a9
refacto: use normal svelte fetch in `/assistant/[assistantId]`
nsarrazin Feb 28, 2025
8818fc8
refacto: use normal svelte fetch in `conversation/[id]`
nsarrazin Feb 28, 2025
ac60722
feat: use universal load function for `tools/[toolId]`
nsarrazin Feb 28, 2025
0d40d92
Merge branch 'main' into feat/api_elysia_setup
nsarrazin Mar 3, 2025
924e943
Merge branch 'main' into feat/api_elysia_setup
nsarrazin Mar 3, 2025
2a54958
Merge branch 'main' into feat/api_elysia_setup
nsarrazin Mar 3, 2025
f8731b3
wip: removing more server load function stuff
nsarrazin Mar 4, 2025
eb3550c
feat: more routes to universal load functions
nsarrazin Mar 4, 2025
16fb59a
feat: more routes to universal
nsarrazin Mar 4, 2025
e89fd95
refactor: move tools loading to API endpoint
nsarrazin Mar 4, 2025
1b513bf
refactor(api): implement tools search API endpoint and move load func…
nsarrazin Mar 4, 2025
089c1eb
fix: types on tool search
nsarrazin Mar 4, 2025
91d15f2
refactor: update assistant route and remove redundant page load function
nsarrazin Mar 4, 2025
585d411
refactor(api): move assistants page load function to api call
nsarrazin Mar 4, 2025
8b3ab35
refactor(settings): remove waterfall loading
nsarrazin Mar 4, 2025
d522f94
refactor: main load function
nsarrazin Mar 4, 2025
3caaff4
fix: types
nsarrazin Mar 4, 2025
c3682cf
feat: improve fetchJSON to handle empty responses
nsarrazin Mar 5, 2025
8a99304
fix: issues with page loading & assistant avatars
nsarrazin Mar 5, 2025
244941c
refactor(api): remove unused Eden fetch utility
nsarrazin Mar 5, 2025
d48431d
refactor(routes): improve conversation page loading and error handling
nsarrazin Mar 5, 2025
9a02c6f
feat(api): migrate login and logout to API routes (#1703)
nsarrazin Mar 6, 2025
ce435bb
refactor(api): update import aliases and configuration for API routes
nsarrazin Mar 6, 2025
b336351
Merge branch 'main' into feat/api_elysia_setup
nsarrazin Mar 12, 2025
19ab47b
refactor: update conversation handling to use generic tree structure
nsarrazin Mar 12, 2025
e3ff38f
fix: specify message type in ChatWindow component
nsarrazin Mar 12, 2025
5224f1b
Merge branch 'main' into feat/api_elysia_setup
nsarrazin Mar 13, 2025
4aa189f
Merge branch 'main' into feat/api_elysia_setup
nsarrazin Mar 20, 2025
b7dbd5f
Merge branch 'main' into feat/api_elysia_setup
nsarrazin Mar 24, 2025
7f01d47
feat: make login simpler with GET's
nsarrazin Mar 24, 2025
3424d8b
fix: debug logs
nsarrazin Mar 25, 2025
275ef22
Merge branch 'main' into feat/api_elysia_setup
nsarrazin Apr 4, 2025
6de2e3c
Merge branch 'main' into feat/api_elysia_setup
nsarrazin Apr 11, 2025
f6e8780
fix: isAdmin flag
nsarrazin Apr 11, 2025
901c1b3
refactor: remove debug route
nsarrazin Apr 11, 2025
abda8d8
Merge branch 'main' into feat/api_elysia_setup
nsarrazin May 27, 2025
8577eab
fix: use config manager in api routes
nsarrazin May 27, 2025
84a39c9
chores: use latest elysia
nsarrazin May 27, 2025
0d72143
wip
nsarrazin May 27, 2025
012c1b3
feat: working with different origin
nsarrazin May 28, 2025
7c316e4
refactor: update API routes to throw errors for unimplemented features
nsarrazin May 28, 2025
6935336
Merge branch 'main' into feat/api_elysia_setup
nsarrazin May 28, 2025
6e8031a
fix: use hook for public config
nsarrazin May 28, 2025
56f7555
refactor: use api client for user reports in settings load function
nsarrazin May 28, 2025
873e63f
merge latest main
nsarrazin Jun 4, 2025
5a7ea2b
fix: deps
nsarrazin Jun 4, 2025
ccb5f99
feat: get rid of last fetchJSON call in load function
nsarrazin Jun 5, 2025
4402c90
cors setup
nsarrazin Jun 5, 2025
7fa358c
Merge branch 'main' into feat/api_elysia_setup
nsarrazin Jun 5, 2025
6a98f5e
feat: use api client side
nsarrazin Jun 5, 2025
67cc106
feat: use api for assistant loading
nsarrazin Jun 5, 2025
efb3660
feat: use api client for assistant search
nsarrazin Jun 5, 2025
57608d0
fix: lint
nsarrazin Jun 5, 2025
0cbd052
feat: use api client for tool
nsarrazin Jun 5, 2025
4b40fe7
feat: rename client hook and use for deleting all convs
nsarrazin Jun 5, 2025
d3a7dc5
fix: let non-authed user set their model
nsarrazin Jun 5, 2025
fa63805
Merge branch 'main' into feat/api_elysia_setup
nsarrazin Jun 5, 2025
57e7ca6
feat: bump minor for elysia API
nsarrazin Jun 5, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5,091 changes: 1,228 additions & 3,863 deletions package-lock.json

Large diffs are not rendered by default.

9 changes: 7 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "chat-ui",
"version": "0.9.5",
"version": "0.10.0",
"private": true,
"packageManager": "[email protected]",
"scripts": {
Expand All @@ -18,6 +18,9 @@
"prepare": "husky"
},
"devDependencies": {
"@elysiajs/cors": "^1.3.3",
"@elysiajs/eden": "^1.3.2",
"@elysiajs/node": "^1.2.6",
"@faker-js/faker": "^8.4.1",
"@iconify-json/carbon": "^1.1.16",
"@iconify-json/eos-icons": "^1.1.6",
Expand All @@ -41,6 +44,7 @@
"@typescript-eslint/eslint-plugin": "^6.x",
"@typescript-eslint/parser": "^6.x",
"dompurify": "^3.2.4",
"elysia": "^1.3.2",
"eslint": "^8.28.0",
"eslint-config-prettier": "^8.5.0",
"eslint-plugin-svelte": "^2.45.1",
Expand Down Expand Up @@ -71,6 +75,7 @@
"dependencies": {
"@aws-sdk/credential-providers": "^3.592.0",
"@cliqz/adblocker-playwright": "^1.34.0",
"@elysiajs/swagger": "^1.3.0",
"@gradio/client": "^1.8.0",
"@huggingface/hub": "^0.5.1",
"@huggingface/inference": "^3.12.1",
Expand All @@ -86,7 +91,7 @@
"date-fns": "^2.29.3",
"dotenv": "^16.5.0",
"express": "^4.21.2",
"file-type": "^19.4.1",
"file-type": "^21.0.0",
"google-auth-library": "^9.13.0",
"handlebars": "^4.7.8",
"highlight.js": "^11.7.0",
Expand Down
162 changes: 53 additions & 109 deletions src/hooks.server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,20 +2,19 @@ import { config, ready } from "$lib/server/config";
import type { Handle, HandleServerError, ServerInit } 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";
import { building, dev } from "$app/environment";
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";
import { adminTokenManager } from "$lib/server/adminToken";
import { isHostLocalhost } from "$lib/server/isURLLocal";

export const init: ServerInit = async () => {
// Wait for config to be fully loaded
Expand Down Expand Up @@ -120,108 +119,13 @@ export const handle: Handle = async ({ event, resolve }) => {
}
}

const token = event.cookies.get(config.COOKIE_NAME);

// if the trusted email header is set we use it to get the user email
const email = config.TRUSTED_EMAIL_HEADER
? event.request.headers.get(config.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/`) &&
config.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;
}
}
}
const auth = await authenticateRequest(
{ type: "svelte", value: event.request.headers },
{ type: "svelte", value: event.cookies }
);

if (!sessionId || !secretSessionId) {
secretSessionId = crypto.randomUUID();
sessionId = await sha256(secretSessionId);

if (await collections.sessions.findOne({ sessionId })) {
return errorResponse(500, "Session ID collision");
}
}

event.locals.sessionId = sessionId;
event.locals.user = auth.user || undefined;
event.locals.sessionId = auth.sessionId;

event.locals.isAdmin =
event.locals.user?.isAdmin || adminTokenManager.isAdmin(event.locals.sessionId);
Expand Down Expand Up @@ -254,12 +158,16 @@ 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);
if (
event.request.method === "POST" ||
event.url.pathname.startsWith(`${base}/login`) ||
event.url.pathname.startsWith(`${base}/login/callback`)
) {
// if the request is a POST request or login-related we refresh the cookie
refreshSessionCookie(event.cookies, auth.secretSessionId);

await collections.sessions.updateOne(
{ sessionId },
{ sessionId: auth.sessionId },
{ $set: { updatedAt: new Date(), expiresAt: addWeeks(new Date(), 2) } }
);
}
Expand Down Expand Up @@ -309,12 +217,48 @@ export const handle: Handle = async ({ event, resolve }) => {

return chunk.html.replace("%gaId%", config.PUBLIC_GOOGLE_ANALYTICS_ID);
},
filterSerializedResponseHeaders: (header) => {
return header.includes("content-type");
},
});

// Add CSP header to disallow framing if ALLOW_IFRAME is not "true"
if (config.ALLOW_IFRAME !== "true") {
response.headers.append("Content-Security-Policy", "frame-ancestors 'none';");
}

if (
event.url.pathname.startsWith(`${base}/login/callback`) ||
event.url.pathname.startsWith(`${base}/login`)
) {
response.headers.append("Cache-Control", "no-store");
}

if (event.url.pathname.startsWith(`${base}/api/`)) {
// get origin from the request
const requestOrigin = event.request.headers.get("origin");

// get origin from the config if its defined
let allowedOrigin = config.PUBLIC_ORIGIN ? new URL(config.PUBLIC_ORIGIN).origin : undefined;

if (
dev || // if we're in dev mode
!requestOrigin || // or the origin is null (SSR)
isHostLocalhost(new URL(requestOrigin).hostname) // or the origin is localhost
) {
allowedOrigin = "*"; // allow all origins
} else if (allowedOrigin === requestOrigin) {
allowedOrigin = requestOrigin; // echo back the caller
}

if (allowedOrigin) {
response.headers.set("Access-Control-Allow-Origin", allowedOrigin);
response.headers.set(
"Access-Control-Allow-Methods",
"GET, POST, PUT, PATCH, DELETE, OPTIONS"
);
response.headers.set("Access-Control-Allow-Headers", "Content-Type, Authorization");
}
}
return response;
};
6 changes: 6 additions & 0 deletions src/hooks.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { publicConfigTransporter } from "$lib/utils/PublicConfig.svelte";
import type { Transport } from "@sveltejs/kit";

export const transport: Transport = {
PublicConfig: publicConfigTransporter,
};
55 changes: 55 additions & 0 deletions src/lib/APIClient.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import type { App } from "$api";
import { base } from "$app/paths";
import { treaty, type Treaty } from "@elysiajs/eden";
import { browser } from "$app/environment";

export function useAPIClient({ fetch }: { fetch?: Treaty.Config["fetcher"] } = {}) {
let url;

if (!browser) {
let port;
if (process.argv.includes("--port")) {
port = parseInt(process.argv[process.argv.indexOf("--port") + 1]);
} else {
const mode = process.argv.find((arg) => arg === "preview" || arg === "dev");
if (mode === "preview") {
port = 4173;
} else if (mode === "dev") {
port = 5173;
} else {
port = 3000;
}
}
// Always use localhost for server-side requests to avoid external HTTP calls during SSR
url = `http://localhost:${port}${base}/api/v2`;
} else {
url = `${window.location.origin}${base}/api/v2`;
}
const app = treaty<App>(url, { fetcher: fetch });

return app;
}

export function throwOnErrorNullable<T extends Record<number, unknown>>(
response: Treaty.TreatyResponse<T>
): T[200] {
if (response.error) {
throw new Error(JSON.stringify(response.error));
}

return response.data as T[200];
}

export function throwOnError<T extends Record<number, unknown>>(
response: Treaty.TreatyResponse<T>
): NonNullable<T[200]> {
if (response.error) {
throw new Error(JSON.stringify(response.error));
}

if (response.data === null) {
throw new Error("No data received on API call");
}

return response.data as NonNullable<T[200]>;
}
4 changes: 3 additions & 1 deletion src/lib/components/AssistantSettings.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,16 @@
import CarbonTools from "~icons/carbon/tools";

import { useSettingsStore } from "$lib/stores/settings";
import { publicConfig } from "$lib/utils/PublicConfig.svelte";
import IconInternet from "./icons/IconInternet.svelte";
import TokensCounter from "./TokensCounter.svelte";
import HoverTooltip from "./HoverTooltip.svelte";
import { findCurrentModel } from "$lib/utils/models";
import AssistantToolPicker from "./AssistantToolPicker.svelte";
import { error } from "$lib/stores/errors";
import { goto } from "$app/navigation";
import { usePublicConfig } from "$lib/utils/PublicConfig.svelte";

const publicConfig = usePublicConfig();

type AssistantFront = Omit<Assistant, "_id" | "createdById"> & { _id: string };

Expand Down
30 changes: 15 additions & 15 deletions src/lib/components/DisclaimerModal.svelte
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
<script lang="ts">
import { base } from "$app/paths";
import { page } from "$app/state";
import { publicConfig } from "$lib/utils/PublicConfig.svelte";

import LogoHuggingFaceBorderless from "$lib/components/icons/LogoHuggingFaceBorderless.svelte";
import Modal from "$lib/components/Modal.svelte";
import { useSettingsStore } from "$lib/stores/settings";
import { cookiesAreEnabled } from "$lib/utils/cookiesAreEnabled";
import Logo from "./icons/Logo.svelte";
import { usePublicConfig } from "$lib/utils/PublicConfig.svelte";

const publicConfig = usePublicConfig();

const settings = useSettingsStore();
</script>
Expand Down Expand Up @@ -56,20 +58,18 @@
{/if}
</button>
{#if page.data.loginEnabled}
<form action="{base}/login" target="_parent" method="POST" class="w-full">
<button
type="submit"
class="flex w-full flex-wrap items-center justify-center whitespace-nowrap rounded-full border-2 border-black bg-black px-5 py-2 text-lg font-semibold text-gray-100 transition-colors hover:bg-gray-900"
>
Sign in
{#if publicConfig.isHuggingChat}
<span class="flex items-center">
&nbsp;with <LogoHuggingFaceBorderless classNames="text-xl mr-1 ml-1.5 flex-none" /> Hugging
Face
</span>
{/if}
</button>
</form>
<a
href="{base}/login"
class="flex w-full flex-wrap items-center justify-center whitespace-nowrap rounded-full border-2 border-black bg-black px-5 py-2 text-lg font-semibold text-gray-100 transition-colors hover:bg-gray-900"
>
Sign in
{#if publicConfig.isHuggingChat}
<span class="flex items-center">
&nbsp;with <LogoHuggingFaceBorderless classNames="text-xl mr-1 ml-1.5 flex-none" /> Hugging
Face
</span>
{/if}
</a>
{/if}
</div>
</div>
Expand Down
Loading
Loading