Spaces:
Build error
Build error
Commit
·
6178b6e
0
Parent(s):
Duplicate from huggingchat/chat-ui
Browse filesCo-authored-by: Eliott Coyac <[email protected]>
This view is limited to 50 files because it contains too many changes.
See raw diff
- .env +19 -0
- .eslintignore +13 -0
- .eslintrc.cjs +23 -0
- .gitignore +10 -0
- .npmrc +1 -0
- .prettierignore +13 -0
- .prettierrc +8 -0
- .vscode/settings.json +7 -0
- Dockerfile +22 -0
- PRIVACY.md +33 -0
- README.md +40 -0
- package-lock.json +0 -0
- package.json +49 -0
- postcss.config.js +6 -0
- src/app.d.ts +17 -0
- src/app.html +21 -0
- src/hooks.server.ts +24 -0
- src/lib/actions/snapScrollToBottom.ts +54 -0
- src/lib/buildPrompt.ts +28 -0
- src/lib/components/CodeBlock.svelte +27 -0
- src/lib/components/CopyToClipBoardBtn.svelte +50 -0
- src/lib/components/MobileNav.svelte +62 -0
- src/lib/components/NavMenu.svelte +97 -0
- src/lib/components/ScrollToBottomBtn.svelte +34 -0
- src/lib/components/StopGeneratingBtn.svelte +17 -0
- src/lib/components/Toast.svelte +19 -0
- src/lib/components/Tooltip.svelte +22 -0
- src/lib/components/chat/ChatInput.svelte +54 -0
- src/lib/components/chat/ChatIntroduction.svelte +110 -0
- src/lib/components/chat/ChatMessage.svelte +100 -0
- src/lib/components/chat/ChatMessages.svelte +47 -0
- src/lib/components/chat/ChatWindow.svelte +82 -0
- src/lib/components/icons/IconChevron.svelte +20 -0
- src/lib/components/icons/IconCopy.svelte +26 -0
- src/lib/components/icons/IconDazzled.svelte +36 -0
- src/lib/components/icons/IconLoading.svelte +31 -0
- src/lib/components/icons/Logo.svelte +25 -0
- src/lib/server/abortedGenerations.ts +29 -0
- src/lib/server/database.ts +27 -0
- src/lib/server/modelEndpoint.ts +21 -0
- src/lib/shareConversation.ts +34 -0
- src/lib/stores/errors.ts +7 -0
- src/lib/stores/pendingMessage.ts +3 -0
- src/lib/switchTheme.ts +10 -0
- src/lib/types/AbortedGeneration.ts +9 -0
- src/lib/types/Conversation.ts +19 -0
- src/lib/types/Message.ts +4 -0
- src/lib/types/SharedConversation.ts +13 -0
- src/lib/types/UrlDependency.ts +4 -0
- 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("<", "<")}</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 + " \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"> 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"> 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("&", "&")
|
18 |
+
.replaceAll("<", "<");
|
19 |
+
}
|
20 |
+
function unsanitizeMd(md: string) {
|
21 |
+
return md.replaceAll("<", "<").replaceAll("&", "&");
|
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("&", "&")}</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 |
+
}
|