BetterAPI coyotte508 HF staff commited on
Commit
1ef0413
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 +26 -0
  2. .eslintignore +13 -0
  3. .eslintrc.cjs +23 -0
  4. .gitignore +10 -0
  5. .npmrc +1 -0
  6. .prettierignore +13 -0
  7. .prettierrc +8 -0
  8. .vscode/settings.json +7 -0
  9. Dockerfile +16 -0
  10. PRIVACY.md +35 -0
  11. README.md +69 -0
  12. package-lock.json +0 -0
  13. package.json +50 -0
  14. postcss.config.js +6 -0
  15. src/app.d.ts +17 -0
  16. src/app.html +45 -0
  17. src/hooks.server.ts +37 -0
  18. src/lib/actions/snapScrollToBottom.ts +54 -0
  19. src/lib/buildPrompt.ts +33 -0
  20. src/lib/components/CodeBlock.svelte +27 -0
  21. src/lib/components/CopyToClipBoardBtn.svelte +50 -0
  22. src/lib/components/EthicsModal.svelte +44 -0
  23. src/lib/components/MobileNav.svelte +62 -0
  24. src/lib/components/Modal.svelte +13 -0
  25. src/lib/components/NavMenu.svelte +109 -0
  26. src/lib/components/ScrollToBottomBtn.svelte +46 -0
  27. src/lib/components/SettingsModal.svelte +59 -0
  28. src/lib/components/StopGeneratingBtn.svelte +17 -0
  29. src/lib/components/Switch.svelte +21 -0
  30. src/lib/components/Toast.svelte +19 -0
  31. src/lib/components/Tooltip.svelte +22 -0
  32. src/lib/components/chat/ChatInput.svelte +54 -0
  33. src/lib/components/chat/ChatIntroduction.svelte +112 -0
  34. src/lib/components/chat/ChatMessage.svelte +116 -0
  35. src/lib/components/chat/ChatMessages.svelte +54 -0
  36. src/lib/components/chat/ChatWindow.svelte +95 -0
  37. src/lib/components/icons/IconChevron.svelte +20 -0
  38. src/lib/components/icons/IconCopy.svelte +26 -0
  39. src/lib/components/icons/IconDazzled.svelte +36 -0
  40. src/lib/components/icons/IconLoading.svelte +31 -0
  41. src/lib/components/icons/Logo.svelte +25 -0
  42. src/lib/server/abortedGenerations.ts +29 -0
  43. src/lib/server/database.ts +31 -0
  44. src/lib/server/modelEndpoint.ts +21 -0
  45. src/lib/shareConversation.ts +27 -0
  46. src/lib/stores/errors.ts +7 -0
  47. src/lib/stores/pendingMessage.ts +3 -0
  48. src/lib/stores/pendingMessageIdToRetry.ts +4 -0
  49. src/lib/switchTheme.ts +10 -0
  50. src/lib/types/AbortedGeneration.ts +8 -0
.env ADDED
@@ -0,0 +1,26 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Use .env.local to change these variables, or directly change your env
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
+
8
+ # Increase depending on the model
9
+ PUBLIC_MAX_INPUT_TOKENS=1000
10
+ PUBLIC_ORIGIN=#https://hf.co
11
+ PUBLIC_MODEL_NAME=OpenAssistant/oasst-sft-6-llama-30b # public facing link
12
+ PUBLIC_MODEL_ID=OpenAssistant/oasst-sft-6-llama-30b-xor # used to link to model page
13
+ PUBLIC_DISABLE_INTRO_TILES=false
14
+ PUBLIC_USER_MESSAGE_TOKEN=<|prompter|>
15
+ PUBLIC_ASSISTANT_MESSAGE_TOKEN=<|assistant|>
16
+ PUBLIC_SEP_TOKEN=</s>
17
+ PUBLIC_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."
18
+ PUBLIC_GOOGLE_ANALYTICS_ID=#G-XXXXXXXX / Leave empty to disable
19
+
20
+ # Copy this in .env.local with and replace "hf_<token>" your HF token from https://huggingface.co/settings/token
21
+ # You can also change the model from OpenAssistant/oasst-sft-4-pythia-12b-epoch-3.5 to your own model
22
+ MODEL_ENDPOINTS=`[{
23
+ "endpoint": "https://api-inference.huggingface.co/models/OpenAssistant/oasst-sft-4-pythia-12b-epoch-3.5",
24
+ "authorization": "Bearer hf_<token>",
25
+ "weight": 1
26
+ }]`
.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,23 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
+ },
18
+ env: {
19
+ browser: true,
20
+ es2017: true,
21
+ node: true,
22
+ },
23
+ };
.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,69 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
+ MODEL_ENDPOINTS=`[{
36
+ "endpoint": "https://api-inference.huggingface.co/models/OpenAssistant/oasst-sft-4-pythia-12b-epoch-3.5",
37
+ "authorization": "Bearer <hf_token>",
38
+ "weight": 1
39
+ }]`
40
+ ```
41
+
42
+ Where the contents in `<...>` are replaced by the MongoDB URL and your [HF Access Token](https://huggingface.co/settings/tokens).
43
+
44
+ ## Duplicating to a Space
45
+
46
+ Create a `DOTENV_LOCAL` secret to your space with the following contents:
47
+
48
+ ```
49
+ MONGODB_URL=<url to mongo, for example a free MongoDB Atlas sandbox instance>
50
+ MODEL_ENDPOINTS=`[{
51
+ "endpoint": "https://api-inference.huggingface.co/models/OpenAssistant/oasst-sft-4-pythia-12b-epoch-3.5",
52
+ "authorization": "Bearer <hf_token>",
53
+ "weight": 1
54
+ }]`
55
+ ```
56
+
57
+ Where the contents in `<...>` are replaced by the MongoDB URL and your [HF Access Token](https://huggingface.co/settings/tokens).
58
+
59
+ ## Building
60
+
61
+ To create a production version of your app:
62
+
63
+ ```bash
64
+ npm run build
65
+ ```
66
+
67
+ You can preview the production build with `npm run preview`.
68
+
69
+ > 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,50 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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.0",
17
+ "@sveltejs/kit": "^1.5.0",
18
+ "@tailwindcss/typography": "^0.5.9",
19
+ "@types/marked": "^4.0.8",
20
+ "@typescript-eslint/eslint-plugin": "^5.45.0",
21
+ "@typescript-eslint/parser": "^5.45.0",
22
+ "eslint": "^8.28.0",
23
+ "eslint-config-prettier": "^8.5.0",
24
+ "eslint-plugin-svelte3": "^4.0.0",
25
+ "prettier": "^2.8.0",
26
+ "prettier-plugin-svelte": "^2.8.1",
27
+ "prettier-plugin-tailwindcss": "^0.2.7",
28
+ "svelte": "^3.54.0",
29
+ "svelte-check": "^3.0.1",
30
+ "tslib": "^2.4.1",
31
+ "typescript": "^4.9.3",
32
+ "unplugin-icons": "^0.16.1",
33
+ "vite": "^4.0.0"
34
+ },
35
+ "type": "module",
36
+ "dependencies": {
37
+ "@huggingface/inference": "^2.2.0",
38
+ "autoprefixer": "^10.4.14",
39
+ "date-fns": "^2.29.3",
40
+ "dotenv": "^16.0.3",
41
+ "highlight.js": "^11.7.0",
42
+ "marked": "^4.3.0",
43
+ "mongodb": "^5.3.0",
44
+ "nanoid": "^4.0.2",
45
+ "postcss": "^8.4.21",
46
+ "tailwind-scrollbar": "^3.0.0",
47
+ "tailwindcss": "^3.3.1",
48
+ "zod": "^3.21.4"
49
+ }
50
+ }
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,45 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
+ </script>
19
+ %sveltekit.head%
20
+ </head>
21
+ <body data-sveltekit-preload-data="hover" class="h-full dark:bg-gray-900">
22
+ <div class="contents h-full">%sveltekit.body%</div>
23
+
24
+ <!-- Google Tag Manager -->
25
+ <script>
26
+ if (window.gaId) {
27
+ const script = document.createElement("script");
28
+ script.src = "https://www.googletagmanager.com/gtag/js?id=" + window.gaId;
29
+ script.async = true;
30
+ document.head.appendChild(script);
31
+
32
+ window.dataLayer = window.dataLayer || [];
33
+ function gtag() {
34
+ dataLayer.push(arguments);
35
+ }
36
+ gtag("js", new Date());
37
+ /// ^ See https://developers.google.com/tag-platform/gtagjs/install
38
+ gtag("config", window.gaId);
39
+ gtag("consent", "default", { ad_storage: "denied", analytics_storage: "denied" });
40
+ /// ^ See https://developers.google.com/tag-platform/gtagjs/reference#consent
41
+ /// TODO: ask the user for their consent and update this with gtag('consent', 'update')
42
+ }
43
+ </script>
44
+ </body>
45
+ </html>
src/hooks.server.ts ADDED
@@ -0,0 +1,37 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { dev } from "$app/environment";
2
+ import { COOKIE_NAME } from "$env/static/private";
3
+ import type { Handle } from "@sveltejs/kit";
4
+ import { PUBLIC_GOOGLE_ANALYTICS_ID } from "$env/static/public";
5
+ import { addYears } from "date-fns";
6
+
7
+ export const handle: Handle = async ({ event, resolve }) => {
8
+ const token = event.cookies.get(COOKIE_NAME);
9
+
10
+ event.locals.sessionId = token || crypto.randomUUID();
11
+
12
+ // Refresh cookie expiration date
13
+ event.cookies.set(COOKIE_NAME, event.locals.sessionId, {
14
+ path: "/",
15
+ // So that it works inside the space's iframe
16
+ sameSite: dev ? "lax" : "none",
17
+ secure: !dev,
18
+ httpOnly: true,
19
+ expires: addYears(new Date(), 1),
20
+ });
21
+
22
+ let replaced = false;
23
+
24
+ const response = await resolve(event, {
25
+ transformPageChunk: (chunk) => {
26
+ // For some reason, Sveltekit doesn't let us load env variables from .env in the app.html template
27
+ if (replaced || !chunk.html.includes("%gaId%")) {
28
+ return chunk.html;
29
+ }
30
+ replaced = true;
31
+
32
+ return chunk.html.replace("%gaId%", PUBLIC_GOOGLE_ANALYTICS_ID);
33
+ },
34
+ });
35
+
36
+ return response;
37
+ };
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: any) => {
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,33 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import {
2
+ PUBLIC_ASSISTANT_MESSAGE_TOKEN,
3
+ PUBLIC_MAX_INPUT_TOKENS,
4
+ PUBLIC_PREPROMPT,
5
+ PUBLIC_SEP_TOKEN,
6
+ PUBLIC_USER_MESSAGE_TOKEN,
7
+ } from "$env/static/public";
8
+ import type { Message } from "./types/Message";
9
+
10
+ /**
11
+ * Convert [{user: "assistant", content: "hi"}, {user: "user", content: "hello"}] to:
12
+ *
13
+ * <|assistant|>hi<|endoftext|><|prompter|>hello<|endoftext|><|assistant|>
14
+ */
15
+ export function buildPrompt(messages: Message[]): string {
16
+ const prompt =
17
+ messages
18
+ .map(
19
+ (m) =>
20
+ (m.from === "user"
21
+ ? PUBLIC_USER_MESSAGE_TOKEN + m.content
22
+ : PUBLIC_ASSISTANT_MESSAGE_TOKEN + m.content) +
23
+ (m.content.endsWith(PUBLIC_SEP_TOKEN) ? "" : PUBLIC_SEP_TOKEN)
24
+ )
25
+ .join("") + PUBLIC_ASSISTANT_MESSAGE_TOKEN;
26
+
27
+ // Not super precise, but it's truncated in the model's backend anyway
28
+ return (
29
+ PUBLIC_PREPROMPT +
30
+ "\n-----\n" +
31
+ prompt.split(" ").slice(-parseInt(PUBLIC_MAX_INPUT_TOKENS)).join(" ")
32
+ );
33
+ }
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: any;
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,44 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <script lang="ts">
2
+ import { PUBLIC_VERSION } from "$env/static/public";
3
+ import Logo from "$lib/components/icons/Logo.svelte";
4
+ import Modal from "$lib/components/Modal.svelte";
5
+ import type { Settings } from "$lib/types/Settings";
6
+ import { updateSettings } from "$lib/updateSettings";
7
+
8
+ export let settings: Omit<Settings, "sessionId">;
9
+ </script>
10
+
11
+ <Modal>
12
+ <div
13
+ 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"
14
+ >
15
+ <h2 class="flex items-center text-2xl font-semibold text-gray-800">
16
+ <Logo classNames="text-3xl mr-1.5" />HuggingChat
17
+ {#if typeof PUBLIC_VERSION !== "undefined"}
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
+ {/if}
24
+ </h2>
25
+ <p class="px-4 text-lg font-semibold leading-snug text-gray-800 sm:px-12">
26
+ This application is for demonstration purposes only.
27
+ </p>
28
+ <p class="text-gray-800">
29
+ AI is an area of active research with known problems such as biased generation and
30
+ misinformation. Do not use this application for high-stakes decisions or advice.
31
+ </p>
32
+ <p class="px-2 text-sm text-gray-500">
33
+ Your conversations will be shared with model authors unless you disable it from your settings.
34
+ </p>
35
+ <!-- The updateSettings call will invalidate the settings, which will reload the page without the modal -->
36
+ <button
37
+ type="button"
38
+ on:click={() => updateSettings({ ...settings, ethicsModalAcceptedAt: new Date() })}
39
+ class="mt-2 rounded-full bg-black px-5 py-2 text-lg font-semibold text-gray-100 transition-colors hover:bg-yellow-500"
40
+ >
41
+ Start chatting
42
+ </button>
43
+ </div>
44
+ </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,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <script lang="ts">
2
+ import { cubicOut } from "svelte/easing";
3
+ import { fade } from "svelte/transition";
4
+ </script>
5
+
6
+ <div
7
+ transition:fade={{ easing: cubicOut, duration: 300 }}
8
+ class="fixed inset-0 z-40 flex items-center justify-center bg-black/80 p-8 backdrop-blur-sm dark:bg-black/50"
9
+ >
10
+ <div class="-mt-10 max-w-sm overflow-hidden rounded-2xl bg-white shadow-2xl md:-mt-20">
11
+ <slot />
12
+ </div>
13
+ </div>
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/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: boolean = 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,59 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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 { updateSettings } from "$lib/updateSettings";
9
+
10
+ export let settings: Pick<Settings, "shareConversationsWithModelAuthors">;
11
+
12
+ const dispatch = createEventDispatcher<{ close: void }>();
13
+ </script>
14
+
15
+ <Modal>
16
+ <div class="flex w-full flex-col gap-5 p-6">
17
+ <div class="flex items-start justify-between text-xl font-semibold text-gray-800">
18
+ <h2>Settings</h2>
19
+ <button class="group" on:click={() => dispatch("close")}>
20
+ <CarbonClose class="text-gray-900 group-hover:text-gray-500" />
21
+ </button>
22
+ </div>
23
+
24
+ <label class="flex cursor-pointer select-none items-center gap-2 text-gray-500" for="switch">
25
+ <Switch name="switch" bind:checked={settings.shareConversationsWithModelAuthors} />
26
+ Share conversations with model authors
27
+ </label>
28
+
29
+ <p class="text-gray-800">
30
+ Sharing your data will help improve the training data and make open models better over time.
31
+ </p>
32
+ <p class="text-gray-800">
33
+ You can change this setting at any time, it applies to all your conversations.
34
+ </p>
35
+ <p class="text-gray-800">
36
+ Read more about this model's authors,
37
+ <a
38
+ href="https://open-assistant.io/"
39
+ target="_blank"
40
+ rel="noreferrer"
41
+ class="underline decoration-gray-300 hover:decoration-gray-700">Open Assistant</a
42
+ >.
43
+ </p>
44
+ <button
45
+ type="button"
46
+ 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"
47
+ on:click={() =>
48
+ updateSettings({
49
+ shareConversationsWithModelAuthors: settings.shareConversationsWithModelAuthors,
50
+ }).then((res) => {
51
+ if (res) {
52
+ dispatch("close");
53
+ }
54
+ })}
55
+ >
56
+ Apply
57
+ </button>
58
+ </div>
59
+ </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: boolean = 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,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <script lang="ts">
2
+ export let checked: boolean;
3
+ export let name: string;
4
+ </script>
5
+
6
+ <div
7
+ class="relative inline-flex h-5 w-9 items-center rounded-full p-1 shadow-inner transition-all {checked
8
+ ? 'bg-black'
9
+ : 'bg-gray-300 hover:bg-gray-400'}"
10
+ >
11
+ <input
12
+ bind:checked
13
+ type="checkbox"
14
+ {name}
15
+ id={name}
16
+ class="peer absolute inset-0 cursor-pointer opacity-0"
17
+ />
18
+ <div
19
+ class="h-3.5 w-3.5 rounded-full bg-white shadow-sm transition-all peer-checked:translate-x-3.5"
20
+ />
21
+ </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 py-3"
30
+ aria-hidden="true"
31
+ style="min-height: {minHeight}; max-height: {maxHeight}">{value + "&nbsp;\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 overflow-x-hidden overflow-y-scroll border-0 bg-transparent p-3 outline-none focus:ring-0 focus-visible:ring-0 dark:bg-transparent"
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,112 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <script lang="ts">
2
+ import {
3
+ PUBLIC_DISABLE_INTRO_TILES,
4
+ PUBLIC_MODEL_ID,
5
+ PUBLIC_MODEL_NAME,
6
+ PUBLIC_VERSION,
7
+ } from "$env/static/public";
8
+
9
+ import Logo from "$lib/components/icons/Logo.svelte";
10
+ import CarbonArrowUpRight from "~icons/carbon/arrow-up-right";
11
+ import CarbonEarth from "~icons/carbon/earth";
12
+ import { createEventDispatcher } from "svelte";
13
+ const dispatch = createEventDispatcher<{ message: string }>();
14
+ </script>
15
+
16
+ <div class="my-auto grid gap-8 lg:grid-cols-3">
17
+ <div class="lg:col-span-1">
18
+ <div>
19
+ <div class="mb-3 flex items-center text-2xl font-semibold">
20
+ <Logo classNames="mr-1 text-yellow-400 text-4xl" />
21
+ HuggingChat
22
+ {#if typeof PUBLIC_VERSION !== "undefined"}
23
+ <div
24
+ 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"
25
+ >
26
+ v{PUBLIC_VERSION}
27
+ </div>
28
+ {/if}
29
+ </div>
30
+ <p class="text-base text-gray-600 dark:text-gray-400">
31
+ Making the community's best AI chat models available to everyone.
32
+ </p>
33
+ </div>
34
+ </div>
35
+ <div class="lg:col-span-2 lg:pl-24">
36
+ <div class="overflow-hidden rounded-xl border dark:border-gray-800">
37
+ <div class="p-3">
38
+ <div class="text-sm text-gray-600 dark:text-gray-400">Current Model</div>
39
+ <div class="font-semibold">{PUBLIC_MODEL_NAME}</div>
40
+ </div>
41
+ <div
42
+ class="flex items-center gap-5 rounded-xl bg-gray-100 px-3 py-2 text-sm text-gray-600 dark:bg-gray-800 dark:text-gray-300"
43
+ >
44
+ <a
45
+ href="https://huggingface.co/{PUBLIC_MODEL_ID}"
46
+ target="_blank"
47
+ rel="noreferrer"
48
+ class="flex items-center hover:underline"
49
+ >
50
+ <CarbonArrowUpRight class="mr-1.5 text-xs text-gray-400" />
51
+ Model
52
+ <div class="max-sm:hidden">&nbsp;page</div>
53
+ </a>
54
+ <a
55
+ href="https://huggingface.co/datasets/OpenAssistant/oasst1"
56
+ target="_blank"
57
+ rel="noreferrer"
58
+ class="flex items-center hover:underline"
59
+ >
60
+ <CarbonArrowUpRight class="mr-1.5 text-xs text-gray-400" />
61
+ Dataset
62
+ <div class="max-sm:hidden">&nbsp;page</div>
63
+ </a>
64
+ <a
65
+ href="https://open-assistant.io/"
66
+ target="_blank"
67
+ class="ml-auto flex items-center hover:underline"
68
+ rel="noreferrer"
69
+ >
70
+ <CarbonEarth class="mr-1.5 text-xs text-gray-400" />
71
+ Open Assistant Website
72
+ </a>
73
+ </div>
74
+ </div>
75
+ </div>
76
+ {#if PUBLIC_DISABLE_INTRO_TILES !== "true"}
77
+ <div class="lg:col-span-3 lg:mt-12">
78
+ <p class="mb-3 text-gray-600 dark:text-gray-300">Examples</p>
79
+ <div class="grid gap-3 lg:grid-cols-3 lg:gap-5">
80
+ <button
81
+ type="button"
82
+ 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"
83
+ on:click={() =>
84
+ dispatch(
85
+ "message",
86
+ "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)"
87
+ )}
88
+ >
89
+ "Write an email from bullet list"
90
+ </button>
91
+ <button
92
+ type="button"
93
+ 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"
94
+ on:click={() =>
95
+ dispatch(
96
+ "message",
97
+ "Code a basic snake game in python, give explanations for each step."
98
+ )}
99
+ >
100
+ "Code a snake game"
101
+ </button>
102
+ <button
103
+ type="button"
104
+ 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"
105
+ on:click={() => dispatch("message", "How do I make a delicious lemon cheesecake?")}
106
+ >
107
+ "Assist in a task"
108
+ </button>
109
+ </div>
110
+ </div>
111
+ {/if}
112
+ </div>
src/lib/components/chat/ChatMessage.svelte ADDED
@@ -0,0 +1,116 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
+
7
+ import CodeBlock from "../CodeBlock.svelte";
8
+ import IconLoading from "../icons/IconLoading.svelte";
9
+ import CarbonRotate360 from "~icons/carbon/rotate-360";
10
+ import { PUBLIC_SEP_TOKEN } from "$env/static/public";
11
+
12
+ function sanitizeMd(md: string) {
13
+ return md
14
+ .replace(/<\|[a-z]*$/, "")
15
+ .replace(/<\|[a-z]+\|$/, "")
16
+ .replace(/<$/, "")
17
+ .replaceAll(PUBLIC_SEP_TOKEN, " ")
18
+ .replaceAll(/<\|[a-z]+\|>/g, " ")
19
+ .replaceAll(/<br\s?\/?>/gi, "\n")
20
+ .trim()
21
+ .replaceAll("<", "&lt;");
22
+ }
23
+ function unsanitizeMd(md: string) {
24
+ return md.replaceAll("&lt;", "<");
25
+ }
26
+
27
+ export let message: Message;
28
+ export let loading: boolean = false;
29
+
30
+ const dispatch = createEventDispatcher<{ retry: void }>();
31
+
32
+ let contentEl: HTMLElement;
33
+ let loadingEl: any;
34
+ let pendingTimeout: NodeJS.Timeout;
35
+
36
+ const renderer = new marked.Renderer();
37
+
38
+ // For code blocks with simple backticks
39
+ renderer.codespan = (code) => {
40
+ // Unsanitize double-sanitized code
41
+ return `<code>${code.replaceAll("&amp;", "&")}</code>`;
42
+ };
43
+
44
+ const options: marked.MarkedOptions = {
45
+ ...marked.getDefaults(),
46
+ gfm: true,
47
+ breaks: true,
48
+ renderer,
49
+ };
50
+
51
+ $: tokens = marked.lexer(sanitizeMd(message.content));
52
+
53
+ afterUpdate(() => {
54
+ loadingEl?.$destroy();
55
+ clearTimeout(pendingTimeout);
56
+
57
+ // Add loading animation to the last message if update takes more than 600ms
58
+ if (loading) {
59
+ pendingTimeout = setTimeout(() => {
60
+ if (contentEl) {
61
+ loadingEl = new IconLoading({
62
+ target: deepestChild(contentEl),
63
+ props: { classNames: "loading inline ml-2" },
64
+ });
65
+ }
66
+ }, 600);
67
+ }
68
+ });
69
+ </script>
70
+
71
+ {#if message.from === "assistant"}
72
+ <div class="flex items-start justify-start gap-4 leading-relaxed">
73
+ <img
74
+ alt=""
75
+ src="https://huggingface.co/avatars/2edb18bd0206c16b433841a47f53fa8e.svg"
76
+ class="mt-5 h-3 w-3 flex-none select-none rounded-full shadow-lg"
77
+ />
78
+ <div
79
+ 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"
80
+ >
81
+ {#if !message.content}
82
+ <IconLoading classNames="absolute inset-0 m-auto" />
83
+ {/if}
84
+ <div
85
+ 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"
86
+ bind:this={contentEl}
87
+ >
88
+ {#each tokens as token}
89
+ {#if token.type === "code"}
90
+ <CodeBlock lang={token.lang} code={unsanitizeMd(token.text)} />
91
+ {:else}
92
+ {@html marked(token.raw, options)}
93
+ {/if}
94
+ {/each}
95
+ </div>
96
+ </div>
97
+ </div>
98
+ {/if}
99
+ {#if message.from === "user"}
100
+ <div class="group relative flex items-start justify-start gap-4 max-sm:text-sm">
101
+ <div class="mt-5 h-3 w-3 flex-none rounded-full" />
102
+ <div class="whitespace-break-spaces rounded-2xl px-5 py-3.5 text-gray-500 dark:text-gray-400">
103
+ {message.content.trim()}
104
+ </div>
105
+ {#if !loading && message.id}
106
+ <button
107
+ class="absolute right-0 top-3.5 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"
108
+ title="Retry"
109
+ type="button"
110
+ on:click={() => dispatch("retry")}
111
+ >
112
+ <CarbonRotate360 />
113
+ </button>
114
+ {/if}
115
+ </div>
116
+ {/if}
src/lib/components/chat/ChatMessages.svelte ADDED
@@ -0,0 +1,54 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
+
11
+ const dispatch = createEventDispatcher<{ retry: { id: Message["id"]; content: string } }>();
12
+
13
+ export let messages: Message[];
14
+ export let loading: boolean;
15
+ export let pending: boolean;
16
+
17
+ let chatContainer: HTMLElement;
18
+
19
+ async function scrollToBottom() {
20
+ await tick();
21
+ chatContainer.scrollTop = chatContainer.scrollHeight;
22
+ }
23
+
24
+ // If last message is from user, scroll to bottom
25
+ $: if (messages[messages.length - 1]?.from === "user") {
26
+ scrollToBottom();
27
+ }
28
+ </script>
29
+
30
+ <div
31
+ class="scrollbar-custom mr-1 h-full overflow-y-auto"
32
+ use:snapScrollToBottom={messages.length ? messages : false}
33
+ bind:this={chatContainer}
34
+ >
35
+ <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">
36
+ {#each messages as message, i}
37
+ <ChatMessage
38
+ loading={loading && i === messages.length - 1}
39
+ {message}
40
+ on:retry={() => dispatch("retry", { id: message.id, content: message.content })}
41
+ />
42
+ {:else}
43
+ <ChatIntroduction on:message />
44
+ {/each}
45
+ {#if pending}
46
+ <ChatMessage message={{ from: "assistant", content: "", id: randomUUID() }} />
47
+ {/if}
48
+ <div class="h-32 flex-none" />
49
+ </div>
50
+ <ScrollToBottomBtn
51
+ class="bottom-36 right-4 max-md:hidden lg:right-10"
52
+ scrollNode={chatContainer}
53
+ />
54
+ </div>
src/lib/components/chat/ChatWindow.svelte ADDED
@@ -0,0 +1,95 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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 { PUBLIC_MODEL_ID, PUBLIC_MODEL_NAME } from "$env/static/public";
12
+
13
+ export let messages: Message[] = [];
14
+ export let disabled: boolean = false;
15
+ export let loading: boolean = false;
16
+ export let pending: boolean = false;
17
+
18
+ let message: string;
19
+
20
+ const dispatch = createEventDispatcher<{
21
+ message: string;
22
+ share: void;
23
+ stop: void;
24
+ retry: { id: Message["id"]; content: string };
25
+ }>();
26
+
27
+ const handleSubmit = () => {
28
+ if (loading) return;
29
+ dispatch("message", message);
30
+ message = "";
31
+ };
32
+ </script>
33
+
34
+ <div class="relative min-h-0 min-w-0">
35
+ <ChatMessages
36
+ {loading}
37
+ {pending}
38
+ {messages}
39
+ on:message
40
+ on:retry={(ev) => {
41
+ if (!loading) dispatch("retry", ev.detail);
42
+ }}
43
+ />
44
+ <div
45
+ 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"
46
+ >
47
+ <StopGeneratingBtn
48
+ visible={loading}
49
+ className="right-5 mr-[1px] md:mr-0 md:right-7 top-6 md:top-10 z-10"
50
+ on:click={() => dispatch("stop")}
51
+ />
52
+ <form
53
+ on:submit|preventDefault={handleSubmit}
54
+ 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 "
55
+ >
56
+ <div class="flex w-full flex-1 border-none bg-transparent">
57
+ <ChatInput
58
+ placeholder="Ask anything"
59
+ bind:value={message}
60
+ on:submit={handleSubmit}
61
+ autofocus
62
+ maxRows={10}
63
+ />
64
+ <button
65
+ 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"
66
+ disabled={!message || loading || disabled}
67
+ type="submit"
68
+ >
69
+ <CarbonSendAltFilled />
70
+ </button>
71
+ </div>
72
+ </form>
73
+ <div class="mt-2 flex justify-between self-stretch px-1 text-xs text-gray-400/90 max-sm:gap-2">
74
+ <p>
75
+ Model: <a
76
+ href="https://huggingface.co/{PUBLIC_MODEL_ID}"
77
+ target="_blank"
78
+ rel="noreferrer"
79
+ class="hover:underline">{PUBLIC_MODEL_NAME}</a
80
+ > <span class="max-sm:hidden">·</span><br class="sm:hidden" /> Generated content may be inaccurate
81
+ or false.
82
+ </p>
83
+ {#if messages.length}
84
+ <button
85
+ 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"
86
+ type="button"
87
+ on:click={() => dispatch("share")}
88
+ >
89
+ <CarbonExport class="text-[.6rem] sm:mr-1.5 sm:text-yellow-500" />
90
+ <div class="max-sm:hidden">Share this conversation</div>
91
+ </button>
92
+ {/if}
93
+ </div>
94
+ </div>
95
+ </div>
src/lib/components/icons/IconChevron.svelte ADDED
@@ -0,0 +1,20 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <script lang="ts">
2
+ export let classNames: string = "";
3
+ </script>
4
+
5
+ <svg
6
+ width="15"
7
+ height="8"
8
+ viewBox="0 0 15 8"
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: string = "";
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: string = "";
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/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,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { MODEL_ENDPOINTS } from "$env/static/private";
2
+ import { sum } from "$lib/utils/sum";
3
+
4
+ const endpoints: Array<{ endpoint: string; authorization: string; weight: number }> =
5
+ JSON.parse(MODEL_ENDPOINTS);
6
+ const totalWeight = sum(endpoints.map((e) => e.weight));
7
+
8
+ /**
9
+ * Find a random load-balanced endpoint
10
+ */
11
+ export function modelEndpoint(): { endpoint: string; authorization: string; weight: number } {
12
+ let random = Math.random() * totalWeight;
13
+ for (const endpoint of endpoints) {
14
+ if (random < endpoint.weight) {
15
+ return endpoint;
16
+ }
17
+ random -= endpoint.weight;
18
+ }
19
+
20
+ throw new Error("Invalid config, no endpoint found");
21
+ }
src/lib/shareConversation.ts ADDED
@@ -0,0 +1,27 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { base } from "$app/paths";
2
+ import { ERROR_MESSAGES, error } from "$lib/stores/errors";
3
+ import { share } from "./utils/share";
4
+
5
+ export async function shareConversation(id: string, title: string) {
6
+ try {
7
+ const res = await fetch(`${base}/conversation/${id}/share`, {
8
+ method: "POST",
9
+ headers: {
10
+ "Content-Type": "application/json",
11
+ },
12
+ });
13
+
14
+ if (!res.ok) {
15
+ error.set("Error while sharing conversation, try again.");
16
+ console.error("Error while sharing conversation: " + (await res.text()));
17
+ return;
18
+ }
19
+
20
+ const { url } = await res.json();
21
+
22
+ share(url, title);
23
+ } catch (err) {
24
+ error.set(ERROR_MESSAGES.default);
25
+ console.error(err);
26
+ }
27
+ }
src/lib/stores/errors.ts ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ import { writable } from "svelte/store";
2
+
3
+ export const ERROR_MESSAGES = {
4
+ default: "Oops, something went wrong.",
5
+ };
6
+
7
+ export const error = writable<string | null>(null);
src/lib/stores/pendingMessage.ts ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ import { writable } from "svelte/store";
2
+
3
+ export const pendingMessage = writable<string>("");
src/lib/stores/pendingMessageIdToRetry.ts ADDED
@@ -0,0 +1,4 @@
 
 
 
 
 
1
+ import type { Message } from "$lib/types/Message";
2
+ import { writable } from "svelte/store";
3
+
4
+ export const pendingMessageIdToRetry = writable<Message["id"] | null>(null);
src/lib/switchTheme.ts ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ export function switchTheme() {
2
+ const { classList } = document.querySelector("html") as HTMLElement;
3
+ if (classList.contains("dark")) {
4
+ classList.remove("dark");
5
+ localStorage.theme = "light";
6
+ } else {
7
+ classList.add("dark");
8
+ localStorage.theme = "dark";
9
+ }
10
+ }
src/lib/types/AbortedGeneration.ts ADDED
@@ -0,0 +1,8 @@
 
 
 
 
 
 
 
 
 
1
+ // Ideally shouldn't be needed, see https://github.com/huggingface/chat-ui/pull/88#issuecomment-1523173850
2
+
3
+ import type { Conversation } from "./Conversation";
4
+ import type { Timestamps } from "./Timestamps";
5
+
6
+ export interface AbortedGeneration extends Timestamps {
7
+ conversationId: Conversation["_id"];
8
+ }