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