DaFujaTyping coyotte508 HF staff commited on
Commit
0260a08
0 Parent(s):

Duplicate from huggingchat/chat-ui

Browse files

Co-authored-by: Eliott Coyac <coyotte508@users.noreply.huggingface.co>

This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. .env +78 -0
  2. .eslintignore +13 -0
  3. .eslintrc.cjs +25 -0
  4. .github/workflows/lint-and-test.yml +27 -0
  5. .gitignore +10 -0
  6. .npmrc +1 -0
  7. .prettierignore +13 -0
  8. .prettierrc +8 -0
  9. .vscode/settings.json +7 -0
  10. Dockerfile +16 -0
  11. PRIVACY.md +35 -0
  12. README.md +71 -0
  13. package-lock.json +0 -0
  14. package.json +53 -0
  15. postcss.config.js +6 -0
  16. src/app.d.ts +17 -0
  17. src/app.html +73 -0
  18. src/hooks.server.ts +72 -0
  19. src/lib/actions/snapScrollToBottom.ts +54 -0
  20. src/lib/buildPrompt.ts +30 -0
  21. src/lib/components/AnnouncementBanner.svelte +15 -0
  22. src/lib/components/CodeBlock.svelte +27 -0
  23. src/lib/components/CopyToClipBoardBtn.svelte +50 -0
  24. src/lib/components/EthicsModal.svelte +47 -0
  25. src/lib/components/MobileNav.svelte +62 -0
  26. src/lib/components/Modal.svelte +59 -0
  27. src/lib/components/ModelCardMetadata.svelte +48 -0
  28. src/lib/components/ModelsModal.svelte +80 -0
  29. src/lib/components/NavMenu.svelte +109 -0
  30. src/lib/components/Portal.svelte +19 -0
  31. src/lib/components/ScrollToBottomBtn.svelte +46 -0
  32. src/lib/components/SettingsModal.svelte +65 -0
  33. src/lib/components/StopGeneratingBtn.svelte +17 -0
  34. src/lib/components/Switch.svelte +11 -0
  35. src/lib/components/Toast.svelte +19 -0
  36. src/lib/components/Tooltip.svelte +22 -0
  37. src/lib/components/chat/ChatInput.svelte +54 -0
  38. src/lib/components/chat/ChatIntroduction.svelte +84 -0
  39. src/lib/components/chat/ChatMessage.svelte +144 -0
  40. src/lib/components/chat/ChatMessages.svelte +65 -0
  41. src/lib/components/chat/ChatWindow.svelte +102 -0
  42. src/lib/components/icons/IconChevron.svelte +20 -0
  43. src/lib/components/icons/IconCopy.svelte +26 -0
  44. src/lib/components/icons/IconDazzled.svelte +36 -0
  45. src/lib/components/icons/IconLoading.svelte +31 -0
  46. src/lib/components/icons/Logo.svelte +25 -0
  47. src/lib/constants/publicSepToken.ts +1 -0
  48. src/lib/server/abortedGenerations.ts +29 -0
  49. src/lib/server/database.ts +31 -0
  50. src/lib/server/modelEndpoint.ts +32 -0
.env ADDED
@@ -0,0 +1,78 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Use .env.local to change these variables
2
+ # DO NOT EDIT THIS FILE WITH SENSITIVE DATA
3
+
4
+ MONGODB_URL=#your mongodb URL here
5
+ MONGODB_DB_NAME=chat-ui
6
+ COOKIE_NAME=hf-chat
7
+ HF_ACCESS_TOKEN=#hf_<token> from from https://huggingface.co/settings/token
8
+ # 'name', 'userMessageToken', 'assistantMessageToken', 'parameters' are required
9
+ MODELS=`[
10
+ {
11
+ "name": "OpenAssistant/oasst-sft-4-pythia-12b-epoch-3.5",
12
+ "datasetName": "OpenAssistant/oasst1",
13
+ "description": "A good alternative to ChatGPT",
14
+ "websiteUrl": "https://open-assistant.io",
15
+ "userMessageToken": "<|prompter|>",
16
+ "assistantMessageToken": "<|assistant|>",
17
+ "messageEndToken": "</s>",
18
+ "preprompt": "Below are a series of dialogues between various people and an AI assistant. The AI tries to be helpful, polite, honest, sophisticated, emotionally aware, and humble-but-knowledgeable. The assistant is happy to help with almost anything, and will do its best to understand exactly what is needed. It also tries to avoid giving false or misleading information, and it caveats when it isn't entirely sure about the right answer. That said, the assistant is practical and really does its best, and doesn't let caution get too much in the way of being useful.\n-----\n",
19
+ "promptExamples": [
20
+ {
21
+ "title": "Write an email from bullet list",
22
+ "prompt": "As a restaurant owner, write a professional email to the supplier to get these products every week: \n\n- Wine (x10)\n- Eggs (x24)\n- Bread (x12)"
23
+ }, {
24
+ "title": "Code a snake game",
25
+ "prompt": "Code a basic snake game in python, give explanations for each step."
26
+ }, {
27
+ "title": "Assist in a task",
28
+ "prompt": "How do I make a delicious lemon cheesecake?"
29
+ }
30
+ ],
31
+ "parameters": {
32
+ "temperature": 0.9,
33
+ "top_p": 0.95,
34
+ "repetition_penalty": 1.2,
35
+ "top_k": 50,
36
+ "truncate": 1000,
37
+ "max_new_tokens": 1024
38
+ }
39
+ },
40
+ {
41
+ "name":"bigcode/starcoderbase",
42
+ "displayName":"BigCode/StarCoderBase",
43
+ "datasetName":"bigcode/the-stack-dedup",
44
+ "description": "A good model for answering technical questions",
45
+ "websiteUrl":"https://huggingface.co/bigcode/",
46
+ "prepromptUrl": "https://huggingface.co/datasets/coyotte508/bigcodeprompt/raw/main/prompt.txt",
47
+ "promptExamples": [
48
+ {
49
+ "title": "Write a code snippet",
50
+ "prompt": "Write a function that loads a file and filters line starting with \"Star\"?"
51
+ }, {
52
+ "title": "Explain a technical concept",
53
+ "prompt": "What is a Dockerfile?"
54
+ }, {
55
+ "title": "Solve a technical task",
56
+ "prompt": "How to install pytorch with cuda?"
57
+ }
58
+ ],
59
+ "userMessageToken": "\n\nHuman: ",
60
+ "assistantMessageToken": "\n\nAssistant:",
61
+ "parameters": {
62
+ "temperature": 0.1,
63
+ "top_p": 0.9,
64
+ "repetition_penalty": 1.2,
65
+ "truncate": 8000,
66
+ "max_new_tokens": 2000,
67
+ "stop": ["Human:", "-----", "Assistant:"]
68
+ }
69
+ }
70
+ ]`
71
+
72
+ PUBLIC_ORIGIN=#https://hf.co
73
+ PUBLIC_GOOGLE_ANALYTICS_ID=#G-XXXXXXXX / Leave empty to disable
74
+ PUBLIC_DEPRECATED_GOOGLE_ANALYTICS_ID=#UA-XXXXXXXX-X / Leave empty to disable
75
+
76
+ PARQUET_EXPORT_DATASET=
77
+ PARQUET_EXPORT_HF_TOKEN=
78
+ PARQUET_EXPORT_SECRET=
.eslintignore ADDED
@@ -0,0 +1,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ .DS_Store
2
+ node_modules
3
+ /build
4
+ /.svelte-kit
5
+ /package
6
+ .env
7
+ .env.*
8
+ !.env.example
9
+
10
+ # Ignore files for PNPM, NPM and YARN
11
+ pnpm-lock.yaml
12
+ package-lock.json
13
+ yarn.lock
.eslintrc.cjs ADDED
@@ -0,0 +1,25 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ module.exports = {
2
+ root: true,
3
+ parser: "@typescript-eslint/parser",
4
+ extends: ["eslint:recommended", "plugin:@typescript-eslint/recommended", "prettier"],
5
+ plugins: ["svelte3", "@typescript-eslint"],
6
+ ignorePatterns: ["*.cjs"],
7
+ overrides: [{ files: ["*.svelte"], processor: "svelte3/svelte3" }],
8
+ settings: {
9
+ "svelte3/typescript": () => require("typescript"),
10
+ },
11
+ parserOptions: {
12
+ sourceType: "module",
13
+ ecmaVersion: 2020,
14
+ },
15
+ rules: {
16
+ "no-shadow": ["error"],
17
+ "@typescript-eslint/no-explicit-any": "error",
18
+ "@typescript-eslint/no-non-null-assertion": "error",
19
+ },
20
+ env: {
21
+ browser: true,
22
+ es2017: true,
23
+ node: true,
24
+ },
25
+ };
.github/workflows/lint-and-test.yml ADDED
@@ -0,0 +1,27 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ name: Lint and test
2
+ on:
3
+ pull_request:
4
+ push:
5
+ branches:
6
+ - main
7
+
8
+ jobs:
9
+ lint:
10
+ runs-on: ubuntu-latest
11
+ timeout-minutes: 10
12
+
13
+ steps:
14
+ - uses: actions/checkout@v3
15
+
16
+ - uses: actions/setup-node@v3
17
+ with:
18
+ node-version: "18"
19
+ cache: "npm"
20
+ - run: |
21
+ npm install ci
22
+ - name: "Checking lint/format errors"
23
+ run: |
24
+ npm run lint
25
+ - name: "Checking type errors"
26
+ run: |
27
+ npm run check
.gitignore ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ .DS_Store
2
+ node_modules
3
+ /build
4
+ /.svelte-kit
5
+ /package
6
+ .env
7
+ .env.*
8
+ !.env.example
9
+ vite.config.js.timestamp-*
10
+ vite.config.ts.timestamp-*
.npmrc ADDED
@@ -0,0 +1 @@
 
 
1
+ engine-strict=true
.prettierignore ADDED
@@ -0,0 +1,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ .DS_Store
2
+ node_modules
3
+ /build
4
+ /.svelte-kit
5
+ /package
6
+ .env
7
+ .env.*
8
+ !.env.example
9
+
10
+ # Ignore files for PNPM, NPM and YARN
11
+ pnpm-lock.yaml
12
+ package-lock.json
13
+ yarn.lock
.prettierrc ADDED
@@ -0,0 +1,8 @@
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "useTabs": true,
3
+ "trailingComma": "es5",
4
+ "printWidth": 100,
5
+ "plugins": ["prettier-plugin-svelte", "prettier-plugin-tailwindcss"],
6
+ "pluginSearchDirs": ["."],
7
+ "overrides": [{ "files": "*.svelte", "options": { "parser": "svelte" } }]
8
+ }
.vscode/settings.json ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ {
2
+ "editor.formatOnSave": true,
3
+ "editor.defaultFormatter": "esbenp.prettier-vscode",
4
+ "editor.codeActionsOnSave": {
5
+ "source.fixAll": true
6
+ }
7
+ }
Dockerfile ADDED
@@ -0,0 +1,16 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # read the doc: https://huggingface.co/docs/hub/spaces-sdks-docker
2
+ # you will also find guides on how best to write your Dockerfile
3
+
4
+ FROM node:19
5
+
6
+ RUN npm install -g pm2
7
+
8
+ WORKDIR /app
9
+
10
+ COPY --link --chown=1000 . .
11
+
12
+ RUN npm i
13
+
14
+ RUN --mount=type=secret,id=DOTENV_LOCAL,dst=.env.local npm run build
15
+
16
+ CMD pm2 start build/index.js -i $CPU_CORES --no-daemon
PRIVACY.md ADDED
@@ -0,0 +1,35 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ## Privacy
2
+
3
+ > Last updated: May 2nd, 2023
4
+
5
+ In this `v0.1` of HuggingChat, users are not authenticated in any way, i.e. this app doesn't have access to your HF user account even if you're logged in to huggingface.co. The app is only using an anonymous session cookie. ❗️ Warning ❗️ this means if you switch browsers or clear cookies, you will currently lose your conversations.
6
+
7
+ By default, your conversations are shared with the model's authors (for the `v0.1` model, to <a target="_blank" href="https://open-assistant.io/dashboard">Open Assistant</a>) to improve their training data and model over time. Model authors are the custodians of the data collected by their model, even if it's hosted on our platform.
8
+
9
+ If you disable data sharing in your settings, your conversations will not be used for any downstream usage (including for research or model training purposes), and they will only be stored to let you access past conversations. You can click on the Delete icon to delete any past conversation at any moment.
10
+
11
+ 🗓 Please also consult huggingface.co's main privacy policy at https://huggingface.co/privacy. To exercise any of your legal privacy rights, please send an email to privacy@huggingface.co.
12
+
13
+ ## About available LLMs
14
+
15
+ The goal of this app is to showcase that it is now (April 2023) possible to build an open source alternative to ChatGPT. 💪
16
+
17
+ For now, it's running OpenAssistant's [latest LLaMA based model](https://huggingface.co/OpenAssistant/oasst-sft-6-llama-30b-xor) (which is one of the current best open source chat models), but the plan in the longer-term is to expose all good-quality chat models from the Hub.
18
+
19
+ We are not affiliated with Open Assistant, but if you want to contribute to the training data for the next generation of open models, please consider contributing to https://open-assistant.io/ ❤️
20
+
21
+ ## Technical details
22
+
23
+ This app is running in a [Space](https://huggingface.co/docs/hub/spaces-overview), which entails that the code for this UI is open source: https://huggingface.co/spaces/huggingchat/chat-ui/tree/main.
24
+ The inference backend is running [text-generation-inference](https://github.com/huggingface/text-generation-inference) on HuggingFace's Inference API infrastructure.
25
+
26
+ It is therefore possible to deploy a copy of this app to a Space and customize it (swap model, add some UI elements, or store user messages according to your own Terms and conditions)
27
+
28
+ We welcome any feedback on this app: please participate to the public discussion at https://huggingface.co/spaces/huggingchat/chat-ui/discussions
29
+
30
+ <a target="_blank" href="https://huggingface.co/spaces/huggingchat/chat-ui/discussions"><img src="https://huggingface.co/datasets/huggingface/badges/raw/main/open-a-discussion-xl.svg" title="open a discussion"></a>
31
+
32
+ ## Coming soon
33
+
34
+ - LLM watermarking
35
+ - User setting to share conversations with model authors (done ✅)
README.md ADDED
@@ -0,0 +1,71 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ title: chat-ui
3
+ emoji: 🔥
4
+ colorFrom: purple
5
+ colorTo: purple
6
+ sdk: docker
7
+ pinned: false
8
+ license: apache-2.0
9
+ base_path: /chat
10
+ app_port: 3000
11
+ duplicated_from: huggingchat/chat-ui
12
+ ---
13
+
14
+ # Chat UI
15
+
16
+ A chat interface using open source models, eg OpenAssistant.
17
+
18
+ ## Launch
19
+
20
+ ```bash
21
+ npm install
22
+ npm run dev
23
+ ```
24
+
25
+ ## Environment
26
+
27
+ Default configuration is in `.env`. Put custom config and secrets in `.env.local`, it will override the values in `.env`.
28
+
29
+ Check out [.env](./.env) to see what needs to be set.
30
+
31
+ Basically you need to create a `.env.local` with the following contents:
32
+
33
+ ```
34
+ MONGODB_URL=<url to mongo, for example a free MongoDB Atlas sandbox instance>
35
+ HF_ACCESS_TOKEN=<your HF access token from https://huggingface.co/settings/tokens>
36
+ ```
37
+
38
+ ## Duplicating to a Space
39
+
40
+ Create a `DOTENV_LOCAL` secret to your space with the following contents:
41
+
42
+ ```
43
+ MONGODB_URL=<url to mongo, for example a free MongoDB Atlas sandbox instance>
44
+ HF_ACCESS_TOKEN=<your HF access token from https://huggingface.co/settings/tokens>
45
+ ```
46
+
47
+ Where the contents in `<...>` are replaced by the MongoDB URL and your [HF Access Token](https://huggingface.co/settings/tokens).
48
+
49
+ ## Running Local Inference
50
+
51
+ Both the example above use the HF Inference API or HF Endpoints API.
52
+
53
+ If you want to run the model locally, you need to run this inference server locally: https://github.com/huggingface/text-generation-inference
54
+
55
+ And add this to your `.env.local`:
56
+
57
+ ```
58
+ MODELS=`[{"name": "...", "endpoints": [{"url": "127.0.0.1:8080/generate_stream"}]}]`
59
+ ```
60
+
61
+ ## Building
62
+
63
+ To create a production version of your app:
64
+
65
+ ```bash
66
+ npm run build
67
+ ```
68
+
69
+ You can preview the production build with `npm run preview`.
70
+
71
+ > To deploy your app, you may need to install an [adapter](https://kit.svelte.dev/docs/adapters) for your target environment.
package-lock.json ADDED
The diff for this file is too large to render. See raw diff
 
package.json ADDED
@@ -0,0 +1,53 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "name": "chat-ui",
3
+ "version": "0.1.0",
4
+ "private": true,
5
+ "scripts": {
6
+ "dev": "vite dev",
7
+ "build": "vite build",
8
+ "preview": "vite preview",
9
+ "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
10
+ "check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
11
+ "lint": "prettier --plugin-search-dir . --check . && eslint .",
12
+ "format": "prettier --plugin-search-dir . --write ."
13
+ },
14
+ "devDependencies": {
15
+ "@iconify-json/carbon": "^1.1.16",
16
+ "@sveltejs/adapter-node": "^1.2.4",
17
+ "@sveltejs/kit": "^1.15.10",
18
+ "@tailwindcss/typography": "^0.5.9",
19
+ "@types/marked": "^4.0.8",
20
+ "@types/parquetjs": "^0.10.3",
21
+ "@typescript-eslint/eslint-plugin": "^5.45.0",
22
+ "@typescript-eslint/parser": "^5.45.0",
23
+ "eslint": "^8.28.0",
24
+ "eslint-config-prettier": "^8.5.0",
25
+ "eslint-plugin-svelte3": "^4.0.0",
26
+ "prettier": "^2.8.0",
27
+ "prettier-plugin-svelte": "^2.8.1",
28
+ "prettier-plugin-tailwindcss": "^0.2.7",
29
+ "svelte": "^3.58.0",
30
+ "svelte-check": "^3.2.0",
31
+ "tslib": "^2.4.1",
32
+ "typescript": "^4.9.3",
33
+ "unplugin-icons": "^0.16.1",
34
+ "vite": "^4.0.0"
35
+ },
36
+ "type": "module",
37
+ "dependencies": {
38
+ "@huggingface/inference": "^2.2.0",
39
+ "@huggingface/hub": "^0.5.1",
40
+ "autoprefixer": "^10.4.14",
41
+ "date-fns": "^2.29.3",
42
+ "dotenv": "^16.0.3",
43
+ "highlight.js": "^11.7.0",
44
+ "marked": "^4.3.0",
45
+ "mongodb": "^5.3.0",
46
+ "nanoid": "^4.0.2",
47
+ "parquetjs": "^0.11.2",
48
+ "postcss": "^8.4.21",
49
+ "tailwind-scrollbar": "^3.0.0",
50
+ "tailwindcss": "^3.3.1",
51
+ "zod": "^3.21.4"
52
+ }
53
+ }
postcss.config.js ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ export default {
2
+ plugins: {
3
+ tailwindcss: {},
4
+ autoprefixer: {},
5
+ },
6
+ };
src/app.d.ts ADDED
@@ -0,0 +1,17 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /// <reference types="@sveltejs/kit" />
2
+ /// <reference types="unplugin-icons/types/svelte" />
3
+
4
+ // See https://kit.svelte.dev/docs/types#app
5
+ // for information about these interfaces
6
+ declare global {
7
+ namespace App {
8
+ // interface Error {}
9
+ interface Locals {
10
+ sessionId: string;
11
+ }
12
+ // interface PageData {}
13
+ // interface Platform {}
14
+ }
15
+ }
16
+
17
+ export {};
src/app.html ADDED
@@ -0,0 +1,73 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en" class="h-full">
3
+ <head>
4
+ <meta charset="utf-8" />
5
+ <link rel="icon" href="%sveltekit.assets%/favicon.png" />
6
+ <meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=no" />
7
+ <title>HuggingChat</title>
8
+ <script>
9
+ if (
10
+ localStorage.theme === "dark" ||
11
+ (!("theme" in localStorage) && window.matchMedia("(prefers-color-scheme: dark)").matches)
12
+ ) {
13
+ document.documentElement.classList.add("dark");
14
+ }
15
+
16
+ // For some reason, Sveltekit doesn't let us load env variables from .env here, so we load it from hooks.server.ts
17
+ window.gaId = "%gaId%";
18
+ window.gaIdDeprecated = "%gaIdDeprecated%";
19
+ </script>
20
+ %sveltekit.head%
21
+ </head>
22
+ <body data-sveltekit-preload-data="hover" class="h-full dark:bg-gray-900">
23
+ <div id="app" class="contents h-full">%sveltekit.body%</div>
24
+
25
+ <!-- Google Tag Manager -->
26
+ <script>
27
+ if (window.gaId) {
28
+ const script = document.createElement("script");
29
+ script.src = "https://www.googletagmanager.com/gtag/js?id=" + window.gaId;
30
+ script.async = true;
31
+ document.head.appendChild(script);
32
+
33
+ window.dataLayer = window.dataLayer || [];
34
+ function gtag() {
35
+ dataLayer.push(arguments);
36
+ }
37
+ gtag("js", new Date());
38
+ /// ^ See https://developers.google.com/tag-platform/gtagjs/install
39
+ gtag("config", window.gaId);
40
+ gtag("consent", "default", { ad_storage: "denied", analytics_storage: "denied" });
41
+ /// ^ See https://developers.google.com/tag-platform/gtagjs/reference#consent
42
+ /// TODO: ask the user for their consent and update this with gtag('consent', 'update')
43
+ }
44
+ </script>
45
+
46
+ <!-- Google Analytics v3 (deprecated on 1 July 2023) -->
47
+ <script>
48
+ if (window.gaIdDeprecated) {
49
+ (function (i, s, o, g, r, a, m) {
50
+ i["GoogleAnalyticsObject"] = r;
51
+ (i[r] =
52
+ i[r] ||
53
+ function () {
54
+ (i[r].q = i[r].q || []).push(arguments);
55
+ }),
56
+ (i[r].l = 1 * new Date());
57
+ (a = s.createElement(o)), (m = s.getElementsByTagName(o)[0]);
58
+ a.async = 1;
59
+ a.src = g;
60
+ m.parentNode.insertBefore(a, m);
61
+ })(
62
+ window,
63
+ document,
64
+ "script",
65
+ "https://www.google-analytics.com/analytics.js",
66
+ "ganalytics"
67
+ );
68
+ ganalytics("create", window.gaIdDeprecated, "auto");
69
+ ganalytics("send", "pageview");
70
+ }
71
+ </script>
72
+ </body>
73
+ </html>
src/hooks.server.ts ADDED
@@ -0,0 +1,72 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { dev } from "$app/environment";
2
+ import { COOKIE_NAME } from "$env/static/private";
3
+ import type { Handle } from "@sveltejs/kit";
4
+ import {
5
+ PUBLIC_GOOGLE_ANALYTICS_ID,
6
+ PUBLIC_DEPRECATED_GOOGLE_ANALYTICS_ID,
7
+ } from "$env/static/public";
8
+ import { addYears } from "date-fns";
9
+ import { collections } from "$lib/server/database";
10
+ import { base } from "$app/paths";
11
+
12
+ export const handle: Handle = async ({ event, resolve }) => {
13
+ const token = event.cookies.get(COOKIE_NAME);
14
+
15
+ event.locals.sessionId = token || crypto.randomUUID();
16
+
17
+ if (
18
+ event.request.method === "POST" &&
19
+ !event.url.pathname.startsWith(`${base}/settings`) &&
20
+ !event.url.pathname.startsWith(`${base}/admin`)
21
+ ) {
22
+ const hasAcceptedEthicsModal = await collections.settings.countDocuments({
23
+ sessionId: event.locals.sessionId,
24
+ ethicsModalAcceptedAt: { $exists: true },
25
+ });
26
+
27
+ if (!hasAcceptedEthicsModal) {
28
+ const sendJson =
29
+ event.request.headers.get("accept")?.includes("application/json") ||
30
+ event.request.headers.get("content-type")?.includes("application/json");
31
+ return new Response(
32
+ sendJson
33
+ ? JSON.stringify({ error: "You need to accept the welcome modal first" })
34
+ : "You need to accept the welcome modal first",
35
+ {
36
+ status: 405,
37
+ headers: {
38
+ "content-type": sendJson ? "application/json" : "text/plain",
39
+ },
40
+ }
41
+ );
42
+ }
43
+ }
44
+
45
+ // Refresh cookie expiration date
46
+ event.cookies.set(COOKIE_NAME, event.locals.sessionId, {
47
+ path: "/",
48
+ // So that it works inside the space's iframe
49
+ sameSite: dev ? "lax" : "none",
50
+ secure: !dev,
51
+ httpOnly: true,
52
+ expires: addYears(new Date(), 1),
53
+ });
54
+
55
+ let replaced = false;
56
+
57
+ const response = await resolve(event, {
58
+ transformPageChunk: (chunk) => {
59
+ // For some reason, Sveltekit doesn't let us load env variables from .env in the app.html template
60
+ if (replaced || !chunk.html.includes("%gaId%") || !chunk.html.includes("%gaIdDeprecated%")) {
61
+ return chunk.html;
62
+ }
63
+ replaced = true;
64
+
65
+ return chunk.html
66
+ .replace("%gaId%", PUBLIC_GOOGLE_ANALYTICS_ID)
67
+ .replace("%gaIdDeprecated%", PUBLIC_DEPRECATED_GOOGLE_ANALYTICS_ID);
68
+ },
69
+ });
70
+
71
+ return response;
72
+ };
src/lib/actions/snapScrollToBottom.ts ADDED
@@ -0,0 +1,54 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { navigating } from "$app/stores";
2
+ import { tick } from "svelte";
3
+ import { get } from "svelte/store";
4
+
5
+ const detachedOffset = 10;
6
+
7
+ /**
8
+ * @param node element to snap scroll to bottom
9
+ * @param dependency pass in a dependency to update scroll on changes.
10
+ */
11
+ export const snapScrollToBottom = (node: HTMLElement, dependency: unknown) => {
12
+ let prevScrollValue = node.scrollTop;
13
+ let isDetached = false;
14
+
15
+ const handleScroll = () => {
16
+ // if user scrolled up, we detach
17
+ if (node.scrollTop < prevScrollValue) {
18
+ isDetached = true;
19
+ }
20
+
21
+ // if user scrolled back to within 10px of bottom, we reattach
22
+ if (node.scrollTop - (node.scrollHeight - node.clientHeight) >= -detachedOffset) {
23
+ isDetached = false;
24
+ }
25
+
26
+ prevScrollValue = node.scrollTop;
27
+ };
28
+
29
+ const updateScroll = async (_options: { force?: boolean } = {}) => {
30
+ const defaultOptions = { force: false };
31
+ const options = { ...defaultOptions, ..._options };
32
+ const { force } = options;
33
+
34
+ if (!force && isDetached && !get(navigating)) return;
35
+
36
+ // wait for next tick to ensure that the DOM is updated
37
+ await tick();
38
+
39
+ node.scrollTo({ top: node.scrollHeight });
40
+ };
41
+
42
+ node.addEventListener("scroll", handleScroll);
43
+
44
+ if (dependency) {
45
+ updateScroll({ force: true });
46
+ }
47
+
48
+ return {
49
+ update: updateScroll,
50
+ destroy: () => {
51
+ node.removeEventListener("scroll", handleScroll);
52
+ },
53
+ };
54
+ };
src/lib/buildPrompt.ts ADDED
@@ -0,0 +1,30 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import type { BackendModel } from "./server/models";
2
+ import type { Message } from "./types/Message";
3
+
4
+ /**
5
+ * Convert [{user: "assistant", content: "hi"}, {user: "user", content: "hello"}] to:
6
+ *
7
+ * <|assistant|>hi<|endoftext|><|prompter|>hello<|endoftext|><|assistant|>
8
+ */
9
+ export function buildPrompt(
10
+ messages: Pick<Message, "from" | "content">[],
11
+ model: BackendModel
12
+ ): string {
13
+ const prompt =
14
+ messages
15
+ .map(
16
+ (m) =>
17
+ (m.from === "user"
18
+ ? model.userMessageToken + m.content
19
+ : model.assistantMessageToken + m.content) +
20
+ (model.messageEndToken
21
+ ? m.content.endsWith(model.messageEndToken)
22
+ ? ""
23
+ : model.messageEndToken
24
+ : "")
25
+ )
26
+ .join("") + model.assistantMessageToken;
27
+
28
+ // Not super precise, but it's truncated in the model's backend anyway
29
+ return model.preprompt + prompt.split(" ").slice(-model.parameters.truncate).join(" ");
30
+ }
src/lib/components/AnnouncementBanner.svelte ADDED
@@ -0,0 +1,15 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <script lang="ts">
2
+ export let title = "";
3
+ export let classNames = "";
4
+ </script>
5
+
6
+ <div class="flex items-center rounded-xl bg-gray-100 p-1 text-sm dark:bg-gray-800 {classNames}">
7
+ <span
8
+ class="mr-2 inline-flex items-center rounded-lg bg-gradient-to-br from-yellow-300 px-2 py-1 text-xxs font-medium uppercase leading-3 text-yellow-700 dark:from-[#373010] dark:text-yellow-400"
9
+ >New</span
10
+ >
11
+ {title}
12
+ <div class="ml-auto shrink-0">
13
+ <slot />
14
+ </div>
15
+ </div>
src/lib/components/CodeBlock.svelte ADDED
@@ -0,0 +1,27 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <script lang="ts">
2
+ import { afterUpdate } from "svelte";
3
+ import CopyToClipBoardBtn from "./CopyToClipBoardBtn.svelte";
4
+
5
+ export let code = "";
6
+ export let lang = "";
7
+
8
+ $: highlightedCode = "";
9
+
10
+ afterUpdate(async () => {
11
+ const { default: hljs } = await import("highlight.js");
12
+ const language = hljs.getLanguage(lang);
13
+
14
+ highlightedCode = hljs.highlightAuto(code, language?.aliases).value;
15
+ });
16
+ </script>
17
+
18
+ <div class="group relative my-4 rounded-lg">
19
+ <pre
20
+ class="scrollbar-custom overflow-auto px-5 scrollbar-thumb-gray-500 hover:scrollbar-thumb-gray-400 dark:scrollbar-thumb-white/10 dark:hover:scrollbar-thumb-white/20"><code
21
+ class="language-{lang}">{@html highlightedCode || code.replaceAll("<", "&lt;")}</code
22
+ ></pre>
23
+ <CopyToClipBoardBtn
24
+ classNames="absolute top-2 right-2 invisible opacity-0 group-hover:visible group-hover:opacity-100"
25
+ value={code}
26
+ />
27
+ </div>
src/lib/components/CopyToClipBoardBtn.svelte ADDED
@@ -0,0 +1,50 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <script lang="ts">
2
+ import { onDestroy } from "svelte";
3
+
4
+ import IconCopy from "./icons/IconCopy.svelte";
5
+ import Tooltip from "./Tooltip.svelte";
6
+
7
+ export let classNames = "";
8
+ export let value: string;
9
+
10
+ let isSuccess = false;
11
+ let timeout: ReturnType<typeof setTimeout>;
12
+
13
+ const handleClick = async () => {
14
+ // writeText() can be unavailable or fail in some cases (iframe, etc) so we try/catch
15
+ try {
16
+ await navigator.clipboard.writeText(value);
17
+
18
+ isSuccess = true;
19
+ if (timeout) {
20
+ clearTimeout(timeout);
21
+ }
22
+ timeout = setTimeout(() => {
23
+ isSuccess = false;
24
+ }, 1000);
25
+ } catch (err) {
26
+ console.error(err);
27
+ }
28
+ };
29
+
30
+ onDestroy(() => {
31
+ if (timeout) {
32
+ clearTimeout(timeout);
33
+ }
34
+ });
35
+ </script>
36
+
37
+ <button
38
+ class="btn rounded-lg border border-gray-200 px-2 py-2 text-sm shadow-sm transition-all hover:border-gray-300 active:shadow-inner dark:border-gray-600 dark:hover:border-gray-400 {classNames}
39
+ {!isSuccess && 'text-gray-200 dark:text-gray-200'}
40
+ {isSuccess && 'text-green-500'}
41
+ "
42
+ title={"Copy to clipboard"}
43
+ type="button"
44
+ on:click={handleClick}
45
+ >
46
+ <span class="relative">
47
+ <IconCopy />
48
+ <Tooltip classNames={isSuccess ? "opacity-100" : "opacity-0"} />
49
+ </span>
50
+ </button>
src/lib/components/EthicsModal.svelte ADDED
@@ -0,0 +1,47 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <script lang="ts">
2
+ import { enhance } from "$app/forms";
3
+ import { base } from "$app/paths";
4
+ import { PUBLIC_VERSION } from "$env/static/public";
5
+ import Logo from "$lib/components/icons/Logo.svelte";
6
+ import Modal from "$lib/components/Modal.svelte";
7
+ import type { LayoutData } from "../../routes/$types";
8
+
9
+ export let settings: LayoutData["settings"];
10
+ </script>
11
+
12
+ <Modal>
13
+ <div
14
+ class="flex w-full flex-col items-center gap-6 bg-gradient-to-t from-yellow-500/40 via-yellow-500/10 to-yellow-500/0 px-4 pb-10 pt-9 text-center"
15
+ >
16
+ <h2 class="flex items-center text-2xl font-semibold text-gray-800">
17
+ <Logo classNames="text-3xl mr-1.5" />HuggingChat
18
+ <div
19
+ class="ml-3 flex h-6 items-center rounded-lg border border-gray-100 bg-gray-50 px-2 text-base text-gray-400"
20
+ >
21
+ v{PUBLIC_VERSION}
22
+ </div>
23
+ </h2>
24
+ <p class="px-4 text-lg font-semibold leading-snug text-gray-800 sm:px-12">
25
+ This application is for demonstration purposes only.
26
+ </p>
27
+ <p class="text-gray-800">
28
+ AI is an area of active research with known problems such as biased generation and
29
+ misinformation. Do not use this application for high-stakes decisions or advice.
30
+ </p>
31
+ <p class="px-2 text-sm text-gray-500">
32
+ Your conversations will be shared with model authors unless you disable it from your settings.
33
+ </p>
34
+ <form action="{base}/settings" use:enhance method="POST">
35
+ <input type="hidden" name="ethicsModalAccepted" value={true} />
36
+ {#each Object.entries(settings) as [key, val]}
37
+ <input type="hidden" name={key} value={val} />
38
+ {/each}
39
+ <button
40
+ type="submit"
41
+ class="mt-2 rounded-full bg-black px-5 py-2 text-lg font-semibold text-gray-100 transition-colors hover:bg-yellow-500"
42
+ >
43
+ Start chatting
44
+ </button>
45
+ </form>
46
+ </div>
47
+ </Modal>
src/lib/components/MobileNav.svelte ADDED
@@ -0,0 +1,62 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <script lang="ts">
2
+ import { navigating } from "$app/stores";
3
+ import { createEventDispatcher } from "svelte";
4
+ import { browser } from "$app/environment";
5
+ import { base } from "$app/paths";
6
+
7
+ import CarbonClose from "~icons/carbon/close";
8
+ import CarbonAdd from "~icons/carbon/add";
9
+ import CarbonTextAlignJustify from "~icons/carbon/text-align-justify";
10
+
11
+ export let isOpen = false;
12
+ export let title: string | undefined;
13
+
14
+ $: title = title || "New Chat";
15
+
16
+ let closeEl: HTMLButtonElement;
17
+ let openEl: HTMLButtonElement;
18
+
19
+ const dispatch = createEventDispatcher();
20
+
21
+ $: if ($navigating) {
22
+ dispatch("toggle", false);
23
+ }
24
+
25
+ $: if (isOpen && closeEl) {
26
+ closeEl.focus();
27
+ } else if (!isOpen && browser && document.activeElement === closeEl) {
28
+ openEl.focus();
29
+ }
30
+ </script>
31
+
32
+ <nav
33
+ class="flex h-12 items-center justify-between border-b bg-gray-50 px-4 dark:border-gray-800 dark:bg-gray-800/70 md:hidden"
34
+ >
35
+ <button
36
+ type="button"
37
+ class="-ml-3 flex h-9 w-9 shrink-0 items-center justify-center"
38
+ on:click={() => dispatch("toggle", true)}
39
+ aria-label="Open menu"
40
+ bind:this={openEl}><CarbonTextAlignJustify /></button
41
+ >
42
+ <span class="truncate px-4">{title}</span>
43
+ <a href={base || "/"} class="-mr-3 flex h-9 w-9 shrink-0 items-center justify-center"
44
+ ><CarbonAdd /></a
45
+ >
46
+ </nav>
47
+ <nav
48
+ class="fixed inset-0 z-30 grid max-h-screen grid-cols-1 grid-rows-[auto,auto,1fr,auto] bg-white bg-gradient-to-l from-gray-50 dark:bg-gray-900 dark:from-gray-800/30 {isOpen
49
+ ? 'block'
50
+ : 'hidden'}"
51
+ >
52
+ <div class="flex h-12 items-center px-4">
53
+ <button
54
+ type="button"
55
+ class="-mr-3 ml-auto flex h-9 w-9 items-center justify-center"
56
+ on:click={() => dispatch("toggle", false)}
57
+ aria-label="Close menu"
58
+ bind:this={closeEl}><CarbonClose /></button
59
+ >
60
+ </div>
61
+ <slot />
62
+ </nav>
src/lib/components/Modal.svelte ADDED
@@ -0,0 +1,59 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <script lang="ts">
2
+ import { createEventDispatcher, onDestroy, onMount } from "svelte";
3
+ import { cubicOut } from "svelte/easing";
4
+ import { fade } from "svelte/transition";
5
+ import Portal from "./Portal.svelte";
6
+ import { browser } from "$app/environment";
7
+
8
+ export let width = "max-w-sm";
9
+
10
+ let backdropEl: HTMLDivElement;
11
+ let modalEl: HTMLDivElement;
12
+
13
+ const dispatch = createEventDispatcher<{ close: void }>();
14
+
15
+ function handleKeydown(event: KeyboardEvent) {
16
+ // close on ESC
17
+ if (event.key === "Escape") {
18
+ event.preventDefault();
19
+ dispatch("close");
20
+ }
21
+ }
22
+
23
+ function handleBackdropClick(event: MouseEvent) {
24
+ if (event.target === backdropEl) {
25
+ dispatch("close");
26
+ }
27
+ }
28
+
29
+ onMount(() => {
30
+ document.getElementById("app")?.setAttribute("inert", "true");
31
+ modalEl.focus();
32
+ });
33
+
34
+ onDestroy(() => {
35
+ if (!browser) return;
36
+ document.getElementById("app")?.removeAttribute("inert");
37
+ });
38
+ </script>
39
+
40
+ <Portal>
41
+ <div
42
+ role="presentation"
43
+ tabindex="-1"
44
+ bind:this={backdropEl}
45
+ on:click={handleBackdropClick}
46
+ transition:fade={{ easing: cubicOut, duration: 300 }}
47
+ class="fixed inset-0 z-40 flex items-center justify-center bg-black/80 p-8 backdrop-blur-sm dark:bg-black/50"
48
+ >
49
+ <div
50
+ role="dialog"
51
+ tabindex="-1"
52
+ bind:this={modalEl}
53
+ on:keydown={handleKeydown}
54
+ class="-mt-10 overflow-hidden rounded-2xl bg-white shadow-2xl outline-none md:-mt-20 {width}"
55
+ >
56
+ <slot />
57
+ </div>
58
+ </div>
59
+ </Portal>
src/lib/components/ModelCardMetadata.svelte ADDED
@@ -0,0 +1,48 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <script lang="ts">
2
+ import CarbonEarth from "~icons/carbon/earth";
3
+ import CarbonArrowUpRight from "~icons/carbon/arrow-up-right";
4
+ import type { Model } from "$lib/types/Model";
5
+
6
+ export let model: Pick<Model, "name" | "datasetName" | "websiteUrl">;
7
+
8
+ export let variant: "light" | "dark" = "light";
9
+ </script>
10
+
11
+ <div
12
+ class="flex items-center gap-5 rounded-xl bg-gray-100 px-3 py-2 text-sm
13
+ {variant === 'dark'
14
+ ? 'text-gray-600 dark:bg-gray-800 dark:text-gray-300'
15
+ : 'text-gray-800 dark:bg-gray-100 dark:text-gray-600'}"
16
+ >
17
+ <a
18
+ href="https://huggingface.co/{model.name}"
19
+ target="_blank"
20
+ rel="noreferrer"
21
+ class="flex items-center hover:underline"
22
+ ><CarbonArrowUpRight class="mr-1.5 shrink-0 text-xs text-gray-400" />
23
+ Model
24
+ <div class="max-sm:hidden">&nbsp;page</div></a
25
+ >
26
+ {#if model.datasetName}
27
+ <a
28
+ href="https://huggingface.co/datasets/{model.datasetName}"
29
+ target="_blank"
30
+ rel="noreferrer"
31
+ class="flex items-center hover:underline"
32
+ ><CarbonArrowUpRight class="mr-1.5 shrink-0 text-xs text-gray-400" />
33
+ Dataset
34
+ <div class="max-sm:hidden">&nbsp;page</div></a
35
+ >
36
+ {/if}
37
+ {#if model.websiteUrl}
38
+ <a
39
+ href={model.websiteUrl}
40
+ target="_blank"
41
+ class="ml-auto flex items-center hover:underline"
42
+ rel="noreferrer"
43
+ >
44
+ <CarbonEarth class="mr-1.5 shrink-0 text-xs text-gray-400" />
45
+ Website
46
+ </a>
47
+ {/if}
48
+ </div>
src/lib/components/ModelsModal.svelte ADDED
@@ -0,0 +1,80 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <script lang="ts">
2
+ import { createEventDispatcher } from "svelte";
3
+
4
+ import Modal from "$lib/components/Modal.svelte";
5
+ import CarbonClose from "~icons/carbon/close";
6
+ import CarbonCheckmark from "~icons/carbon/checkmark-filled";
7
+ import ModelCardMetadata from "./ModelCardMetadata.svelte";
8
+ import type { Model } from "$lib/types/Model";
9
+ import type { LayoutData } from "../../routes/$types";
10
+ import { enhance } from "$app/forms";
11
+ import { base } from "$app/paths";
12
+
13
+ export let settings: LayoutData["settings"];
14
+ export let models: Array<Model>;
15
+
16
+ let selectedModelId = settings.activeModel;
17
+
18
+ const dispatch = createEventDispatcher<{ close: void }>();
19
+ </script>
20
+
21
+ <Modal width="max-w-lg" on:close>
22
+ <form
23
+ action="{base}/settings"
24
+ method="post"
25
+ use:enhance={() => {
26
+ dispatch("close");
27
+ }}
28
+ class="flex w-full flex-col gap-5 p-6"
29
+ >
30
+ {#each Object.entries(settings).filter(([k]) => k !== "activeModel") as [key, val]}
31
+ <input type="hidden" name={key} value={val} />
32
+ {/each}
33
+ <div class="flex items-start justify-between text-xl font-semibold text-gray-800">
34
+ <h2>Models</h2>
35
+ <button type="button" class="group" on:click={() => dispatch("close")}>
36
+ <CarbonClose class="text-gray-900 group-hover:text-gray-500" />
37
+ </button>
38
+ </div>
39
+
40
+ <div class="space-y-4">
41
+ {#each models as model}
42
+ <div
43
+ class="rounded-xl border border-gray-100 {model.id === selectedModelId
44
+ ? 'bg-gradient-to-r from-yellow-200/40 via-yellow-500/10'
45
+ : ''}"
46
+ >
47
+ <label class="group flex cursor-pointer p-3" on:change aria-label={model.displayName}>
48
+ <input
49
+ type="radio"
50
+ class="sr-only"
51
+ name="activeModel"
52
+ value={model.id}
53
+ bind:group={selectedModelId}
54
+ />
55
+ <span>
56
+ <span class="text-md block font-semibold leading-tight text-gray-800"
57
+ >{model.displayName}</span
58
+ >
59
+ {#if model.description}
60
+ <span class="text-xs text-[#9FA8B5]">{model.description}</span>
61
+ {/if}
62
+ </span>
63
+ <CarbonCheckmark
64
+ class="-mr-1 -mt-1 ml-auto shrink-0 text-xl {model.id === selectedModelId
65
+ ? 'text-yellow-400'
66
+ : 'text-transparent group-hover:text-gray-200'}"
67
+ />
68
+ </label>
69
+ <ModelCardMetadata {model} />
70
+ </div>
71
+ {/each}
72
+ </div>
73
+ <button
74
+ type="submit"
75
+ class="mt-2 rounded-full bg-black px-5 py-2 text-lg font-semibold text-gray-100 ring-gray-400 ring-offset-1 transition-colors hover:ring"
76
+ >
77
+ Apply
78
+ </button>
79
+ </form>
80
+ </Modal>
src/lib/components/NavMenu.svelte ADDED
@@ -0,0 +1,109 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <script lang="ts">
2
+ import { base } from "$app/paths";
3
+ import { page } from "$app/stores";
4
+ import { createEventDispatcher } from "svelte";
5
+
6
+ import Logo from "$lib/components/icons/Logo.svelte";
7
+ import CarbonTrashCan from "~icons/carbon/trash-can";
8
+ import CarbonEdit from "~icons/carbon/edit";
9
+
10
+ import { switchTheme } from "$lib/switchTheme";
11
+ import { PUBLIC_ORIGIN } from "$env/static/public";
12
+
13
+ const dispatch = createEventDispatcher<{
14
+ shareConversation: { id: string; title: string };
15
+ deleteConversation: string;
16
+ clickSettings: void;
17
+ editConversationTitle: { id: string; title: string };
18
+ }>();
19
+
20
+ export let conversations: Array<{
21
+ id: string;
22
+ title: string;
23
+ }> = [];
24
+ </script>
25
+
26
+ <div class="sticky top-0 flex flex-none items-center justify-between px-3 py-3.5 max-sm:pt-0">
27
+ <a class="flex items-center rounded-xl text-lg font-semibold" href="{PUBLIC_ORIGIN}{base}/">
28
+ <Logo classNames="mr-1 text-3xl" />
29
+ HuggingChat
30
+ </a>
31
+ <a
32
+ href={base || "/"}
33
+ class="flex rounded-lg border bg-white px-2 py-0.5 text-center shadow-sm hover:shadow-none dark:border-gray-600 dark:bg-gray-700"
34
+ >
35
+ New Chat
36
+ </a>
37
+ </div>
38
+ <div
39
+ class="scrollbar-custom flex flex-col gap-1 overflow-y-auto rounded-r-xl bg-gradient-to-l from-gray-50 px-3 pb-3 pt-2 dark:from-gray-800/30"
40
+ >
41
+ {#each conversations as conv (conv.id)}
42
+ <a
43
+ data-sveltekit-noscroll
44
+ href="{base}/conversation/{conv.id}"
45
+ class="group flex h-11 flex-none items-center gap-1.5 rounded-lg pl-3 pr-2 text-gray-500 hover:bg-gray-100 dark:text-gray-400 dark:hover:bg-gray-700 {conv.id ===
46
+ $page.params.id
47
+ ? 'bg-gray-100 dark:bg-gray-700'
48
+ : ''}"
49
+ >
50
+ <div class="flex-1 truncate">{conv.title}</div>
51
+
52
+ <button
53
+ type="button"
54
+ class="flex h-5 w-5 items-center justify-center rounded md:hidden md:group-hover:flex"
55
+ title="Edit conversation title"
56
+ on:click|preventDefault={() => {
57
+ const newTitle = prompt("Edit this conversation title:", conv.title);
58
+ if (!newTitle) return;
59
+ dispatch("editConversationTitle", { id: conv.id, title: newTitle });
60
+ }}
61
+ >
62
+ <CarbonEdit class="text-xs text-gray-400 hover:text-gray-500 dark:hover:text-gray-300" />
63
+ </button>
64
+
65
+ <button
66
+ type="button"
67
+ class="flex h-5 w-5 items-center justify-center rounded md:hidden md:group-hover:flex"
68
+ title="Delete conversation"
69
+ on:click|preventDefault={() => dispatch("deleteConversation", conv.id)}
70
+ >
71
+ <CarbonTrashCan
72
+ class="text-xs text-gray-400 hover:text-gray-500 dark:hover:text-gray-300"
73
+ />
74
+ </button>
75
+ </a>
76
+ {/each}
77
+ </div>
78
+ <div
79
+ class="mt-0.5 flex flex-col gap-1 rounded-r-xl bg-gradient-to-l from-gray-50 p-3 text-sm dark:from-gray-800/30"
80
+ >
81
+ <button
82
+ on:click={switchTheme}
83
+ type="button"
84
+ class="group flex h-9 flex-none items-center gap-1.5 rounded-lg pl-3 pr-2 text-gray-500 hover:bg-gray-100 dark:text-gray-400 dark:hover:bg-gray-700"
85
+ >
86
+ Theme
87
+ </button>
88
+ <button
89
+ on:click={() => dispatch("clickSettings")}
90
+ type="button"
91
+ class="group flex h-9 flex-none items-center gap-1.5 rounded-lg pl-3 pr-2 text-gray-500 hover:bg-gray-100 dark:text-gray-400 dark:hover:bg-gray-700"
92
+ >
93
+ Settings
94
+ </button>
95
+ <a
96
+ href="https://huggingface.co/spaces/huggingchat/chat-ui/discussions"
97
+ target="_blank"
98
+ rel="noreferrer"
99
+ class="group flex h-9 flex-none items-center gap-1.5 rounded-lg pl-3 pr-2 text-gray-500 hover:bg-gray-100 dark:text-gray-400 dark:hover:bg-gray-700"
100
+ >
101
+ Feedback
102
+ </a>
103
+ <a
104
+ href="{base}/privacy"
105
+ class="group flex h-9 flex-none items-center gap-1.5 rounded-lg pl-3 pr-2 text-gray-500 hover:bg-gray-100 dark:text-gray-400 dark:hover:bg-gray-700"
106
+ >
107
+ About & Privacy
108
+ </a>
109
+ </div>
src/lib/components/Portal.svelte ADDED
@@ -0,0 +1,19 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <script lang="ts">
2
+ import { onMount, onDestroy } from "svelte";
3
+
4
+ let el: HTMLElement;
5
+
6
+ onMount(() => {
7
+ el.ownerDocument.body.appendChild(el);
8
+ });
9
+
10
+ onDestroy(() => {
11
+ if (el?.parentNode) {
12
+ el.parentNode.removeChild(el);
13
+ }
14
+ });
15
+ </script>
16
+
17
+ <div bind:this={el} class="contents" hidden>
18
+ <slot />
19
+ </div>
src/lib/components/ScrollToBottomBtn.svelte ADDED
@@ -0,0 +1,46 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <script lang="ts">
2
+ import { fade } from "svelte/transition";
3
+ import { onDestroy } from "svelte";
4
+ import IconChevron from "./icons/IconChevron.svelte";
5
+
6
+ export let scrollNode: HTMLElement;
7
+ export { className as class };
8
+
9
+ let visible = false;
10
+ let className = "";
11
+ let observer: ResizeObserver | null = null;
12
+
13
+ $: if (scrollNode) {
14
+ destroy();
15
+
16
+ if (window.ResizeObserver) {
17
+ observer = new ResizeObserver(() => {
18
+ updateVisibility();
19
+ });
20
+ observer.observe(scrollNode);
21
+ }
22
+ scrollNode.addEventListener("scroll", updateVisibility);
23
+ }
24
+
25
+ function updateVisibility() {
26
+ if (!scrollNode) return;
27
+ visible =
28
+ Math.ceil(scrollNode.scrollTop) + 200 < scrollNode.scrollHeight - scrollNode.clientHeight;
29
+ }
30
+
31
+ function destroy() {
32
+ observer?.disconnect();
33
+ scrollNode?.removeEventListener("scroll", updateVisibility);
34
+ }
35
+
36
+ onDestroy(destroy);
37
+ </script>
38
+
39
+ {#if visible}
40
+ <button
41
+ transition:fade|local={{ duration: 150 }}
42
+ on:click={() => scrollNode.scrollTo({ top: scrollNode.scrollHeight, behavior: "smooth" })}
43
+ class="btn absolute flex h-[41px] w-[41px] rounded-full border bg-white shadow-md transition-all hover:bg-gray-100 dark:border-gray-600 dark:bg-gray-700 dark:shadow-gray-950 dark:hover:bg-gray-600 {className}"
44
+ ><IconChevron classNames="mt-[2px]" /></button
45
+ >
46
+ {/if}
src/lib/components/SettingsModal.svelte ADDED
@@ -0,0 +1,65 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <script lang="ts">
2
+ import { createEventDispatcher } from "svelte";
3
+
4
+ import Modal from "$lib/components/Modal.svelte";
5
+ import CarbonClose from "~icons/carbon/close";
6
+ import Switch from "$lib/components/Switch.svelte";
7
+ import type { Settings } from "$lib/types/Settings";
8
+ import { enhance } from "$app/forms";
9
+ import { base } from "$app/paths";
10
+
11
+ export let settings: Pick<Settings, "shareConversationsWithModelAuthors">;
12
+
13
+ const dispatch = createEventDispatcher<{ close: void }>();
14
+ </script>
15
+
16
+ <Modal on:close>
17
+ <form
18
+ class="flex w-full flex-col gap-5 p-6"
19
+ use:enhance={() => {
20
+ dispatch("close");
21
+ }}
22
+ method="post"
23
+ action="{base}/settings"
24
+ >
25
+ <div class="flex items-start justify-between text-xl font-semibold text-gray-800">
26
+ <h2>Settings</h2>
27
+ <button type="button" class="group" on:click={() => dispatch("close")}>
28
+ <CarbonClose class="text-gray-900 group-hover:text-gray-500" />
29
+ </button>
30
+ </div>
31
+
32
+ <label class="flex cursor-pointer select-none items-center gap-2 text-gray-500">
33
+ {#each Object.entries(settings).filter(([k]) => k !== "shareConversationsWithModelAuthors") as [key, val]}
34
+ <input type="hidden" name={key} value={val} />
35
+ {/each}
36
+ <Switch
37
+ name="shareConversationsWithModelAuthors"
38
+ bind:checked={settings.shareConversationsWithModelAuthors}
39
+ />
40
+ Share conversations with model authors
41
+ </label>
42
+
43
+ <p class="text-gray-800">
44
+ Sharing your data will help improve the training data and make open models better over time.
45
+ </p>
46
+ <p class="text-gray-800">
47
+ You can change this setting at any time, it applies to all your conversations.
48
+ </p>
49
+ <p class="text-gray-800">
50
+ Read more about this model's authors,
51
+ <a
52
+ href="https://open-assistant.io/"
53
+ target="_blank"
54
+ rel="noreferrer"
55
+ class="underline decoration-gray-300 hover:decoration-gray-700">Open Assistant</a
56
+ >.
57
+ </p>
58
+ <button
59
+ type="submit"
60
+ class="mt-2 rounded-full bg-black px-5 py-2 text-lg font-semibold text-gray-100 ring-gray-400 ring-offset-1 transition-colors hover:ring"
61
+ >
62
+ Apply
63
+ </button>
64
+ </form>
65
+ </Modal>
src/lib/components/StopGeneratingBtn.svelte ADDED
@@ -0,0 +1,17 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <script lang="ts">
2
+ import CarbonPause from "~icons/carbon/pause-filled";
3
+
4
+ export let visible = false;
5
+ export let className = "";
6
+ </script>
7
+
8
+ <button
9
+ type="button"
10
+ on:click
11
+ class="btn absolute flex rounded-lg border bg-white px-3 py-1 shadow-sm transition-all hover:bg-gray-100 dark:border-gray-600 dark:bg-gray-700 dark:hover:bg-gray-600
12
+ {className}
13
+ {visible ? 'visible opacity-100' : 'invisible opacity-0'}
14
+ "
15
+ >
16
+ <CarbonPause class="-ml-1 mr-1 h-[1.25rem] w-[1.1875rem] text-gray-400" /> Stop generating
17
+ </button>
src/lib/components/Switch.svelte ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <script lang="ts">
2
+ export let checked: boolean;
3
+ export let name: string;
4
+ </script>
5
+
6
+ <input bind:checked type="checkbox" {name} class="peer pointer-events-none absolute opacity-0" />
7
+ <div
8
+ class="relative inline-flex h-5 w-9 items-center rounded-full bg-gray-300 p-1 shadow-inner transition-all peer-checked:bg-black hover:bg-gray-400 peer-checked:[&>div]:translate-x-3.5"
9
+ >
10
+ <div class="h-3.5 w-3.5 rounded-full bg-white shadow-sm transition-all" />
11
+ </div>
src/lib/components/Toast.svelte ADDED
@@ -0,0 +1,19 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <script lang="ts">
2
+ import { fade } from "svelte/transition";
3
+
4
+ import IconDazzled from "$lib/components/icons/IconDazzled.svelte";
5
+
6
+ export let message = "";
7
+ </script>
8
+
9
+ <div
10
+ transition:fade={{ duration: 300 }}
11
+ class="pointer-events-none fixed right-0 top-12 z-20 bg-gradient-to-bl from-red-500/20 via-red-500/0 to-red-500/0 pb-36 pl-36 pr-2 pt-2 md:top-0 md:pr-8 md:pt-5"
12
+ >
13
+ <div
14
+ class="pointer-events-auto flex items-center rounded-full bg-white/90 px-3 py-1 shadow-sm dark:bg-gray-900/80"
15
+ >
16
+ <IconDazzled classNames="text-2xl mr-2" />
17
+ <h2 class="font-semibold">{message}</h2>
18
+ </div>
19
+ </div>
src/lib/components/Tooltip.svelte ADDED
@@ -0,0 +1,22 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <script lang="ts">
2
+ export let classNames = "";
3
+ export let label = "Copied";
4
+ export let position = "left-1/2 top-full transform -translate-x-1/2 translate-y-2";
5
+ </script>
6
+
7
+ <div
8
+ class="
9
+ pointer-events-none absolute rounded bg-black px-2 py-1 font-normal leading-tight text-white shadow transition-opacity
10
+ {position}
11
+ {classNames}
12
+ "
13
+ >
14
+ <div
15
+ class="absolute bottom-full left-1/2 h-0 w-0 -translate-x-1/2 transform border-4 border-t-0 border-black"
16
+ style="
17
+ border-left-color: transparent;
18
+ border-right-color: transparent;
19
+ "
20
+ />
21
+ {label}
22
+ </div>
src/lib/components/chat/ChatInput.svelte ADDED
@@ -0,0 +1,54 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <script lang="ts">
2
+ import { createEventDispatcher } from "svelte";
3
+
4
+ export let value = "";
5
+ export let minRows = 1;
6
+ export let maxRows: null | number = null;
7
+ export let placeholder = "";
8
+ export let disabled = false;
9
+ export let autofocus = false;
10
+
11
+ const dispatch = createEventDispatcher<{ submit: void }>();
12
+
13
+ $: minHeight = `${1 + minRows * 1.5}em`;
14
+ $: maxHeight = maxRows ? `${1 + maxRows * 1.5}em` : `auto`;
15
+
16
+ function handleKeydown(event: KeyboardEvent) {
17
+ // submit on enter
18
+ if (event.key === "Enter" && !event.shiftKey) {
19
+ event.preventDefault();
20
+ dispatch("submit"); // use a custom event instead of `event.target.form.requestSubmit()` as it does not work on Safari 14
21
+ }
22
+ }
23
+
24
+ let textareaElement: HTMLTextAreaElement;
25
+ </script>
26
+
27
+ <div class="relative min-w-0 flex-1">
28
+ <pre
29
+ class="invisible whitespace-pre-wrap p-3"
30
+ aria-hidden="true"
31
+ style="min-height: {minHeight}; max-height: {maxHeight}">{value + "\n"}</pre>
32
+
33
+ <textarea
34
+ enterkeyhint="send"
35
+ tabindex="0"
36
+ rows="1"
37
+ class="scrollbar-custom absolute top-0 m-0 h-full w-full resize-none scroll-p-3 overflow-x-hidden overflow-y-scroll border-0 bg-transparent p-3 outline-none focus:ring-0 focus-visible:ring-0"
38
+ bind:value
39
+ bind:this={textareaElement}
40
+ {disabled}
41
+ on:keydown={handleKeydown}
42
+ {placeholder}
43
+ {autofocus}
44
+ />
45
+ </div>
46
+
47
+ <style>
48
+ pre,
49
+ textarea {
50
+ font-family: inherit;
51
+ box-sizing: border-box;
52
+ line-height: 1.5;
53
+ }
54
+ </style>
src/lib/components/chat/ChatIntroduction.svelte ADDED
@@ -0,0 +1,84 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <script lang="ts">
2
+ import { PUBLIC_VERSION } from "$env/static/public";
3
+ import Logo from "$lib/components/icons/Logo.svelte";
4
+ import { createEventDispatcher } from "svelte";
5
+ import IconChevron from "$lib/components/icons/IconChevron.svelte";
6
+ import AnnouncementBanner from "../AnnouncementBanner.svelte";
7
+ import ModelsModal from "../ModelsModal.svelte";
8
+ import type { Model } from "$lib/types/Model";
9
+ import ModelCardMetadata from "../ModelCardMetadata.svelte";
10
+ import type { LayoutData } from "../../../routes/$types";
11
+ import { findCurrentModel } from "$lib/utils/models";
12
+
13
+ export let currentModel: Model;
14
+ export let settings: LayoutData["settings"];
15
+ export let models: Model[];
16
+
17
+ let isModelsModalOpen = false;
18
+
19
+ $: currentModelMetadata = findCurrentModel(models, settings.activeModel);
20
+
21
+ const dispatch = createEventDispatcher<{ message: string }>();
22
+ </script>
23
+
24
+ <div class="my-auto grid gap-8 lg:grid-cols-3">
25
+ <div class="lg:col-span-1">
26
+ <div>
27
+ <div class="mb-3 flex items-center text-2xl font-semibold">
28
+ <Logo classNames="mr-1 text-yellow-400 text-4xl" />
29
+ HuggingChat
30
+ <div
31
+ class="ml-3 flex h-6 items-center rounded-lg border border-gray-100 bg-gray-50 px-2 text-base text-gray-400 dark:border-gray-700/60 dark:bg-gray-800"
32
+ >
33
+ v{PUBLIC_VERSION}
34
+ </div>
35
+ </div>
36
+ <p class="text-base text-gray-600 dark:text-gray-400">
37
+ Making the community's best AI chat models available to everyone.
38
+ </p>
39
+ </div>
40
+ </div>
41
+ <div class="lg:col-span-2 lg:pl-24">
42
+ <AnnouncementBanner classNames="mb-4" title="BigCode/StarCoder is now available">
43
+ <button
44
+ type="button"
45
+ on:click={() => (isModelsModalOpen = true)}
46
+ class="mr-2 flex items-center underline hover:no-underline"
47
+ ><IconChevron classNames="mr-1" /> Switch model</button
48
+ >
49
+ </AnnouncementBanner>
50
+ {#if isModelsModalOpen}
51
+ <ModelsModal {settings} {models} on:close={() => (isModelsModalOpen = false)} />
52
+ {/if}
53
+ <div class="overflow-hidden rounded-xl border dark:border-gray-800">
54
+ <div class="flex p-3">
55
+ <div>
56
+ <div class="text-sm text-gray-600 dark:text-gray-400">Current Model</div>
57
+ <div class="font-semibold">{currentModel.displayName}</div>
58
+ </div>
59
+ <button
60
+ type="button"
61
+ on:click={() => (isModelsModalOpen = true)}
62
+ class="btn ml-auto flex h-7 w-7 self-start rounded-full bg-gray-100 p-1 text-xs hover:bg-gray-100 dark:border-gray-600 dark:bg-gray-800 dark:hover:bg-gray-600"
63
+ ><IconChevron /></button
64
+ >
65
+ </div>
66
+ <ModelCardMetadata variant="dark" model={currentModel} />
67
+ </div>
68
+ </div>
69
+ {#if currentModelMetadata.promptExamples}
70
+ <div class="lg:col-span-3 lg:mt-12">
71
+ <p class="mb-3 text-gray-600 dark:text-gray-300">Examples</p>
72
+ <div class="grid gap-3 lg:grid-cols-3 lg:gap-5">
73
+ {#each currentModelMetadata.promptExamples as example}
74
+ <button
75
+ type="button"
76
+ class="rounded-xl border bg-gray-50 p-2.5 text-gray-600 hover:bg-gray-100 dark:border-gray-800 dark:bg-gray-800 dark:text-gray-300 dark:hover:bg-gray-700 sm:p-4"
77
+ on:click={() => dispatch("message", example.prompt)}
78
+ >
79
+ {example.title}
80
+ </button>
81
+ {/each}
82
+ </div>
83
+ </div>{/if}
84
+ </div>
src/lib/components/chat/ChatMessage.svelte ADDED
@@ -0,0 +1,144 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <script lang="ts">
2
+ import { marked } from "marked";
3
+ import type { Message } from "$lib/types/Message";
4
+ import { afterUpdate, createEventDispatcher } from "svelte";
5
+ import { deepestChild } from "$lib/utils/deepestChild";
6
+ import { page } from "$app/stores";
7
+
8
+ import CodeBlock from "../CodeBlock.svelte";
9
+ import IconLoading from "../icons/IconLoading.svelte";
10
+ import CarbonRotate360 from "~icons/carbon/rotate-360";
11
+ import CarbonDownload from "~icons/carbon/download";
12
+ import { PUBLIC_SEP_TOKEN } from "$lib/constants/publicSepToken";
13
+ import type { Model } from "$lib/types/Model";
14
+
15
+ function sanitizeMd(md: string) {
16
+ let ret = md
17
+ .replace(/<\|[a-z]*$/, "")
18
+ .replace(/<\|[a-z]+\|$/, "")
19
+ .replace(/<$/, "")
20
+ .replaceAll(PUBLIC_SEP_TOKEN, " ")
21
+ .replaceAll(/<\|[a-z]+\|>/g, " ")
22
+ .replaceAll(/<br\s?\/?>/gi, "\n")
23
+ .replaceAll("<", "&lt;")
24
+ .trim();
25
+
26
+ for (const stop of [...(model.parameters.stop ?? []), "<|endoftext|>"]) {
27
+ if (ret.endsWith(stop)) {
28
+ ret = ret.slice(0, -stop.length).trim();
29
+ }
30
+ }
31
+
32
+ return ret;
33
+ }
34
+ function unsanitizeMd(md: string) {
35
+ return md.replaceAll("&lt;", "<");
36
+ }
37
+
38
+ export let model: Model;
39
+ export let message: Message;
40
+ export let loading = false;
41
+
42
+ const dispatch = createEventDispatcher<{ retry: void }>();
43
+
44
+ let contentEl: HTMLElement;
45
+ let loadingEl: IconLoading;
46
+ let pendingTimeout: ReturnType<typeof setTimeout>;
47
+
48
+ const renderer = new marked.Renderer();
49
+
50
+ // For code blocks with simple backticks
51
+ renderer.codespan = (code) => {
52
+ // Unsanitize double-sanitized code
53
+ return `<code>${code.replaceAll("&amp;", "&")}</code>`;
54
+ };
55
+
56
+ const options: marked.MarkedOptions = {
57
+ ...marked.getDefaults(),
58
+ gfm: true,
59
+ breaks: true,
60
+ renderer,
61
+ };
62
+
63
+ $: tokens = marked.lexer(sanitizeMd(message.content));
64
+
65
+ afterUpdate(() => {
66
+ loadingEl?.$destroy();
67
+ clearTimeout(pendingTimeout);
68
+
69
+ // Add loading animation to the last message if update takes more than 600ms
70
+ if (loading) {
71
+ pendingTimeout = setTimeout(() => {
72
+ if (contentEl) {
73
+ loadingEl = new IconLoading({
74
+ target: deepestChild(contentEl),
75
+ props: { classNames: "loading inline ml-2" },
76
+ });
77
+ }
78
+ }, 600);
79
+ }
80
+ });
81
+
82
+ $: downloadLink =
83
+ message.from === "user" ? `${$page.url.pathname}/message/${message.id}/prompt` : undefined;
84
+ </script>
85
+
86
+ {#if message.from === "assistant"}
87
+ <div class="flex items-start justify-start gap-4 leading-relaxed">
88
+ <img
89
+ alt=""
90
+ src="https://huggingface.co/avatars/2edb18bd0206c16b433841a47f53fa8e.svg"
91
+ class="mt-5 h-3 w-3 flex-none select-none rounded-full shadow-lg"
92
+ />
93
+ <div
94
+ class="relative min-h-[calc(2rem+theme(spacing[3.5])*2)] min-w-[100px] rounded-2xl border border-gray-100 bg-gradient-to-br from-gray-50 px-5 py-3.5 text-gray-600 prose-pre:my-2 dark:border-gray-800 dark:from-gray-800/40 dark:text-gray-300"
95
+ >
96
+ {#if !message.content}
97
+ <IconLoading classNames="absolute inset-0 m-auto" />
98
+ {/if}
99
+ <div
100
+ class="prose max-w-none dark:prose-invert max-sm:prose-sm prose-headings:font-semibold prose-h1:text-lg prose-h2:text-base prose-h3:text-base prose-pre:bg-gray-800 dark:prose-pre:bg-gray-900"
101
+ bind:this={contentEl}
102
+ >
103
+ {#each tokens as token}
104
+ {#if token.type === "code"}
105
+ <CodeBlock lang={token.lang} code={unsanitizeMd(token.text)} />
106
+ {:else}
107
+ {@html marked(token.raw, options)}
108
+ {/if}
109
+ {/each}
110
+ </div>
111
+ </div>
112
+ </div>
113
+ {/if}
114
+ {#if message.from === "user"}
115
+ <div class="group relative flex items-start justify-start gap-4 max-sm:text-sm">
116
+ <div class="mt-5 h-3 w-3 flex-none rounded-full" />
117
+ <div class="whitespace-break-spaces rounded-2xl px-5 py-3.5 text-gray-500 dark:text-gray-400">
118
+ {message.content.trim()}
119
+ </div>
120
+ {#if !loading}
121
+ <div class="absolute right-0 top-3.5 flex gap-2 lg:-right-2">
122
+ {#if downloadLink}
123
+ <a
124
+ class="rounded-lg border border-gray-100 p-1 text-xs text-gray-400 group-hover:block hover:text-gray-500 dark:border-gray-800 dark:text-gray-400 dark:hover:text-gray-300 md:hidden"
125
+ title="Download prompt and parameters"
126
+ type="button"
127
+ target="_blank"
128
+ href={downloadLink}
129
+ >
130
+ <CarbonDownload />
131
+ </a>
132
+ {/if}
133
+ <button
134
+ class="cursor-pointer rounded-lg border border-gray-100 p-1 text-xs text-gray-400 group-hover:block hover:text-gray-500 dark:border-gray-800 dark:text-gray-400 dark:hover:text-gray-300 md:hidden lg:-right-2"
135
+ title="Retry"
136
+ type="button"
137
+ on:click={() => dispatch("retry")}
138
+ >
139
+ <CarbonRotate360 />
140
+ </button>
141
+ </div>
142
+ {/if}
143
+ </div>
144
+ {/if}
src/lib/components/chat/ChatMessages.svelte ADDED
@@ -0,0 +1,65 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <script lang="ts">
2
+ import type { Message } from "$lib/types/Message";
3
+ import { snapScrollToBottom } from "$lib/actions/snapScrollToBottom";
4
+ import ScrollToBottomBtn from "$lib/components/ScrollToBottomBtn.svelte";
5
+ import { createEventDispatcher, tick } from "svelte";
6
+
7
+ import ChatIntroduction from "./ChatIntroduction.svelte";
8
+ import ChatMessage from "./ChatMessage.svelte";
9
+ import { randomUUID } from "$lib/utils/randomUuid";
10
+ import type { Model } from "$lib/types/Model";
11
+ import type { LayoutData } from "../../../routes/$types";
12
+
13
+ const dispatch = createEventDispatcher<{ retry: { id: Message["id"]; content: string } }>();
14
+
15
+ export let messages: Message[];
16
+ export let loading: boolean;
17
+ export let pending: boolean;
18
+ export let currentModel: Model;
19
+ export let settings: LayoutData["settings"];
20
+ export let models: Model[] | undefined;
21
+
22
+ let chatContainer: HTMLElement;
23
+
24
+ async function scrollToBottom() {
25
+ await tick();
26
+ chatContainer.scrollTop = chatContainer.scrollHeight;
27
+ }
28
+
29
+ // If last message is from user, scroll to bottom
30
+ $: if (messages[messages.length - 1]?.from === "user") {
31
+ scrollToBottom();
32
+ }
33
+ </script>
34
+
35
+ <div
36
+ class="scrollbar-custom mr-1 h-full overflow-y-auto"
37
+ use:snapScrollToBottom={messages.length ? messages : false}
38
+ bind:this={chatContainer}
39
+ >
40
+ <div class="mx-auto flex h-full max-w-3xl flex-col gap-5 px-5 pt-6 sm:gap-8 xl:max-w-4xl">
41
+ {#each messages as message, i}
42
+ <ChatMessage
43
+ loading={loading && i === messages.length - 1}
44
+ {message}
45
+ model={currentModel}
46
+ on:retry={() => dispatch("retry", { id: message.id, content: message.content })}
47
+ />
48
+ {:else}
49
+ {#if models}
50
+ <ChatIntroduction {settings} {models} {currentModel} on:message />
51
+ {/if}
52
+ {/each}
53
+ {#if pending}
54
+ <ChatMessage
55
+ message={{ from: "assistant", content: "", id: randomUUID() }}
56
+ model={currentModel}
57
+ />
58
+ {/if}
59
+ <div class="h-32 flex-none" />
60
+ </div>
61
+ <ScrollToBottomBtn
62
+ class="bottom-36 right-4 max-md:hidden lg:right-10"
63
+ scrollNode={chatContainer}
64
+ />
65
+ </div>
src/lib/components/chat/ChatWindow.svelte ADDED
@@ -0,0 +1,102 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <script lang="ts">
2
+ import type { Message } from "$lib/types/Message";
3
+ import { createEventDispatcher } from "svelte";
4
+
5
+ import CarbonSendAltFilled from "~icons/carbon/send-alt-filled";
6
+ import CarbonExport from "~icons/carbon/export";
7
+
8
+ import ChatMessages from "./ChatMessages.svelte";
9
+ import ChatInput from "./ChatInput.svelte";
10
+ import StopGeneratingBtn from "../StopGeneratingBtn.svelte";
11
+ import type { Model } from "$lib/types/Model";
12
+ import type { LayoutData } from "../../../routes/$types";
13
+
14
+ export let messages: Message[] = [];
15
+ export let disabled = false;
16
+ export let loading = false;
17
+ export let pending = false;
18
+ export let currentModel: Model;
19
+ export let models: Model[] | undefined = undefined;
20
+ export let settings: LayoutData["settings"];
21
+
22
+ let message: string;
23
+
24
+ const dispatch = createEventDispatcher<{
25
+ message: string;
26
+ share: void;
27
+ stop: void;
28
+ retry: { id: Message["id"]; content: string };
29
+ }>();
30
+
31
+ const handleSubmit = () => {
32
+ if (loading) return;
33
+ dispatch("message", message);
34
+ message = "";
35
+ };
36
+ </script>
37
+
38
+ <div class="relative min-h-0 min-w-0">
39
+ <ChatMessages
40
+ {loading}
41
+ {pending}
42
+ {settings}
43
+ {currentModel}
44
+ {models}
45
+ {messages}
46
+ on:message
47
+ on:retry={(ev) => {
48
+ if (!loading) dispatch("retry", ev.detail);
49
+ }}
50
+ />
51
+ <div
52
+ class="dark:via-gray-80 pointer-events-none absolute inset-x-0 bottom-0 z-0 mx-auto flex w-full max-w-3xl flex-col items-center justify-center bg-gradient-to-t from-white via-white/80 to-white/0 px-3.5 py-4 dark:border-gray-800 dark:from-gray-900 dark:to-gray-900/0 max-md:border-t max-md:bg-white max-md:dark:bg-gray-900 sm:px-5 md:py-8 xl:max-w-4xl [&>*]:pointer-events-auto"
53
+ >
54
+ <StopGeneratingBtn
55
+ visible={loading}
56
+ className="right-5 mr-[1px] md:mr-0 md:right-7 top-6 md:top-10 z-10"
57
+ on:click={() => dispatch("stop")}
58
+ />
59
+ <form
60
+ on:submit|preventDefault={handleSubmit}
61
+ class="relative flex w-full max-w-4xl flex-1 items-center rounded-xl border bg-gray-100 focus-within:border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:focus-within:border-gray-500 "
62
+ >
63
+ <div class="flex w-full flex-1 border-none bg-transparent">
64
+ <ChatInput
65
+ placeholder="Ask anything"
66
+ bind:value={message}
67
+ on:submit={handleSubmit}
68
+ autofocus
69
+ maxRows={4}
70
+ />
71
+ <button
72
+ class="btn mx-1 my-1 h-[2.4rem] self-end rounded-lg bg-transparent p-1 px-[0.7rem] text-gray-400 disabled:opacity-60 enabled:hover:text-gray-700 dark:disabled:opacity-40 enabled:dark:hover:text-gray-100"
73
+ disabled={!message || loading || disabled}
74
+ type="submit"
75
+ >
76
+ <CarbonSendAltFilled />
77
+ </button>
78
+ </div>
79
+ </form>
80
+ <div class="mt-2 flex justify-between self-stretch px-1 text-xs text-gray-400/90 max-sm:gap-2">
81
+ <p>
82
+ Model: <a
83
+ href="https://huggingface.co/{currentModel.name}"
84
+ target="_blank"
85
+ rel="noreferrer"
86
+ class="hover:underline">{currentModel.displayName}</a
87
+ > <span class="max-sm:hidden">·</span><br class="sm:hidden" /> Generated content may be inaccurate
88
+ or false.
89
+ </p>
90
+ {#if messages.length}
91
+ <button
92
+ class="flex flex-none items-center hover:text-gray-400 hover:underline max-sm:rounded-lg max-sm:bg-gray-50 max-sm:px-2.5 dark:max-sm:bg-gray-800"
93
+ type="button"
94
+ on:click={() => dispatch("share")}
95
+ >
96
+ <CarbonExport class="text-[.6rem] sm:mr-1.5 sm:text-yellow-500" />
97
+ <div class="max-sm:hidden">Share this conversation</div>
98
+ </button>
99
+ {/if}
100
+ </div>
101
+ </div>
102
+ </div>
src/lib/components/icons/IconChevron.svelte ADDED
@@ -0,0 +1,20 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <script lang="ts">
2
+ export let classNames = "";
3
+ </script>
4
+
5
+ <svg
6
+ width="1em"
7
+ height="1em"
8
+ viewBox="0 0 15 6"
9
+ class={classNames}
10
+ fill="none"
11
+ xmlns="http://www.w3.org/2000/svg"
12
+ >
13
+ <path
14
+ d="M1.67236 1L7.67236 7L13.6724 1"
15
+ stroke="currentColor"
16
+ stroke-width="2"
17
+ stroke-linecap="round"
18
+ stroke-linejoin="round"
19
+ />
20
+ </svg>
src/lib/components/icons/IconCopy.svelte ADDED
@@ -0,0 +1,26 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <script lang="ts">
2
+ export let classNames = "";
3
+ </script>
4
+
5
+ <svg
6
+ class={classNames}
7
+ xmlns="http://www.w3.org/2000/svg"
8
+ aria-hidden="true"
9
+ fill="currentColor"
10
+ focusable="false"
11
+ role="img"
12
+ width="1em"
13
+ height="1em"
14
+ preserveAspectRatio="xMidYMid meet"
15
+ viewBox="0 0 32 32"
16
+ >
17
+ <path
18
+ d="M28,10V28H10V10H28m0-2H10a2,2,0,0,0-2,2V28a2,2,0,0,0,2,2H28a2,2,0,0,0,2-2V10a2,2,0,0,0-2-2Z"
19
+ transform="translate(0)"
20
+ />
21
+ <path d="M4,18H2V4A2,2,0,0,1,4,2H18V4H4Z" transform="translate(0)" /><rect
22
+ fill="none"
23
+ width="32"
24
+ height="32"
25
+ />
26
+ </svg>
src/lib/components/icons/IconDazzled.svelte ADDED
@@ -0,0 +1,36 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <script lang="ts">
2
+ export let classNames = "";
3
+ </script>
4
+
5
+ <svg
6
+ xmlns="http://www.w3.org/2000/svg"
7
+ width="1em"
8
+ height="1em"
9
+ class={classNames}
10
+ fill="none"
11
+ viewBox="0 0 26 23"
12
+ >
13
+ <path
14
+ fill="url(#a)"
15
+ d="M.93 10.65A10.17 10.17 0 0 1 11.11.48h4.67a9.45 9.45 0 0 1 0 18.89H4.53L1.62 22.2a.38.38 0 0 1-.69-.28V10.65Z"
16
+ />
17
+ <path
18
+ fill="#000"
19
+ fill-rule="evenodd"
20
+ d="M11.52 7.4a1.86 1.86 0 1 1-3.72 0 1.86 1.86 0 0 1 3.72 0Zm7.57 0a1.86 1.86 0 1 1-3.73 0 1.86 1.86 0 0 1 3.73 0ZM8.9 12.9a.55.55 0 0 0-.11.35.76.76 0 0 1-1.51 0c0-.95.67-1.94 1.76-1.94 1.09 0 1.76 1 1.76 1.94H9.3a.55.55 0 0 0-.12-.35c-.06-.07-.1-.08-.13-.08s-.08 0-.14.08Zm4.04 0a.55.55 0 0 0-.12.35h-1.51c0-.95.68-1.94 1.76-1.94 1.1 0 1.77 1 1.77 1.94h-1.51a.55.55 0 0 0-.12-.35c-.06-.07-.11-.08-.14-.08-.02 0-.07 0-.13.08Zm-1.89.79c-.02 0-.07-.01-.13-.08a.55.55 0 0 1-.12-.36h-1.5c0 .95.67 1.95 1.75 1.95 1.1 0 1.77-1 1.77-1.95h-1.51c0 .16-.06.28-.12.36-.06.07-.11.08-.14.08Zm4.04 0c-.03 0-.08-.01-.14-.08a.55.55 0 0 1-.12-.36h-1.5c0 .95.67 1.95 1.76 1.95 1.08 0 1.76-1 1.76-1.95h-1.51c0 .16-.06.28-.12.36-.06.07-.11.08-.13.08Zm1.76-.44c0-.16.05-.28.12-.35.06-.07.1-.08.13-.08s.08 0 .14.08c.06.07.11.2.11.35a.76.76 0 0 0 1.51 0c0-.95-.67-1.94-1.76-1.94-1.09 0-1.76 1-1.76 1.94h1.5Z"
21
+ clip-rule="evenodd"
22
+ />
23
+ <defs>
24
+ <radialGradient
25
+ id="a"
26
+ cx="0"
27
+ cy="0"
28
+ r="1"
29
+ gradientTransform="matrix(0 31.37 -34.85 0 13.08 -9.02)"
30
+ gradientUnits="userSpaceOnUse"
31
+ >
32
+ <stop stop-color="#FFD21E" />
33
+ <stop offset="1" stop-color="red" />
34
+ </radialGradient>
35
+ </defs>
36
+ </svg>
src/lib/components/icons/IconLoading.svelte ADDED
@@ -0,0 +1,31 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <script lang="ts">
2
+ export let classNames = "";
3
+ </script>
4
+
5
+ <svg
6
+ xmlns="http://www.w3.org/2000/svg"
7
+ width="40px"
8
+ height="25px"
9
+ viewBox="0 0 60 40"
10
+ preserveAspectRatio="xMidYMid"
11
+ class={classNames}
12
+ >
13
+ {#each Array(3) as _, index}
14
+ <g transform={`translate(${20 * index + 10} 20)`}>
15
+ {index}
16
+ <circle cx="0" cy="0" r="6" fill="currentColor">
17
+ <animateTransform
18
+ attributeName="transform"
19
+ type="scale"
20
+ begin={`${-0.375 + 0.15 * index}s`}
21
+ calcMode="spline"
22
+ keySplines="0.3 0 0.7 1;0.3 0 0.7 1"
23
+ values="0.5;1;0.5"
24
+ keyTimes="0;0.5;1"
25
+ dur="1s"
26
+ repeatCount="indefinite"
27
+ />
28
+ </circle>
29
+ </g>
30
+ {/each}
31
+ </svg>
src/lib/components/icons/Logo.svelte ADDED
@@ -0,0 +1,25 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <script lang="ts">
2
+ export let classNames = "";
3
+ </script>
4
+
5
+ <svg
6
+ width="1em"
7
+ height="1em"
8
+ class={classNames}
9
+ viewBox="0 0 13 12"
10
+ fill="none"
11
+ xmlns="http://www.w3.org/2000/svg"
12
+ >
13
+ <path
14
+ fill="#FFD21E"
15
+ d="M1.76 5.63a3.7 3.7 0 0 1 3.7-3.7h1.7a3.43 3.43 0 0 1 0 6.87H3.07L2.01 9.83a.14.14 0 0 1-.25-.1v-4.1Z"
16
+ />
17
+ <path
18
+ fill="#32343D"
19
+ d="M7.37 4.8c.13.05.19.33.33.25a.54.54 0 0 0 .22-.73.54.54 0 0 0-.73-.22.54.54 0 0 0-.22.73c.06.13.27-.08.4-.03ZM4.83 4.8c-.14.05-.2.33-.33.25a.54.54 0 0 1-.23-.73A.54.54 0 0 1 5 4.1c.26.14.36.47.22.73-.06.13-.27-.08-.4-.03ZM6.12 7.4c1.06 0 1.4-.96 1.4-1.44 0-.49-.62.26-1.4.26-.77 0-1.4-.75-1.4-.26 0 .48.34 1.43 1.4 1.43Z"
20
+ />
21
+ <path
22
+ fill="#FF323D"
23
+ d="M6.97 7.12c-.2.16-.49.27-.85.27-.34 0-.6-.1-.81-.24a.94.94 0 0 1 .57-.49c.04-.01.09.06.13.14.05.07.1.15.14.15.05 0 .1-.08.14-.15.05-.08.1-.15.14-.13a.93.93 0 0 1 .54.45Z"
24
+ />
25
+ </svg>
src/lib/constants/publicSepToken.ts ADDED
@@ -0,0 +1 @@
 
 
1
+ export const PUBLIC_SEP_TOKEN = "</s>";
src/lib/server/abortedGenerations.ts ADDED
@@ -0,0 +1,29 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // Shouldn't be needed if we dove into sveltekit internals, see https://github.com/huggingface/chat-ui/pull/88#issuecomment-1523173850
2
+
3
+ import { setTimeout } from "node:timers/promises";
4
+ import { collections } from "./database";
5
+
6
+ let closed = false;
7
+ process.on("SIGINT", () => {
8
+ closed = true;
9
+ });
10
+
11
+ export let abortedGenerations: Map<string, Date> = new Map();
12
+
13
+ async function maintainAbortedGenerations() {
14
+ while (!closed) {
15
+ await setTimeout(1000);
16
+
17
+ try {
18
+ const aborts = await collections.abortedGenerations.find({}).sort({ createdAt: 1 }).toArray();
19
+
20
+ abortedGenerations = new Map(
21
+ aborts.map(({ conversationId, createdAt }) => [conversationId.toString(), createdAt])
22
+ );
23
+ } catch (err) {
24
+ console.error(err);
25
+ }
26
+ }
27
+ }
28
+
29
+ maintainAbortedGenerations();
src/lib/server/database.ts ADDED
@@ -0,0 +1,31 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { MONGODB_URL, MONGODB_DB_NAME } from "$env/static/private";
2
+ import { MongoClient } from "mongodb";
3
+ import type { Conversation } from "$lib/types/Conversation";
4
+ import type { SharedConversation } from "$lib/types/SharedConversation";
5
+ import type { AbortedGeneration } from "$lib/types/AbortedGeneration";
6
+ import type { Settings } from "$lib/types/Settings";
7
+
8
+ const client = new MongoClient(MONGODB_URL, {
9
+ // directConnection: true
10
+ });
11
+
12
+ export const connectPromise = client.connect().catch(console.error);
13
+
14
+ const db = client.db(MONGODB_DB_NAME);
15
+
16
+ const conversations = db.collection<Conversation>("conversations");
17
+ const sharedConversations = db.collection<SharedConversation>("sharedConversations");
18
+ const abortedGenerations = db.collection<AbortedGeneration>("abortedGenerations");
19
+ const settings = db.collection<Settings>("settings");
20
+
21
+ export { client, db };
22
+ export const collections = { conversations, sharedConversations, abortedGenerations, settings };
23
+
24
+ client.on("open", () => {
25
+ conversations.createIndex({ sessionId: 1, updatedAt: -1 });
26
+ abortedGenerations.createIndex({ updatedAt: 1 }, { expireAfterSeconds: 30 });
27
+ abortedGenerations.createIndex({ conversationId: 1 }, { unique: true });
28
+ sharedConversations.createIndex({ hash: 1 }, { unique: true });
29
+ // Sparse so that we can have settings on userId later
30
+ settings.createIndex({ sessionId: 1 }, { unique: true, sparse: true });
31
+ });
src/lib/server/modelEndpoint.ts ADDED
@@ -0,0 +1,32 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { HF_ACCESS_TOKEN } from "$env/static/private";
2
+ import { sum } from "$lib/utils/sum";
3
+ import type { BackendModel } from "./models";
4
+
5
+ /**
6
+ * Find a random load-balanced endpoint
7
+ */
8
+ export function modelEndpoint(model: BackendModel): {
9
+ url: string;
10
+ authorization: string;
11
+ weight: number;
12
+ } {
13
+ if (!model.endpoints) {
14
+ return {
15
+ url: `https://api-inference.huggingface.co/models/${model.name}`,
16
+ authorization: `Bearer ${HF_ACCESS_TOKEN}`,
17
+ weight: 1,
18
+ };
19
+ }
20
+ const endpoints = model.endpoints;
21
+ const totalWeight = sum(endpoints.map((e) => e.weight));
22
+
23
+ let random = Math.random() * totalWeight;
24
+ for (const endpoint of endpoints) {
25
+ if (random < endpoint.weight) {
26
+ return endpoint;
27
+ }
28
+ random -= endpoint.weight;
29
+ }
30
+
31
+ throw new Error("Invalid config, no endpoint found");
32
+ }