WebraftAI coyotte508 HF Staff commited on
Commit
6178b6e
·
0 Parent(s):

Duplicate from huggingchat/chat-ui

Browse files

Co-authored-by: Eliott Coyac <[email protected]>

This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. .env +19 -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 +22 -0
  10. PRIVACY.md +33 -0
  11. README.md +40 -0
  12. package-lock.json +0 -0
  13. package.json +49 -0
  14. postcss.config.js +6 -0
  15. src/app.d.ts +17 -0
  16. src/app.html +21 -0
  17. src/hooks.server.ts +24 -0
  18. src/lib/actions/snapScrollToBottom.ts +54 -0
  19. src/lib/buildPrompt.ts +28 -0
  20. src/lib/components/CodeBlock.svelte +27 -0
  21. src/lib/components/CopyToClipBoardBtn.svelte +50 -0
  22. src/lib/components/MobileNav.svelte +62 -0
  23. src/lib/components/NavMenu.svelte +97 -0
  24. src/lib/components/ScrollToBottomBtn.svelte +34 -0
  25. src/lib/components/StopGeneratingBtn.svelte +17 -0
  26. src/lib/components/Toast.svelte +19 -0
  27. src/lib/components/Tooltip.svelte +22 -0
  28. src/lib/components/chat/ChatInput.svelte +54 -0
  29. src/lib/components/chat/ChatIntroduction.svelte +110 -0
  30. src/lib/components/chat/ChatMessage.svelte +100 -0
  31. src/lib/components/chat/ChatMessages.svelte +47 -0
  32. src/lib/components/chat/ChatWindow.svelte +82 -0
  33. src/lib/components/icons/IconChevron.svelte +20 -0
  34. src/lib/components/icons/IconCopy.svelte +26 -0
  35. src/lib/components/icons/IconDazzled.svelte +36 -0
  36. src/lib/components/icons/IconLoading.svelte +31 -0
  37. src/lib/components/icons/Logo.svelte +25 -0
  38. src/lib/server/abortedGenerations.ts +29 -0
  39. src/lib/server/database.ts +27 -0
  40. src/lib/server/modelEndpoint.ts +21 -0
  41. src/lib/shareConversation.ts +34 -0
  42. src/lib/stores/errors.ts +7 -0
  43. src/lib/stores/pendingMessage.ts +3 -0
  44. src/lib/switchTheme.ts +10 -0
  45. src/lib/types/AbortedGeneration.ts +9 -0
  46. src/lib/types/Conversation.ts +19 -0
  47. src/lib/types/Message.ts +4 -0
  48. src/lib/types/SharedConversation.ts +13 -0
  49. src/lib/types/UrlDependency.ts +4 -0
  50. src/lib/utils/concatUint8Arrays.ts +12 -0
.env ADDED
@@ -0,0 +1,19 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
+ PUBLIC_MAX_INPUT_TOKENS=1024
9
+ PUBLIC_ORIGIN=#https://hf.co
10
+ PUBLIC_MODEL_NAME=OpenAssistant/oasst-sft-6-llama-30b # public facing link
11
+ PUBLIC_MODEL_ID=OpenAssistant/oasst-sft-6-llama-30b-xor # used to link to model page
12
+ PUBLIC_DISABLE_INTRO_TILES=false
13
+ PUBLIC_USER_MESSAGE_TOKEN=<|prompter|>
14
+ PUBLIC_ASSISTANT_MESSAGE_TOKEN=<|assistant|>
15
+ PUBLIC_SEP_TOKEN=<|endoftext|>
16
+
17
+ # [{"endpoint": "https://api-inference.huggingface.co/models/...", authorization: "Bearer hf_<token>", weight: 1}] to load balance
18
+ # Eg if one endpoint has weight 2 and the other has weight 1, the first endpoint will be called twice as often
19
+ MODEL_ENDPOINTS=`[]`
.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"],
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,22 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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 . .
11
+
12
+ RUN npm i
13
+
14
+ RUN chown -R 1000:1000 /app
15
+
16
+ RUN --mount=type=secret,id=DOTENV_LOCAL,mode=0444,required=true cat /run/secrets/DOTENV_LOCAL > .env.local
17
+
18
+ RUN npm run build
19
+
20
+ ENV PORT 7860
21
+
22
+ CMD pm2 start build/index.js -i $CPU_CORES --no-daemon
PRIVACY.md ADDED
@@ -0,0 +1,33 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ## Privacy
2
+
3
+ In this `v0` of HuggingChat, we only store messages to display them to the user, not for any other usage (including for research or model training purposes).
4
+
5
+ Please note that in `v0`, 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
+ In a future version, we are considering exposing a setting for users to share their conversations with the model authors (here OpenAssistant) to improve their training data and their model over time. In other terms, model authors are the custodians of the data collected by their model, even if it's hosted on our platform.
8
+
9
+ 🗓 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 [email protected].
10
+
11
+ ## About available LLMs
12
+
13
+ The goal of this app is to showcase that it is now (April 2023) possible to build an open source alternative to ChatGPT. 💪
14
+
15
+ 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.
16
+
17
+ 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/ ❤️
18
+
19
+ ## Technical details
20
+
21
+ 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.
22
+ The inference backend is running [text-generation-inference](https://github.com/huggingface/text-generation-inference) on HuggingFace's Inference API infrastructure.
23
+
24
+ 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)
25
+
26
+ We welcome any feedback on this app: please participate to the public discussion at https://huggingface.co/spaces/huggingchat/chat-ui/discussions
27
+
28
+ <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>
29
+
30
+ ## Coming soon
31
+
32
+ - LLM watermarking
33
+ - User setting to share conversations with model authors
README.md ADDED
@@ -0,0 +1,40 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
+ duplicated_from: huggingchat/chat-ui
11
+ ---
12
+
13
+ # Chat UI
14
+
15
+ A chat interface using open source models, eg OpenAssistant.
16
+
17
+ ## Launch
18
+
19
+ ```bash
20
+ npm install
21
+ npm run dev
22
+ ```
23
+
24
+ ## Environment
25
+
26
+ Default configuration is in `.env`. Put custom config and secrets in `.env.local`, it will override the values in `.env`.
27
+
28
+ Check out [.env](./.env) to see what needs to be set.
29
+
30
+ ## Building
31
+
32
+ To create a production version of your app:
33
+
34
+ ```bash
35
+ npm run build
36
+ ```
37
+
38
+ You can preview the production build with `npm run preview`.
39
+
40
+ > 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,49 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "name": "chat-ui",
3
+ "version": "0.0.1",
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
+ "svelte": "^3.54.0",
28
+ "svelte-check": "^3.0.1",
29
+ "tslib": "^2.4.1",
30
+ "typescript": "^4.9.3",
31
+ "unplugin-icons": "^0.16.1",
32
+ "vite": "^4.0.0"
33
+ },
34
+ "type": "module",
35
+ "dependencies": {
36
+ "@huggingface/inference": "^2.1.2",
37
+ "autoprefixer": "^10.4.14",
38
+ "date-fns": "^2.29.3",
39
+ "dotenv": "^16.0.3",
40
+ "highlight.js": "^11.7.0",
41
+ "marked": "^4.3.0",
42
+ "mongodb": "^5.3.0",
43
+ "nanoid": "^4.0.2",
44
+ "postcss": "^8.4.21",
45
+ "tailwind-scrollbar": "^3.0.0",
46
+ "tailwindcss": "^3.3.1",
47
+ "zod": "^3.21.4"
48
+ }
49
+ }
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,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
+ </script>
16
+ %sveltekit.head%
17
+ </head>
18
+ <body data-sveltekit-preload-data="hover" class="dark:bg-gray-900 h-full">
19
+ <div class="contents h-full">%sveltekit.body%</div>
20
+ </body>
21
+ </html>
src/hooks.server.ts ADDED
@@ -0,0 +1,24 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { dev } from "$app/environment";
2
+ import { COOKIE_NAME } from "$env/static/private";
3
+ import type { Handle } from "@sveltejs/kit";
4
+ import { addYears } from "date-fns";
5
+
6
+ export const handle: Handle = async ({ event, resolve }) => {
7
+ const token = event.cookies.get(COOKIE_NAME);
8
+
9
+ event.locals.sessionId = token || crypto.randomUUID();
10
+
11
+ // Refresh cookie expiration date
12
+ event.cookies.set(COOKIE_NAME, event.locals.sessionId, {
13
+ path: "/",
14
+ // So that it works inside the space's iframe
15
+ sameSite: "none",
16
+ secure: !dev,
17
+ httpOnly: true,
18
+ expires: addYears(new Date(), 1),
19
+ });
20
+
21
+ const response = await resolve(event);
22
+
23
+ return response;
24
+ };
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,28 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import {
2
+ PUBLIC_ASSISTANT_MESSAGE_TOKEN,
3
+ PUBLIC_MAX_INPUT_TOKENS,
4
+ PUBLIC_SEP_TOKEN,
5
+ PUBLIC_USER_MESSAGE_TOKEN,
6
+ } from "$env/static/public";
7
+ import type { Message } from "./types/Message";
8
+
9
+ /**
10
+ * Convert [{user: "assistant", content: "hi"}, {user: "user", content: "hello"}] to:
11
+ *
12
+ * <|assistant|>hi<|endoftext|><|prompter|>hello<|endoftext|><|assistant|>
13
+ */
14
+ export function buildPrompt(messages: Message[]): string {
15
+ const prompt =
16
+ messages
17
+ .map(
18
+ (m) =>
19
+ (m.from === "user"
20
+ ? PUBLIC_USER_MESSAGE_TOKEN + m.content
21
+ : PUBLIC_ASSISTANT_MESSAGE_TOKEN + m.content) +
22
+ (m.content.endsWith(PUBLIC_SEP_TOKEN) ? "" : PUBLIC_SEP_TOKEN)
23
+ )
24
+ .join("") + PUBLIC_ASSISTANT_MESSAGE_TOKEN;
25
+
26
+ // Not super precise, but it's truncated in the model's backend anyway
27
+ return prompt.split(" ").slice(-parseInt(PUBLIC_MAX_INPUT_TOKENS)).join(" ");
28
+ }
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 rounded-lg my-4">
19
+ <pre
20
+ class="overflow-auto scrollbar-custom scrollbar-thumb-gray-500 hover:scrollbar-thumb-gray-400 dark:scrollbar-thumb-white/10 dark:hover:scrollbar-thumb-white/20 px-5"><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 text-sm rounded-lg border py-2 px-2 shadow-sm border-gray-200 active:shadow-inner dark:border-gray-600 hover:border-gray-300 dark:hover:border-gray-400 transition-all {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/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;
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="md:hidden flex items-center h-12 border-b px-4 justify-between dark:border-gray-800 bg-gray-50 dark:bg-gray-800/70"
34
+ >
35
+ <button
36
+ type="button"
37
+ class="flex items-center justify-center w-9 h-9 -ml-3 shrink-0"
38
+ on:click={() => dispatch("toggle", true)}
39
+ aria-label="Open menu"
40
+ bind:this={openEl}><CarbonTextAlignJustify /></button
41
+ >
42
+ <span class="px-4 truncate">{title}</span>
43
+ <a href={base || "/"} class="flex items-center justify-center w-9 h-9 -mr-3 shrink-0"
44
+ ><CarbonAdd /></a
45
+ >
46
+ </nav>
47
+ <nav
48
+ class="fixed inset-0 z-50 grid grid-rows-[auto,auto,1fr,auto] grid-cols-1 max-h-screen bg-white dark:bg-gray-900 bg-gradient-to-l from-gray-50 dark:from-gray-800/30 {isOpen
49
+ ? 'block'
50
+ : 'hidden'}"
51
+ >
52
+ <div class="flex items-center px-4 h-12">
53
+ <button
54
+ type="button"
55
+ class="flex items-center justify-center ml-auto w-9 h-9 -mr-3"
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/NavMenu.svelte ADDED
@@ -0,0 +1,97 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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 CarbonExport from "~icons/carbon/export";
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
+ }>();
17
+
18
+ export let conversations: Array<{
19
+ id: string;
20
+ title: string;
21
+ }> = [];
22
+ </script>
23
+
24
+ <div class="flex-none max-sm:pt-0 sticky top-0 px-3 py-3.5 flex items-center justify-between">
25
+ <a class="rounded-xl font-semibold text-lg flex items-center" href="{PUBLIC_ORIGIN}{base}/">
26
+ <Logo classNames="mr-1 text-3xl" />
27
+ HuggingChat
28
+ </a>
29
+ <a
30
+ href={base || "/"}
31
+ class="flex border py-0.5 px-2 rounded-lg shadow-sm hover:shadow-none bg-white dark:bg-gray-700 dark:border-gray-600 text-center"
32
+ >
33
+ New Chat
34
+ </a>
35
+ </div>
36
+ <div
37
+ class="flex flex-col overflow-y-auto scrollbar-custom px-3 pb-3 pt-2 gap-1 bg-gradient-to-l from-gray-50 dark:from-gray-800/30 rounded-r-xl"
38
+ >
39
+ {#each conversations as conv}
40
+ <a
41
+ data-sveltekit-noscroll
42
+ href="{base}/conversation/{conv.id}"
43
+ class="group pl-3 pr-2 h-11 rounded-lg flex-none text-gray-500 dark:text-gray-400 hover:bg-gray-100 dark:hover:bg-gray-700 flex items-center gap-1.5 {conv.id ===
44
+ $page.params.id
45
+ ? 'bg-gray-100 dark:bg-gray-700'
46
+ : ''}"
47
+ >
48
+ <div class="flex-1 truncate">{conv.title}</div>
49
+
50
+ <button
51
+ type="button"
52
+ class="flex md:hidden md:group-hover:flex w-5 h-5 items-center justify-center rounded"
53
+ title="Share conversation"
54
+ on:click|preventDefault={() =>
55
+ dispatch("shareConversation", { id: conv.id, title: conv.title })}
56
+ >
57
+ <CarbonExport class="text-gray-400 hover:text-gray-500 dark:hover:text-gray-300 text-xs" />
58
+ </button>
59
+
60
+ <button
61
+ type="button"
62
+ class="flex md:hidden md:group-hover:flex w-5 h-5 items-center justify-center rounded"
63
+ title="Delete conversation"
64
+ on:click|preventDefault={() => dispatch("deleteConversation", conv.id)}
65
+ >
66
+ <CarbonTrashCan
67
+ class="text-gray-400 hover:text-gray-500 dark:hover:text-gray-300 text-xs"
68
+ />
69
+ </button>
70
+ </a>
71
+ {/each}
72
+ </div>
73
+ <div
74
+ class="flex flex-col p-3 gap-2 bg-gradient-to-l from-gray-50 dark:from-gray-800/30 rounded-r-xl mt-0.5 text-sm"
75
+ >
76
+ <button
77
+ on:click={switchTheme}
78
+ type="button"
79
+ class="group pl-3 pr-2 h-9 rounded-lg flex-none text-gray-500 dark:text-gray-400 dark:hover:bg-gray-700 flex items-center gap-1.5 hover:bg-gray-100"
80
+ >
81
+ Theme
82
+ </button>
83
+ <a
84
+ href="https://huggingface.co/spaces/huggingchat/chat-ui/discussions"
85
+ target="_blank"
86
+ rel="noreferrer"
87
+ class="group pl-3 pr-2 h-9 rounded-lg flex-none text-gray-500 dark:text-gray-400 dark:hover:bg-gray-700 flex items-center gap-1.5 hover:bg-gray-100"
88
+ >
89
+ Feedback
90
+ </a>
91
+ <a
92
+ href="{base}/privacy"
93
+ class="group pl-3 pr-2 h-9 rounded-lg flex-none text-gray-500 dark:text-gray-400 dark:hover:bg-gray-700 flex items-center gap-1.5 hover:bg-gray-100"
94
+ >
95
+ About & Privacy
96
+ </a>
97
+ </div>
src/lib/components/ScrollToBottomBtn.svelte ADDED
@@ -0,0 +1,34 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <script lang="ts">
2
+ import { fade } from "svelte/transition";
3
+ import IconChevron from "./icons/IconChevron.svelte";
4
+ import { onDestroy } from "svelte";
5
+
6
+ export let scrollNode: HTMLElement;
7
+ export { className as class };
8
+
9
+ let visible: boolean = false;
10
+ let className = "";
11
+
12
+ $: if (scrollNode) {
13
+ scrollNode.addEventListener("scroll", onScroll);
14
+ }
15
+
16
+ function onScroll() {
17
+ visible =
18
+ Math.ceil(scrollNode.scrollTop) + 200 < scrollNode.scrollHeight - scrollNode.clientHeight;
19
+ }
20
+
21
+ onDestroy(() => {
22
+ if (!scrollNode) return;
23
+ scrollNode.removeEventListener("scroll", onScroll);
24
+ });
25
+ </script>
26
+
27
+ {#if visible}
28
+ <button
29
+ transition:fade={{ duration: 150 }}
30
+ on:click={() => scrollNode.scrollTo({ top: scrollNode.scrollHeight, behavior: "smooth" })}
31
+ class="btn absolute flex rounded-full border w-[41px] h-[41px] shadow-md dark:shadow-gray-950 bg-white dark:bg-gray-700 hover:bg-gray-100 dark:hover:bg-gray-600 dark:border-gray-600 transition-all {className}"
32
+ ><IconChevron classNames="mt-[2px]" /></button
33
+ >
34
+ {/if}
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="absolute btn flex rounded-lg border py-1 px-3 shadow-sm bg-white dark:bg-gray-700 hover:bg-gray-100 dark:hover:bg-gray-600 dark:border-gray-600 transition-all
12
+ {className}
13
+ {visible ? 'opacity-100 visible' : 'opacity-0 invisible'}
14
+ "
15
+ >
16
+ <CarbonPause class="mr-1 -ml-1 w-[1.1875rem] h-[1.25rem] text-gray-400" /> Stop generating
17
+ </button>
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="fixed right-0 top-12 md:top-0 bg-gradient-to-bl from-red-500/20 via-red-500/0 to-red-500/0 pt-2 md:pt-5 pr-2 md:pr-8 pl-36 pb-36 z-20 pointer-events-none"
12
+ >
13
+ <div
14
+ class="flex items-center bg-white/90 dark:bg-gray-900/80 rounded-full py-1 px-3 shadow-sm pointer-events-auto"
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 py-1 px-2 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 flex-1 min-w-0">
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="absolute m-0 w-full h-full top-0 resize-none border-0 bg-transparent p-3 focus:ring-0 focus-visible:ring-0 dark:bg-transparent outline-none scrollbar-custom overflow-x-hidden overflow-y-scroll"
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,110 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <script lang="ts">
2
+ import {
3
+ PUBLIC_DISABLE_INTRO_TILES,
4
+ PUBLIC_MODEL_ID,
5
+ PUBLIC_MODEL_NAME,
6
+ } from "$env/static/public";
7
+
8
+ import Logo from "$lib/components/icons/Logo.svelte";
9
+ import CarbonArrowUpRight from "~icons/carbon/arrow-up-right";
10
+ import CarbonEarth from "~icons/carbon/earth";
11
+ import { createEventDispatcher } from "svelte";
12
+
13
+ const dispatch = createEventDispatcher<{ message: string }>();
14
+ </script>
15
+
16
+ <div class="grid lg:grid-cols-3 gap-8 my-auto">
17
+ <div class="lg:col-span-1">
18
+ <div>
19
+ <div class="text-2xl font-semibold mb-3 flex items-center">
20
+ <Logo classNames="mr-1 text-yellow-400 text-4xl" />
21
+ HuggingChat
22
+ <div
23
+ class="text-base h-6 px-2 rounded-lg text-gray-400 bg-gray-50 ml-3 flex items-center border border-gray-100 dark:bg-gray-800 dark:border-gray-700/60"
24
+ >
25
+ v0
26
+ </div>
27
+ </div>
28
+ <p class="text-base text-gray-600 dark:text-gray-400">
29
+ Making the best open source AI chat models available to everyone.
30
+ </p>
31
+ </div>
32
+ </div>
33
+ <div class="lg:col-span-2 lg:pl-24">
34
+ <div class="border dark:border-gray-800 rounded-xl overflow-hidden">
35
+ <div class="p-3">
36
+ <div class="text-sm text-gray-600 dark:text-gray-400">Current Model</div>
37
+ <div class="font-semibold">{PUBLIC_MODEL_NAME}</div>
38
+ </div>
39
+ <div
40
+ class="flex items-center gap-5 px-3 py-2 bg-gray-100 rounded-xl text-sm text-gray-600 dark:text-gray-300 dark:bg-gray-800"
41
+ >
42
+ <a
43
+ href="https://huggingface.co/{PUBLIC_MODEL_ID}"
44
+ target="_blank"
45
+ rel="noreferrer"
46
+ class="flex items-center hover:underline"
47
+ >
48
+ <CarbonArrowUpRight class="text-xs mr-1.5 text-gray-400" />
49
+ Model
50
+ <div class="max-sm:hidden">&nbsp;page</div>
51
+ </a>
52
+ <a
53
+ href="https://huggingface.co/datasets/OpenAssistant/oasst1"
54
+ target="_blank"
55
+ rel="noreferrer"
56
+ class="flex items-center hover:underline"
57
+ >
58
+ <CarbonArrowUpRight class="text-xs mr-1.5 text-gray-400" />
59
+ Dataset
60
+ <div class="max-sm:hidden">&nbsp;page</div>
61
+ </a>
62
+ <a
63
+ href="https://open-assistant.io/"
64
+ target="_blank"
65
+ class="flex items-center hover:underline ml-auto"
66
+ rel="noreferrer"
67
+ >
68
+ <CarbonEarth class="text-xs mr-1.5 text-gray-400" />
69
+ Open Assistant Website
70
+ </a>
71
+ </div>
72
+ </div>
73
+ </div>
74
+ {#if PUBLIC_DISABLE_INTRO_TILES !== "true"}
75
+ <div class="lg:col-span-3 lg:mt-12">
76
+ <p class="mb-3 text-gray-600 dark:text-gray-300">Examples</p>
77
+ <div class="grid lg:grid-cols-3 gap-3 lg:gap-5">
78
+ <button
79
+ type="button"
80
+ class="text-gray-600 dark:text-gray-300 p-2.5 sm:p-4 bg-gray-50 dark:bg-gray-800 dark:hover:bg-gray-700 hover:bg-gray-100 border dark:border-gray-800 rounded-xl"
81
+ on:click={() =>
82
+ dispatch(
83
+ "message",
84
+ "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)"
85
+ )}
86
+ >
87
+ "Write an email from bullet list"
88
+ </button>
89
+ <button
90
+ type="button"
91
+ class="text-gray-600 dark:text-gray-300 p-2.5 sm:p-4 bg-gray-50 dark:bg-gray-800 dark:hover:bg-gray-700 hover:bg-gray-100 border dark:border-gray-800 rounded-xl"
92
+ on:click={() =>
93
+ dispatch(
94
+ "message",
95
+ "Code a basic snake game in python, give explanations for each step."
96
+ )}
97
+ >
98
+ "Code a snake game"
99
+ </button>
100
+ <button
101
+ type="button"
102
+ class="text-gray-600 dark:text-gray-300 p-2.5 sm:p-4 bg-gray-50 dark:bg-gray-800 dark:hover:bg-gray-700 hover:bg-gray-100 border dark:border-gray-800 rounded-xl"
103
+ on:click={() => dispatch("message", "How do I make a delicious lemon cheesecake?")}
104
+ >
105
+ "Assist in a task"
106
+ </button>
107
+ </div>
108
+ </div>
109
+ {/if}
110
+ </div>
src/lib/components/chat/ChatMessage.svelte ADDED
@@ -0,0 +1,100 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <script lang="ts">
2
+ import { marked } from "marked";
3
+ import type { Message } from "$lib/types/Message";
4
+ import { afterUpdate } from "svelte";
5
+ import { deepestChild } from "$lib/utils/deepestChild";
6
+
7
+ import CodeBlock from "../CodeBlock.svelte";
8
+ import IconLoading from "../icons/IconLoading.svelte";
9
+
10
+ function sanitizeMd(md: string) {
11
+ return md
12
+ .replace(/<\|[a-z]*$/, "")
13
+ .replace(/<\|[a-z]+\|$/, "")
14
+ .replace(/<$/, "")
15
+ .replaceAll(/<\|[a-z]+\|>/g, " ")
16
+ .trim()
17
+ .replaceAll("&", "&amp;")
18
+ .replaceAll("<", "&lt;");
19
+ }
20
+ function unsanitizeMd(md: string) {
21
+ return md.replaceAll("&lt;", "<").replaceAll("&amp;", "&");
22
+ }
23
+
24
+ export let message: Message;
25
+ export let loading: boolean = false;
26
+
27
+ let contentEl: HTMLElement;
28
+ let loadingEl: any;
29
+ let pendingTimeout: NodeJS.Timeout;
30
+
31
+ const renderer = new marked.Renderer();
32
+
33
+ // For code blocks with simple backticks
34
+ renderer.codespan = (code) => {
35
+ // Unsanitize double-sanitized code
36
+ return `<code>${code.replaceAll("&amp;", "&")}</code>`;
37
+ };
38
+
39
+ const options: marked.MarkedOptions = {
40
+ ...marked.getDefaults(),
41
+ gfm: true,
42
+ renderer,
43
+ };
44
+
45
+ $: tokens = marked.lexer(sanitizeMd(message.content));
46
+
47
+ afterUpdate(() => {
48
+ loadingEl?.$destroy();
49
+ clearTimeout(pendingTimeout);
50
+
51
+ // Add loading animation to the last message if update takes more than 600ms
52
+ if (loading) {
53
+ pendingTimeout = setTimeout(() => {
54
+ if (contentEl) {
55
+ loadingEl = new IconLoading({
56
+ target: deepestChild(contentEl),
57
+ props: { classNames: "loading inline ml-2" },
58
+ });
59
+ }
60
+ }, 600);
61
+ }
62
+ });
63
+ </script>
64
+
65
+ {#if message.from === "assistant"}
66
+ <div class="flex items-start justify-start gap-4 leading-relaxed">
67
+ <img
68
+ alt=""
69
+ src="https://huggingface.co/avatars/2edb18bd0206c16b433841a47f53fa8e.svg"
70
+ class="mt-5 w-3 h-3 flex-none rounded-full shadow-lg"
71
+ />
72
+ <div
73
+ class="relative rounded-2xl prose-pre:my-2 px-5 py-3.5 border border-gray-100 bg-gradient-to-br from-gray-50 dark:from-gray-800/40 dark:border-gray-800 text-gray-600 dark:text-gray-300 min-h-[calc(2rem+theme(spacing[3.5])*2)] min-w-[100px]"
74
+ >
75
+ {#if !message.content}
76
+ <IconLoading classNames="absolute inset-0 m-auto" />
77
+ {/if}
78
+ <div
79
+ class="prose max-sm:prose-sm dark:prose-invert prose-pre:bg-gray-800 dark:prose-pre:bg-gray-900 prose-h1:text-lg prose-h2:text-base prose-h3:text-base prose-headings:font-semibold max-w-none"
80
+ bind:this={contentEl}
81
+ >
82
+ {#each tokens as token}
83
+ {#if token.type === "code"}
84
+ <CodeBlock lang={token.lang} code={unsanitizeMd(token.text)} />
85
+ {:else}
86
+ {@html marked.parser([token], options)}
87
+ {/if}
88
+ {/each}
89
+ </div>
90
+ </div>
91
+ </div>
92
+ {/if}
93
+ {#if message.from === "user"}
94
+ <div class="flex items-start justify-start gap-4 max-sm:text-sm">
95
+ <div class="mt-5 w-3 h-3 flex-none rounded-full" />
96
+ <div class="rounded-2xl px-5 py-3.5 text-gray-500 dark:text-gray-400 whitespace-break-spaces">
97
+ {message.content.trim()}
98
+ </div>
99
+ </div>
100
+ {/if}
src/lib/components/chat/ChatMessages.svelte ADDED
@@ -0,0 +1,47 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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 { tick } from "svelte";
6
+
7
+ import ChatIntroduction from "./ChatIntroduction.svelte";
8
+ import ChatMessage from "./ChatMessage.svelte";
9
+
10
+ export let messages: Message[];
11
+ export let loading: boolean;
12
+ export let pending: boolean;
13
+
14
+ let chatContainer: HTMLElement;
15
+
16
+ async function scrollToBottom() {
17
+ await tick();
18
+ chatContainer.scrollTop = chatContainer.scrollHeight;
19
+ }
20
+
21
+ // If last message is from user, scroll to bottom
22
+ $: if (messages.at(-1)?.from === "user") {
23
+ scrollToBottom();
24
+ }
25
+ </script>
26
+
27
+ <div
28
+ class="overflow-y-auto h-full scrollbar-custom mr-1"
29
+ use:snapScrollToBottom={messages.length ? messages : false}
30
+ bind:this={chatContainer}
31
+ >
32
+ <div class="max-w-3xl xl:max-w-4xl mx-auto px-5 pt-6 flex flex-col gap-5 sm:gap-8 h-full">
33
+ {#each messages as message, i}
34
+ <ChatMessage loading={loading && i === messages.length - 1} {message} />
35
+ {:else}
36
+ <ChatIntroduction on:message />
37
+ {/each}
38
+ {#if pending}
39
+ <ChatMessage message={{ from: "assistant", content: "" }} />
40
+ {/if}
41
+ <div class="h-32 flex-none" />
42
+ </div>
43
+ <ScrollToBottomBtn
44
+ class="max-md:hidden bottom-36 right-4 lg:right-10"
45
+ scrollNode={chatContainer}
46
+ />
47
+ </div>
src/lib/components/chat/ChatWindow.svelte ADDED
@@ -0,0 +1,82 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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<{ message: string; share: void; stop: void }>();
21
+
22
+ const handleSubmit = () => {
23
+ if (loading) return;
24
+ dispatch("message", message);
25
+ message = "";
26
+ };
27
+ </script>
28
+
29
+ <div class="relative min-h-0 min-w-0">
30
+ <ChatMessages {loading} {pending} {messages} on:message />
31
+ <div
32
+ class="flex flex-col pointer-events-none [&>*]:pointer-events-auto max-md:border-t dark:border-gray-800 items-center max-md:dark:bg-gray-900 max-md:bg-white bg-gradient-to-t from-white via-white/80 to-white/0 dark:from-gray-900 dark:via-gray-80 dark:to-gray-900/0 justify-center absolute inset-x-0 max-w-3xl xl:max-w-4xl mx-auto px-3.5 sm:px-5 bottom-0 py-4 md:py-8 w-full z-0"
33
+ >
34
+ <StopGeneratingBtn
35
+ visible={loading}
36
+ className="right-5 mr-[1px] md:mr-0 md:right-7 top-6 md:top-10 z-10"
37
+ on:click={() => dispatch("stop")}
38
+ />
39
+ <form
40
+ on:submit|preventDefault={handleSubmit}
41
+ class="w-full relative flex items-center rounded-xl flex-1 max-w-4xl border bg-gray-100 focus-within:border-gray-300 dark:bg-gray-700 dark:border-gray-600 dark:focus-within:border-gray-500 "
42
+ >
43
+ <div class="w-full flex flex-1 border-none bg-transparent">
44
+ <ChatInput
45
+ placeholder="Ask anything"
46
+ bind:value={message}
47
+ on:submit={handleSubmit}
48
+ autofocus
49
+ maxRows={10}
50
+ />
51
+ <button
52
+ class="btn p-1 px-[0.7rem] self-end bg-transparent my-1 h-[2.4rem] text-gray-400 rounded-lg enabled:dark:hover:text-gray-100 enabled:hover:text-gray-700 disabled:opacity-60 dark:disabled:opacity-40 mx-1"
53
+ disabled={!message || loading || disabled}
54
+ type="submit"
55
+ >
56
+ <CarbonSendAltFilled />
57
+ </button>
58
+ </div>
59
+ </form>
60
+ <div class="flex text-xs text-gray-400/90 mt-2 justify-between self-stretch px-1 max-sm:gap-2">
61
+ <p>
62
+ Model: <a
63
+ href="https://huggingface.co/{PUBLIC_MODEL_ID}"
64
+ target="_blank"
65
+ rel="noreferrer"
66
+ class="hover:underline">{PUBLIC_MODEL_NAME}</a
67
+ > <span class="max-sm:hidden">·</span><br class="sm:hidden" /> Generated content may be inaccurate
68
+ or false.
69
+ </p>
70
+ {#if messages.length}
71
+ <button
72
+ class="flex flex-none items-center hover:underline hover:text-gray-400 dark:max-sm:bg-gray-800 max-sm:bg-gray-50 max-sm:px-2.5 max-sm:rounded-lg"
73
+ type="button"
74
+ on:click={() => dispatch("share")}
75
+ >
76
+ <CarbonExport class="text-[.6rem] sm:mr-1.5 sm:text-yellow-500" />
77
+ <div class="max-sm:hidden">Share this conversation</div>
78
+ </button>
79
+ {/if}
80
+ </div>
81
+ </div>
82
+ </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,27 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
+
7
+ const client = new MongoClient(MONGODB_URL, {
8
+ // directConnection: true
9
+ });
10
+
11
+ export const connectPromise = client.connect().catch(console.error);
12
+
13
+ const db = client.db(MONGODB_DB_NAME);
14
+
15
+ const conversations = db.collection<Conversation>("conversations");
16
+ const sharedConversations = db.collection<SharedConversation>("sharedConversations");
17
+ const abortedGenerations = db.collection<AbortedGeneration>("abortedGenerations");
18
+
19
+ export { client, db };
20
+ export const collections = { conversations, sharedConversations, abortedGenerations };
21
+
22
+ client.on("open", () => {
23
+ conversations.createIndex({ sessionId: 1, updatedAt: -1 });
24
+ abortedGenerations.createIndex({ updatedAt: 1 }, { expireAfterSeconds: 30 });
25
+ abortedGenerations.createIndex({ conversationId: 1 }, { unique: true });
26
+ sharedConversations.createIndex({ hash: 1 }, { unique: true });
27
+ });
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,34 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { base } from "$app/paths";
2
+ import { ERROR_MESSAGES, error } from "$lib/stores/errors";
3
+
4
+ export async function shareConversation(id: string, title: string) {
5
+ try {
6
+ const res = await fetch(`${base}/conversation/${id}/share`, {
7
+ method: "POST",
8
+ headers: {
9
+ "Content-Type": "application/json",
10
+ },
11
+ });
12
+
13
+ if (!res.ok) {
14
+ error.set("Error while sharing conversation, try again.");
15
+ console.error("Error while sharing conversation: " + (await res.text()));
16
+ return;
17
+ }
18
+
19
+ const { url } = await res.json();
20
+
21
+ if (navigator.share) {
22
+ navigator.share({
23
+ title,
24
+ text: "Share this chat with others",
25
+ url,
26
+ });
27
+ } else {
28
+ prompt("Copy this public url to share:", url);
29
+ }
30
+ } catch (err) {
31
+ error.set(ERROR_MESSAGES.default);
32
+ console.error(err);
33
+ }
34
+ }
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/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,9 @@
 
 
 
 
 
 
 
 
 
 
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
+
5
+ export interface AbortedGeneration {
6
+ createdAt: Date;
7
+ updatedAt: Date;
8
+ conversationId: Conversation["_id"];
9
+ }
src/lib/types/Conversation.ts ADDED
@@ -0,0 +1,19 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import type { ObjectId } from "mongodb";
2
+ import type { Message } from "./Message";
3
+
4
+ export interface Conversation {
5
+ _id: ObjectId;
6
+
7
+ // Can be undefined for shared convo then deleted
8
+ sessionId: string;
9
+
10
+ title: string;
11
+ messages: Message[];
12
+
13
+ createdAt: Date;
14
+ updatedAt: Date;
15
+
16
+ meta?: {
17
+ fromShareId?: string;
18
+ };
19
+ }
src/lib/types/Message.ts ADDED
@@ -0,0 +1,4 @@
 
 
 
 
 
1
+ export interface Message {
2
+ from: "user" | "assistant";
3
+ content: string;
4
+ }
src/lib/types/SharedConversation.ts ADDED
@@ -0,0 +1,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import type { Message } from "./Message";
2
+
3
+ export interface SharedConversation {
4
+ _id: string;
5
+
6
+ hash: string;
7
+
8
+ title: string;
9
+ messages: Message[];
10
+
11
+ createdAt: Date;
12
+ updatedAt: Date;
13
+ }
src/lib/types/UrlDependency.ts ADDED
@@ -0,0 +1,4 @@
 
 
 
 
 
1
+ /* eslint-disable no-shadow */
2
+ export enum UrlDependency {
3
+ ConversationList = "conversation:list",
4
+ }
src/lib/utils/concatUint8Arrays.ts ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { sum } from "./sum";
2
+
3
+ export function concatUint8Arrays(arrays: Uint8Array[]): Uint8Array {
4
+ const totalLength = sum(arrays.map((a) => a.length));
5
+ const result = new Uint8Array(totalLength);
6
+ let offset = 0;
7
+ for (const array of arrays) {
8
+ result.set(array, offset);
9
+ offset += array.length;
10
+ }
11
+ return result;
12
+ }