Upload folder using huggingface_hub
Browse filesThis view is limited to 50 files because it contains too many changes.
See raw diff
- .github/README.md +28 -35
- NeuroGPT/.babelrc +14 -0
- NeuroGPT/.env.template +39 -0
- NeuroGPT/.eslintignore +1 -0
- NeuroGPT/.eslintrc.json +4 -0
- NeuroGPT/.github/FUNDING.yml +13 -0
- NeuroGPT/.github/ISSUE_TEMPLATE/bug_report.md +38 -0
- NeuroGPT/.github/ISSUE_TEMPLATE/feature_request.md +20 -0
- NeuroGPT/.github/README.md +276 -0
- NeuroGPT/.github/README_RU.md +85 -0
- NeuroGPT/.github/workflows/build_chat.yml +104 -0
- NeuroGPT/.gitignore +46 -0
- NeuroGPT/.gitpod.yml +11 -0
- NeuroGPT/.lintstagedrc.json +6 -0
- NeuroGPT/.prettierrc.js +10 -0
- NeuroGPT/CODE_OF_CONDUCT.md +128 -0
- NeuroGPT/LICENSE +21 -0
- NeuroGPT/app/api/auth.ts +63 -0
- NeuroGPT/app/api/common.ts +102 -0
- NeuroGPT/app/api/config/route.ts +29 -0
- NeuroGPT/app/api/cors/[...path]/route.ts +70 -0
- NeuroGPT/app/api/model-config/route.ts +14 -0
- NeuroGPT/app/api/openai/[...path]/route.ts +104 -0
- NeuroGPT/app/azure.ts +9 -0
- NeuroGPT/app/client/api.ts +156 -0
- NeuroGPT/app/client/controller.ts +37 -0
- NeuroGPT/app/client/platforms/openai.ts +661 -0
- NeuroGPT/app/command.ts +90 -0
- NeuroGPT/app/components/auth.module.scss +36 -0
- NeuroGPT/app/components/auth.tsx +116 -0
- NeuroGPT/app/components/button.module.scss +83 -0
- NeuroGPT/app/components/button.tsx +51 -0
- NeuroGPT/app/components/changelog.module.scss +108 -0
- NeuroGPT/app/components/changelog.tsx +93 -0
- NeuroGPT/app/components/chat-list.tsx +167 -0
- NeuroGPT/app/components/chat.module.scss +518 -0
- NeuroGPT/app/components/chat.tsx +1453 -0
- NeuroGPT/app/components/emoji.tsx +79 -0
- NeuroGPT/app/components/error.tsx +72 -0
- NeuroGPT/app/components/exporter.module.scss +223 -0
- NeuroGPT/app/components/exporter.tsx +704 -0
- NeuroGPT/app/components/home.module.scss +353 -0
- NeuroGPT/app/components/home.tsx +210 -0
- NeuroGPT/app/components/input-range.module.scss +13 -0
- NeuroGPT/app/components/input-range.tsx +37 -0
- NeuroGPT/app/components/markdown.tsx +212 -0
- NeuroGPT/app/components/mask.module.scss +108 -0
- NeuroGPT/app/components/mask.tsx +618 -0
- NeuroGPT/app/components/message-selector.module.scss +76 -0
- NeuroGPT/app/components/message-selector.tsx +215 -0
.github/README.md
CHANGED
@@ -1,48 +1,45 @@
|
|
1 |
<div align="center">
|
2 |
<a href="https://t.me/neurogen_news">
|
3 |
-
<img src="https://readme-typing-svg.herokuapp.com?font=
|
4 |
</a>
|
5 |
|
6 |
-
<strong>
|
7 |
|
8 |
-
|
9 |
-
|
10 |
-
|
11 |
-
Благодарность авторам.
|
12 |
|
|
|
13 |
<a href="https://github.com/Em1tSan/NeuroGPT/blob/main/LICENSE">
|
14 |
-
<img src="https://img.shields.io/badge/license-GPL_3.0-
|
15 |
</a>
|
16 |
<a href="https://github.com/Em1tSan/NeuroGPT/commits/main">
|
17 |
-
<img src="https://img.shields.io/badge/latest-v1.4
|
18 |
-
</a>
|
19 |
-
|
20 |
-
<br> Перед использованием обязательно ознакомьтесь с <a href="https://github.com/Em1tSan/NeuroGPT/wiki#%D1%80%D1%83%D1%81%D1%81%D0%BA%D0%B8%D0%B9-%D1%8F%D0%B7%D1%8B%D0%BA">wiki проекта</a><br/>
|
21 |
-
<br> Инструкции по установке: <br/>
|
22 |
|
23 |
-
<a href="https://github.com/Em1tSan/NeuroGPT/wiki
|
24 |
<img src="https://img.shields.io/badge/-Windows-1371c3?logo=windows" alt="windows"/>
|
25 |
</a>
|
26 |
-
<a href="https://github.com/Em1tSan/NeuroGPT/wiki
|
27 |
<img src="https://img.shields.io/badge/-Linux-F1502F?logo=linux" alt="linux"/>
|
28 |
</a>
|
29 |
-
<a href="https://github.com/Em1tSan/NeuroGPT/wiki
|
30 |
<img src="https://img.shields.io/badge/-MacOS-C0BFC0?logo=apple" alt="macos"/>
|
31 |
-
</a>
|
32 |
-
<a href="https://github.com/Em1tSan/NeuroGPT/wiki
|
33 |
-
<img src="https://img.shields.io/badge
|
34 |
</a>
|
35 |
|
36 |
-
<br>
|
37 |
|
38 |
<a href="https://t.me/neurogen_news">
|
39 |
-
<img src="https://img.shields.io/badge/-Telegram
|
40 |
</a>
|
41 |
<a href="https://t.me/neurogen_chat">
|
42 |
-
<img src="https://img.shields.io/badge/-Telegram
|
43 |
</a>
|
44 |
|
45 |
-
<br>
|
46 |
|
47 |
<a href="https://boosty.to/neurogen">
|
48 |
<img src="https://upload.wikimedia.org/wikipedia/commons/thumb/9/92/Boosty_logo.svg/512px-Boosty_logo.svg.png?20230209172145" alt="neurogen_boosty" width="20%">
|
@@ -50,20 +47,16 @@
|
|
50 |
|
51 |
</div>
|
52 |
|
53 |
-
##
|
54 |
-
|
55 |
-
Поскольку данный проект функционирует не через официальное API, а благодаря доступу, полученному путем обратной инженерии, то API провайдеры могут падать, а различные модели отключаться. Пожалуйста, учтите это. Если вам необходима высокая стабильность для работы, то стоит обойти этот проект стороной. Также важно помнить, что поддержка осуществляется на чистом энтузиазме.
|
56 |
-
|
57 |
-
## Возможности:
|
58 |
|
59 |
-
-
|
60 |
-
- [
|
61 |
-
-
|
62 |
-
-
|
63 |
-
-
|
64 |
-
-
|
65 |
-
-
|
66 |
-
-
|
67 |
|
68 |
<div align="center">
|
69 |
<img src="https://github.com/NealBelov/screenshots/blob/main/img_03.png?raw=true" width="100%">
|
|
|
1 |
<div align="center">
|
2 |
<a href="https://t.me/neurogen_news">
|
3 |
+
<img src="https://readme-typing-svg.herokuapp.com?font=Jura&weight=700&size=30&duration=4000&pause=1000&color=1BED29¢er=true&width=435&lines=NeuroGPT+by+NeurogenAI" alt="NeuroGPT" />
|
4 |
</a>
|
5 |
|
6 |
+
<strong> <a href="https://github.com/Em1tSan/NeuroGPT/blob/main/.github/README_RU.md">Русский</a> | English </strong>
|
7 |
|
8 |
+
> ### The project is currently undergoing a transition to another client and reconstruction of the API. Technical work is continuing. We apologize for any inconvenience.
|
9 |
+
|
10 |
+
<p> Free API service providing access to GPT-3.5, GPT-4, and other language models. Before using it, please make sure you check out our <a href="https://github.com/Em1tSan/NeuroGPT/wiki#english-language">wiki</a>. The project utilizes a modified version of <a href="https://github.com/xtekky/gpt4free">gpt4free</a>, as well as <a href="https://github.com/GaiZhenbiao/ChuanhuChatGPT">ChuanhuChatGPT</a> as a web interface. We extend our gratitude to the authors.</p>
|
|
|
11 |
|
12 |
+
<p>
|
13 |
<a href="https://github.com/Em1tSan/NeuroGPT/blob/main/LICENSE">
|
14 |
+
<img src="https://img.shields.io/badge/license-GPL_3.0-darkgreen.svg" alt="license"/>
|
15 |
</a>
|
16 |
<a href="https://github.com/Em1tSan/NeuroGPT/commits/main">
|
17 |
+
<img src="https://img.shields.io/badge/latest-v1.5.4-darkgreen.svg" alt="latest"/>
|
18 |
+
</a></p>
|
|
|
|
|
|
|
19 |
|
20 |
+
<a href="https://github.com/Em1tSan/NeuroGPT/wiki/Installing-and-running#windows">
|
21 |
<img src="https://img.shields.io/badge/-Windows-1371c3?logo=windows" alt="windows"/>
|
22 |
</a>
|
23 |
+
<a href="https://github.com/Em1tSan/NeuroGPT/wiki/Installing-and-running#linux">
|
24 |
<img src="https://img.shields.io/badge/-Linux-F1502F?logo=linux" alt="linux"/>
|
25 |
</a>
|
26 |
+
<a href="https://github.com/Em1tSan/NeuroGPT/wiki/Installing-and-running#macos">
|
27 |
<img src="https://img.shields.io/badge/-MacOS-C0BFC0?logo=apple" alt="macos"/>
|
28 |
+
</a>
|
29 |
+
<a href="https://github.com/Em1tSan/NeuroGPT/wiki/Installing-and-running#portable-version">
|
30 |
+
<img src="https://img.shields.io/badge/-Portable version-8080ff?logo=portable" alt="portable"/>
|
31 |
</a>
|
32 |
|
33 |
+
<br> News and feedback: <br/>
|
34 |
|
35 |
<a href="https://t.me/neurogen_news">
|
36 |
+
<img src="https://img.shields.io/badge/-Telegram channel-0088CC?logo=telegram" alt="telegram"/>
|
37 |
</a>
|
38 |
<a href="https://t.me/neurogen_chat">
|
39 |
+
<img src="https://img.shields.io/badge/-Telegram chat-0088CC?logo=telegram" alt="telegram_chat"/>
|
40 |
</a>
|
41 |
|
42 |
+
<br> Support the project: <br/>
|
43 |
|
44 |
<a href="https://boosty.to/neurogen">
|
45 |
<img src="https://upload.wikimedia.org/wikipedia/commons/thumb/9/92/Boosty_logo.svg/512px-Boosty_logo.svg.png?20230209172145" alt="neurogen_boosty" width="20%">
|
|
|
47 |
|
48 |
</div>
|
49 |
|
50 |
+
## Features
|
|
|
|
|
|
|
|
|
51 |
|
52 |
+
- Web search
|
53 |
+
- [Model list](https://status.neuroapi.host/v1/status)
|
54 |
+
- Dialog context
|
55 |
+
- No-logs
|
56 |
+
- API endpoint
|
57 |
+
- Dialog history
|
58 |
+
- Setting generation parameters for GPT models
|
59 |
+
- Built-in prompt templates and jailbreaks for various tasks
|
60 |
|
61 |
<div align="center">
|
62 |
<img src="https://github.com/NealBelov/screenshots/blob/main/img_03.png?raw=true" width="100%">
|
NeuroGPT/.babelrc
ADDED
@@ -0,0 +1,14 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
{
|
2 |
+
"presets": [
|
3 |
+
[
|
4 |
+
"next/babel",
|
5 |
+
{
|
6 |
+
"preset-env": {
|
7 |
+
"targets": {
|
8 |
+
"browsers": ["> 0.25%, not dead"]
|
9 |
+
}
|
10 |
+
}
|
11 |
+
}
|
12 |
+
]
|
13 |
+
]
|
14 |
+
}
|
NeuroGPT/.env.template
ADDED
@@ -0,0 +1,39 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
|
2 |
+
# Your openai api key, separated by comma. (optional) (this for system which can be used access code)
|
3 |
+
# Default: Empty
|
4 |
+
OPENAI_API_KEY=
|
5 |
+
|
6 |
+
# Access passsword, separated by comma. (optional)
|
7 |
+
CODE=your-password
|
8 |
+
|
9 |
+
# You can start service behind a proxy
|
10 |
+
PROXY_URL=http://localhost:7890
|
11 |
+
|
12 |
+
# Override openai api request base url. (optional)
|
13 |
+
# Default: https://api.openai.com
|
14 |
+
# Examples: http://your-openai-proxy.com
|
15 |
+
BASE_URL=
|
16 |
+
|
17 |
+
# Specify OpenAI organization ID.(optional)
|
18 |
+
# Default: Empty
|
19 |
+
OPENAI_ORG_ID=
|
20 |
+
|
21 |
+
# (optional)
|
22 |
+
# Default: Empty
|
23 |
+
# If you do not want users to input their own API key, set this value to 1.
|
24 |
+
HIDE_USER_API_KEY=
|
25 |
+
|
26 |
+
# (optional)
|
27 |
+
# Default: Empty
|
28 |
+
# If you do want users to query balance, set this value to 1.
|
29 |
+
ENABLE_BALANCE_QUERY=
|
30 |
+
|
31 |
+
# (optional)
|
32 |
+
# Default: Empty
|
33 |
+
# If you want to disable parse settings from url, set this value to 1.
|
34 |
+
DISABLE_FAST_LINK=
|
35 |
+
|
36 |
+
# (optional)
|
37 |
+
# Default: Empty
|
38 |
+
# If you want enable vercel web analytics, set this value to 1.
|
39 |
+
VERCEL_ANALYTICS=
|
NeuroGPT/.eslintignore
ADDED
@@ -0,0 +1 @@
|
|
|
|
|
1 |
+
public/serviceWorker.js
|
NeuroGPT/.eslintrc.json
ADDED
@@ -0,0 +1,4 @@
|
|
|
|
|
|
|
|
|
|
|
1 |
+
{
|
2 |
+
"extends": "next/core-web-vitals",
|
3 |
+
"plugins": ["prettier"]
|
4 |
+
}
|
NeuroGPT/.github/FUNDING.yml
ADDED
@@ -0,0 +1,13 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# These are supported funding model platforms
|
2 |
+
|
3 |
+
github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]
|
4 |
+
patreon: # Replace with a single Patreon username
|
5 |
+
open_collective: # Replace with a single Open Collective username
|
6 |
+
ko_fi: # Replace with a single Ko-fi username
|
7 |
+
tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
|
8 |
+
community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
|
9 |
+
liberapay: # Replace with a single Liberapay username
|
10 |
+
issuehunt: # Replace with a single IssueHunt username
|
11 |
+
otechie: # Replace with a single Otechie username
|
12 |
+
lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry
|
13 |
+
custom: # ['https://boosty.to/neurogen']
|
NeuroGPT/.github/ISSUE_TEMPLATE/bug_report.md
ADDED
@@ -0,0 +1,38 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
---
|
2 |
+
name: Bug report
|
3 |
+
about: Create a report to help us improve
|
4 |
+
title: ''
|
5 |
+
labels: ''
|
6 |
+
assignees: ''
|
7 |
+
|
8 |
+
---
|
9 |
+
|
10 |
+
**Describe the bug**
|
11 |
+
A clear and concise description of what the bug is.
|
12 |
+
|
13 |
+
**To Reproduce**
|
14 |
+
Steps to reproduce the behavior:
|
15 |
+
1. Go to '...'
|
16 |
+
2. Click on '....'
|
17 |
+
3. Scroll down to '....'
|
18 |
+
4. See error
|
19 |
+
|
20 |
+
**Expected behavior**
|
21 |
+
A clear and concise description of what you expected to happen.
|
22 |
+
|
23 |
+
**Screenshots**
|
24 |
+
If applicable, add screenshots to help explain your problem.
|
25 |
+
|
26 |
+
**Desktop (please complete the following information):**
|
27 |
+
- OS: [e.g. iOS]
|
28 |
+
- Browser [e.g. chrome, safari]
|
29 |
+
- Version [e.g. 22]
|
30 |
+
|
31 |
+
**Smartphone (please complete the following information):**
|
32 |
+
- Device: [e.g. iPhone6]
|
33 |
+
- OS: [e.g. iOS8.1]
|
34 |
+
- Browser [e.g. stock browser, safari]
|
35 |
+
- Version [e.g. 22]
|
36 |
+
|
37 |
+
**Additional context**
|
38 |
+
Add any other context about the problem here.
|
NeuroGPT/.github/ISSUE_TEMPLATE/feature_request.md
ADDED
@@ -0,0 +1,20 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
---
|
2 |
+
name: Feature request
|
3 |
+
about: Suggest an idea for this project
|
4 |
+
title: ''
|
5 |
+
labels: ''
|
6 |
+
assignees: ''
|
7 |
+
|
8 |
+
---
|
9 |
+
|
10 |
+
**Is your feature request related to a problem? Please describe.**
|
11 |
+
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
|
12 |
+
|
13 |
+
**Describe the solution you'd like**
|
14 |
+
A clear and concise description of what you want to happen.
|
15 |
+
|
16 |
+
**Describe alternatives you've considered**
|
17 |
+
A clear and concise description of any alternative solutions or features you've considered.
|
18 |
+
|
19 |
+
**Additional context**
|
20 |
+
Add any other context or screenshots about the feature request here.
|
NeuroGPT/.github/README.md
ADDED
@@ -0,0 +1,276 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
<div align="center">
|
2 |
+
|
3 |
+
<a href="https://t.me/neurogen_news">
|
4 |
+
<img src="https://readme-typing-svg.herokuapp.com?font=Jura&weight=700&size=30&duration=4000&pause=1000&color=1BED29¢er=true&width=435&lines=NeuroAPI+by+Neurogen" alt="NeuroAPI" />
|
5 |
+
</a>
|
6 |
+
|
7 |
+
Your API access to ChatGPT.
|
8 |
+
|
9 |
+
</div>
|
10 |
+
|
11 |
+
<div align="center">
|
12 |
+
|
13 |
+
[Docs](https://neuroapi.gitbook.io/en/) · [Web Site](https://neuroapi.host/) · [Online Chat](https://chat.neuroapi.host) · [Change Log](https://github.com/Em1tSan/NeuroGPT/commits/main) ·
|
14 |
+
|
15 |
+
<a href="https://github.com/Em1tSan/NeuroGPT/releases/latest">
|
16 |
+
<img src="https://img.shields.io/badge/-Windows-1371c3?logo=windows" alt="windows"/>
|
17 |
+
</a>
|
18 |
+
<a href="https://github.com/Em1tSan/NeuroGPT/releases/latest">
|
19 |
+
<img src="https://img.shields.io/badge/-Linux-F1502F?logo=linux" alt="linux"/>
|
20 |
+
</a>
|
21 |
+
<a href="https://github.com/Em1tSan/NeuroGPT/releases/latest">
|
22 |
+
<img src="https://img.shields.io/badge/-MacOS-C0BFC0?logo=apple" alt="macos"/>
|
23 |
+
</a>
|
24 |
+
<br/>
|
25 |
+
|
26 |
+
<a href="https://t.me/neuro_api">
|
27 |
+
<img src="https://img.shields.io/badge/-Telegram channel-0088CC?logo=telegram" alt="telegram"/>
|
28 |
+
</a>
|
29 |
+
<a href="https://t.me/+IhL96RXP3D9iZTky">
|
30 |
+
<img src="https://img.shields.io/badge/-Telegram chat-0088CC?logo=telegram" alt="telegram_chat"/>
|
31 |
+
</a>
|
32 |
+
|
33 |
+
<br> Support the project: <br/>
|
34 |
+
<a href="https://boosty.to/neuroapi">
|
35 |
+
<img src="https://upload.wikimedia.org/wikipedia/commons/thumb/9/92/Boosty_logo.svg/512px-Boosty_logo.svg.png?20230209172145" alt="neurogen_boosty" width="20%">
|
36 |
+
</a>
|
37 |
+
|
38 |
+
</div>
|
39 |
+
|
40 |
+
|
41 |
+
# About [NeuroAPI](https://neuroapi.host)
|
42 |
+
Core of the project. Reverse API server compatible with OpenAI API format. Freemium. Multiple hundreds of providers.
|
43 |
+
|
44 |
+
# Docs
|
45 |
+
|
46 |
+
[NeuroAPI Docs (EN)](https://neuroapi.gitbook.io/en/)
|
47 |
+
|
48 |
+
[NeuroAPI Docs (RU)](https://neuroapi.gitbook.io/ru/)
|
49 |
+
|
50 |
+
**Endpoint:**
|
51 |
+
```
|
52 |
+
https://neuroapi.host
|
53 |
+
```
|
54 |
+
or
|
55 |
+
```
|
56 |
+
https://neuroapi.host/v1
|
57 |
+
```
|
58 |
+
## Limits:
|
59 |
+
|
60 |
+
Once you have registered your personal account, you will have $30 available for the first month. On the first day of the following month, your balance will be updated, and you will receive another $30. We have implemented these limits due to the increasing demand on our service and to prevent misuse.
|
61 |
+
We use the original pricing system for tokens, which is the same as the one used by OpenAI.
|
62 |
+
If the free limits are not sufficient for your needs, you can support us by subscribing to [Boosty](https://boosty.to/neuroapi). As a token of appreciation, you will receive extended limits based on your subscription level. Here are the available tiers:
|
63 |
+
|
64 |
+
• Tier 1: $10/month, providing $60 monthly.
|
65 |
+
|
66 |
+
• Tier 2: $20/month, providing $120 monthly.
|
67 |
+
|
68 |
+
• Tier 3: $100/month, providing $700 monthly. Subscribers at this tier also gain access to separate private providers and receive personalized support.
|
69 |
+
|
70 |
+
## Supported models
|
71 |
+
|
72 |
+
We currently support the following models:
|
73 |
+
<details>
|
74 |
+
<summary>GPT models:</summary>
|
75 |
+
|
76 |
+
• gpt-3.5-turbo
|
77 |
+
|
78 |
+
• gpt-3.5-turbo-0613
|
79 |
+
|
80 |
+
• gpt-3.5-turbo-1106
|
81 |
+
|
82 |
+
• gpt-3.5-turbo-16k
|
83 |
+
|
84 |
+
• gpt-3.5-turbo-16k-0613
|
85 |
+
|
86 |
+
• gpt-4
|
87 |
+
|
88 |
+
• gpt-4-0613
|
89 |
+
|
90 |
+
• gpt-4-1106-preview (Temporary offline)
|
91 |
+
|
92 |
+
• gpt-4-32k (for Boosty subscribers)
|
93 |
+
|
94 |
+
• gpt-4-32k-0613 (for Boosty subscribers)
|
95 |
+
</details>
|
96 |
+
|
97 |
+
|
98 |
+
|
99 |
+
<details>
|
100 |
+
<summary>Image Generation models:</summary>
|
101 |
+
|
102 |
+
|
103 |
+
• 3guofeng3_v3.4
|
104 |
+
|
105 |
+
• absolutereality_v1.6
|
106 |
+
|
107 |
+
• absolutereality_v1.8.1
|
108 |
+
|
109 |
+
• amIReal_v4.1
|
110 |
+
|
111 |
+
• analog_diffusion_v1
|
112 |
+
|
113 |
+
• anything_v3.0
|
114 |
+
|
115 |
+
• anything_v4.5
|
116 |
+
|
117 |
+
• anything_V5
|
118 |
+
|
119 |
+
• abyss_orangemix_v3
|
120 |
+
|
121 |
+
• blazing_drive_v10g
|
122 |
+
|
123 |
+
• cetusmix_v35
|
124 |
+
|
125 |
+
• childrensStories_v1_3D
|
126 |
+
|
127 |
+
• childrensStories_v1_SemiReal
|
128 |
+
|
129 |
+
• childrensStories_v1_ToonAnime
|
130 |
+
|
131 |
+
• Counterfeit_v3.0
|
132 |
+
|
133 |
+
• cuteyukimix_midchapter3
|
134 |
+
|
135 |
+
• cyberrealistic_v3.3
|
136 |
+
|
137 |
+
• dalcefo_v4
|
138 |
+
|
139 |
+
• deliberate_v2
|
140 |
+
|
141 |
+
• deliberate_v3
|
142 |
+
|
143 |
+
• dreamlike_anime_v1.0
|
144 |
+
|
145 |
+
• dreamlike_diffusion_v1.0
|
146 |
+
|
147 |
+
• dreamlike_photoreal_v2.0
|
148 |
+
|
149 |
+
• dreamshaper_v6
|
150 |
+
|
151 |
+
• dreamshaper_v7
|
152 |
+
|
153 |
+
• dreamshaper_v8
|
154 |
+
|
155 |
+
• edgeOfRealism_eor_v2.0
|
156 |
+
|
157 |
+
• EimisAnimeDiffusion_v1
|
158 |
+
|
159 |
+
• elldreths-vivid
|
160 |
+
|
161 |
+
• epicrealism_natural_Sin_RC1
|
162 |
+
|
163 |
+
• ICantBelieveItsNotPhotography_seco
|
164 |
+
|
165 |
+
• juggernaut_aftermath
|
166 |
+
|
167 |
+
• lofi_v4
|
168 |
+
|
169 |
+
• lyriel_v1.6
|
170 |
+
|
171 |
+
• majicmixRealistic_v4
|
172 |
+
|
173 |
+
• mechamix_v1.0
|
174 |
+
|
175 |
+
• meinamix_v9
|
176 |
+
|
177 |
+
• meinamix_v11
|
178 |
+
|
179 |
+
• neverendingDream_v1.22
|
180 |
+
|
181 |
+
• openjourney_v4
|
182 |
+
|
183 |
+
• pastelMixStylizedAnime_pruned
|
184 |
+
|
185 |
+
• portraitplus_v1.0
|
186 |
+
|
187 |
+
• protogen_x3.4
|
188 |
+
|
189 |
+
• Realistic_Vision_v1.4
|
190 |
+
|
191 |
+
• Realistic_Vision_v2.0
|
192 |
+
|
193 |
+
• Realistic_Vision_v4.0
|
194 |
+
|
195 |
+
• Realistic_Vision_v5.0
|
196 |
+
|
197 |
+
• redshift_diffusion_v1.0
|
198 |
+
|
199 |
+
• revAnimated_v1.2.2
|
200 |
+
|
201 |
+
• rundiffusionFX_v2.5D_v1.0
|
202 |
+
|
203 |
+
• rundiffusionFX_photorealistic_v1.0
|
204 |
+
|
205 |
+
• StableDiffusion_v1.4
|
206 |
+
|
207 |
+
• Stable_Diffusion_v1.5
|
208 |
+
|
209 |
+
• shoninsBeautiful_v1.0
|
210 |
+
|
211 |
+
• theallys_mix_v2
|
212 |
+
|
213 |
+
• timeless_v1.0
|
214 |
+
|
215 |
+
• toonyou_beta6
|
216 |
+
</details>
|
217 |
+
|
218 |
+
Image generation capability implemented with support from [VisionCraft](https://github.com/VisionCraft-org/VisionCraft)
|
219 |
+
|
220 |
+
## Follow us
|
221 |
+
|
222 |
+
[Our Discord server](https://discord.gg/9v9GgYsThY)
|
223 |
+
[Telegram](https://t.me/neuro_api)
|
224 |
+
|
225 |
+
## How generate API key
|
226 |
+
|
227 |
+
1. Visit the main page of our service: [neuroapi.host](https://neuroapi.host).
|
228 |
+
2. Choose **Login** from the menu, then click on **[Click to register](https://key.neuroapi.host/register)** to register your account.
|
229 |
+
3. After registration, in the top menu, select **Token** and then **Add New Token**.
|
230 |
+
4. Fill in the following details:
|
231 |
+
- **Name**: Enter the name for your API key.
|
232 |
+
- **Expiration time**: Click on the "Never Expires" button or choose a suitable option for you.
|
233 |
+
- **Quota**: Click on the "Set to unlimited quota" button.
|
234 |
+
5. Click **Submit** to create the token.
|
235 |
+
6. Once again, click on **Token** at the top, and next to your key, click the **Copy** button.
|
236 |
+
|
237 |
+
You have now successfully obtained your API key! Make sure to keep it secure and use it responsibly.
|
238 |
+
|
239 |
+
https://github.com/Em1tSan/NeuroGPT/assets/70670181/a75f3aae-972e-49f7-a264-02ae165e642a
|
240 |
+
|
241 |
+
# About NeuroGPT
|
242 |
+
PC app configured to use ChatGPT with [our API](https://github.com/Em1tSan/NeuroGPT#about-neuroapi). Based on <a href="https://github.com/Yidadaa/ChatGPT-Next-Web">ChatGPT Next Web</a>.
|
243 |
+
|
244 |
+
- Dialog context
|
245 |
+
- Dialog history
|
246 |
+
- Setting generation parameters for GPT models
|
247 |
+
- Built-in prompt templates and jailbreaks for various tasks
|
248 |
+
|
249 |
+
## Local launch
|
250 |
+
|
251 |
+
Before starting, you must create a new .env file at project root, and place your api key into it:
|
252 |
+
|
253 |
+
```
|
254 |
+
OPENAI_API_KEY=<your api key here>
|
255 |
+
|
256 |
+
# if you are not able to access openai service, use this BASE_URL
|
257 |
+
BASE_URL=https://neuroapi.host
|
258 |
+
```
|
259 |
+
|
260 |
+
```shell
|
261 |
+
# 1. install nodejs and yarn first
|
262 |
+
# 2. config local env vars in `.env.local`
|
263 |
+
# 3. run
|
264 |
+
yarn install
|
265 |
+
yarn dev
|
266 |
+
```
|
267 |
+
Then open http://127.0.0.1:3000 in your browser
|
268 |
+
|
269 |
+
***
|
270 |
+
<div align="center">
|
271 |
+
|
272 |
+
[![Star History Chart](https://api.star-history.com/svg?repos=neurogen-dev/NeuroAPI&type=Date)](https://star-history.com/#neurogen-dev/NeuroAPI&Date)
|
273 |
+
</div>
|
274 |
+
|
275 |
+
# LICENSE
|
276 |
+
[MIT](https://opensource.org/license/mit/)
|
NeuroGPT/.github/README_RU.md
ADDED
@@ -0,0 +1,85 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
<div align="center">
|
2 |
+
|
3 |
+
<a href="https://t.me/neurogen_news">
|
4 |
+
<img src="https://readme-typing-svg.herokuapp.com?font=Jura&weight=700&size=30&duration=4000&pause=1000&color=1BED29¢er=true&width=435&lines=NeuroGPT+by+NeurogenAI" alt="NeuroGPT" />
|
5 |
+
</a>
|
6 |
+
|
7 |
+
<strong> Русский | <a href="https://github.com/Em1tSan/NeuroGPT#readme">English </a></strong>
|
8 |
+
|
9 |
+
Ваш API-доступ к ChatGPT.
|
10 |
+
|
11 |
+
<pre>
|
12 |
+
Ведутся технические работы по улучшению стабильности
|
13 |
+
</pre>
|
14 |
+
</div>
|
15 |
+
|
16 |
+
<div align="center">
|
17 |
+
|
18 |
+
· [Wiki](https://github.com/Em1tSan/NeuroGPT/wiki#русский-язык) · [Web Site](https://chat.neuroapi.host/) · Docs · Q&A · [Change Log](https://github.com/Em1tSan/NeuroGPT/commits/main) ·
|
19 |
+
|
20 |
+
<br>
|
21 |
+
<a href="https://github.com/Em1tSan/NeuroGPT/wiki/Установка-ПК-клиента#windows">
|
22 |
+
<img src="https://img.shields.io/badge/-Windows-1371c3?logo=windows" alt="windows"/>
|
23 |
+
</a>
|
24 |
+
<a href="https://github.com/Em1tSan/NeuroGPT/wiki/Установка-ПК-клиента#linux">
|
25 |
+
<img src="https://img.shields.io/badge/-Linux-F1502F?logo=linux" alt="linux"/>
|
26 |
+
</a>
|
27 |
+
<a href="https://github.com/Em1tSan/NeuroGPT/wiki/Установка-ПК-клиента#macos">
|
28 |
+
<img src="https://img.shields.io/badge/-MacOS-C0BFC0?logo=apple" alt="macos"/>
|
29 |
+
</a>
|
30 |
+
<a href="https://github.com/Em1tSan/NeuroGPT/wiki/Установка-ПК-клиента#портативная-версия">
|
31 |
+
<img src="https://img.shields.io/badge/-Портативная версия-8080ff?logo=portable" alt="portable"/>
|
32 |
+
</a>
|
33 |
+
<br/>
|
34 |
+
|
35 |
+
<a href="https://t.me/neurogen_news">
|
36 |
+
<img src="https://img.shields.io/badge/-Telegram канал-0088CC?logo=telegram" alt="telegram"/>
|
37 |
+
</a>
|
38 |
+
<a href="https://t.me/neurogen_chat">
|
39 |
+
<img src="https://img.shields.io/badge/-Telegram чат-0088CC?logo=telegram" alt="telegram_chat"/>
|
40 |
+
</a>
|
41 |
+
|
42 |
+
<br> Поддержать проект: <br/>
|
43 |
+
<a href="https://boosty.to/neurogen">
|
44 |
+
<img src="https://upload.wikimedia.org/wikipedia/commons/thumb/9/92/Boosty_logo.svg/512px-Boosty_logo.svg.png?20230209172145" alt="neurogen_boosty" width="20%">
|
45 |
+
</a>
|
46 |
+
|
47 |
+
</div>
|
48 |
+
|
49 |
+
## О NeuroGPT
|
50 |
+
ПК клиент настроенный на использование ChatGPT с [нашим API](https://github.com/Em1tSan/NeuroGPT/blob/main/.github/README_RU.md#%D0%BE-neuroapi). Основано на <a href="https://github.com/GaiZhenbiao/ChuanhuChatGPT">ChuanhuChatGPT</a>.
|
51 |
+
|
52 |
+
- Веб-поиск
|
53 |
+
- Контекст диалога
|
54 |
+
- Отсутствие логов
|
55 |
+
- История диалога
|
56 |
+
- Изменение параметров генерации для GPT-моделей
|
57 |
+
- Встроенные джейлбрейки и шаблоны промптов под разные задачи
|
58 |
+
|
59 |
+
<img src="https://github.com/NealBelov/screenshots/blob/main/demo001.gif?raw=true" width="100%">
|
60 |
+
|
61 |
+
## О NeuroAPI
|
62 |
+
Ядро проекта. Reverse API сервер совместимый с форматом OpenAI API. Фримиум. Несколько сотен провайдеров. На основе модифицированной версии [gpt4free](https://github.com/xtekky/gpt4free).
|
63 |
+
|
64 |
+
### GPT-3.5 модели
|
65 |
+
|
66 |
+
15 запросов в минуту, 2000 в день.
|
67 |
+
|
68 |
+
**Endpoint:**
|
69 |
+
```
|
70 |
+
https://neuroapi.host
|
71 |
+
```
|
72 |
+
### GPT-4 модели
|
73 |
+
|
74 |
+
3 запроса в минуту, 200 в день.
|
75 |
+
|
76 |
+
**Endpoint:**
|
77 |
+
```
|
78 |
+
https://neuroapi.host/gpt4
|
79 |
+
```
|
80 |
+
|
81 |
+
***
|
82 |
+
<div align="center">
|
83 |
+
|
84 |
+
[![Star History Chart](https://api.star-history.com/svg?repos=Em1tSan/NeuroGPT&type=Date)](https://star-history.com/#Em1tSan/NeuroGPT&Date)
|
85 |
+
</div>
|
NeuroGPT/.github/workflows/build_chat.yml
ADDED
@@ -0,0 +1,104 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
name: Release App
|
2 |
+
|
3 |
+
on:
|
4 |
+
workflow_dispatch:
|
5 |
+
release:
|
6 |
+
types: [published]
|
7 |
+
|
8 |
+
jobs:
|
9 |
+
create-release:
|
10 |
+
permissions:
|
11 |
+
contents: write
|
12 |
+
runs-on: ubuntu-latest
|
13 |
+
outputs:
|
14 |
+
release_id: ${{ steps.create-release.outputs.result }}
|
15 |
+
|
16 |
+
steps:
|
17 |
+
- uses: actions/checkout@v3
|
18 |
+
- name: setup node
|
19 |
+
uses: actions/setup-node@v3
|
20 |
+
with:
|
21 |
+
node-version: 18
|
22 |
+
- name: get version
|
23 |
+
run: echo "PACKAGE_VERSION=$(node -p "require('./src-tauri/tauri.conf.json').package.version")" >> $GITHUB_ENV
|
24 |
+
- name: create release
|
25 |
+
id: create-release
|
26 |
+
uses: actions/github-script@v6
|
27 |
+
with:
|
28 |
+
script: |
|
29 |
+
const { data } = await github.rest.repos.getLatestRelease({
|
30 |
+
owner: context.repo.owner,
|
31 |
+
repo: context.repo.repo,
|
32 |
+
})
|
33 |
+
return data.id
|
34 |
+
build-tauri:
|
35 |
+
needs: create-release
|
36 |
+
permissions:
|
37 |
+
contents: write
|
38 |
+
strategy:
|
39 |
+
fail-fast: false
|
40 |
+
matrix:
|
41 |
+
config:
|
42 |
+
- os: ubuntu-latest
|
43 |
+
arch: x86_64
|
44 |
+
rust_target: x86_64-unknown-linux-gnu
|
45 |
+
- os: macos-latest
|
46 |
+
arch: x86_64
|
47 |
+
rust_target: x86_64-apple-darwin
|
48 |
+
- os: macos-latest
|
49 |
+
arch: aarch64
|
50 |
+
rust_target: aarch64-apple-darwin
|
51 |
+
- os: windows-latest
|
52 |
+
arch: x86_64
|
53 |
+
rust_target: x86_64-pc-windows-msvc
|
54 |
+
|
55 |
+
runs-on: ${{ matrix.config.os }}
|
56 |
+
steps:
|
57 |
+
- uses: actions/checkout@v3
|
58 |
+
- name: setup node
|
59 |
+
uses: actions/setup-node@v3
|
60 |
+
with:
|
61 |
+
node-version: 18
|
62 |
+
- name: install Rust stable
|
63 |
+
uses: dtolnay/rust-toolchain@stable
|
64 |
+
with:
|
65 |
+
targets: ${{ matrix.config.rust_target }}
|
66 |
+
- uses: Swatinem/rust-cache@v2
|
67 |
+
with:
|
68 |
+
key: ${{ matrix.config.rust_target }}
|
69 |
+
- name: install dependencies (ubuntu only)
|
70 |
+
if: matrix.config.os == 'ubuntu-latest'
|
71 |
+
run: |
|
72 |
+
sudo apt-get update
|
73 |
+
sudo apt-get install -y libgtk-3-dev libwebkit2gtk-4.0-dev libappindicator3-dev librsvg2-dev patchelf
|
74 |
+
- name: install frontend dependencies
|
75 |
+
run: yarn install # change this to npm or pnpm depending on which one you use
|
76 |
+
- uses: tauri-apps/tauri-action@v0
|
77 |
+
env:
|
78 |
+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
79 |
+
TAURI_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }}
|
80 |
+
TAURI_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }}
|
81 |
+
with:
|
82 |
+
releaseId: ${{ needs.create-release.outputs.release_id }}
|
83 |
+
|
84 |
+
publish-release:
|
85 |
+
permissions:
|
86 |
+
contents: write
|
87 |
+
runs-on: ubuntu-latest
|
88 |
+
needs: [create-release, build-tauri]
|
89 |
+
|
90 |
+
steps:
|
91 |
+
- name: publish release
|
92 |
+
id: publish-release
|
93 |
+
uses: actions/github-script@v6
|
94 |
+
env:
|
95 |
+
release_id: ${{ needs.create-release.outputs.release_id }}
|
96 |
+
with:
|
97 |
+
script: |
|
98 |
+
github.rest.repos.updateRelease({
|
99 |
+
owner: context.repo.owner,
|
100 |
+
repo: context.repo.repo,
|
101 |
+
release_id: process.env.release_id,
|
102 |
+
draft: false,
|
103 |
+
prerelease: false
|
104 |
+
})
|
NeuroGPT/.gitignore
ADDED
@@ -0,0 +1,46 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
2 |
+
|
3 |
+
# dependencies
|
4 |
+
/node_modules
|
5 |
+
/.pnp
|
6 |
+
.pnp.js
|
7 |
+
|
8 |
+
# testing
|
9 |
+
/coverage
|
10 |
+
|
11 |
+
# next.js
|
12 |
+
/.next/
|
13 |
+
/out/
|
14 |
+
|
15 |
+
# production
|
16 |
+
/build
|
17 |
+
|
18 |
+
# misc
|
19 |
+
.DS_Store
|
20 |
+
*.pem
|
21 |
+
|
22 |
+
# debug
|
23 |
+
npm-debug.log*
|
24 |
+
yarn-debug.log*
|
25 |
+
yarn-error.log*
|
26 |
+
.pnpm-debug.log*
|
27 |
+
|
28 |
+
# local env files
|
29 |
+
.env*.local
|
30 |
+
|
31 |
+
# vercel
|
32 |
+
.vercel
|
33 |
+
|
34 |
+
# typescript
|
35 |
+
*.tsbuildinfo
|
36 |
+
next-env.d.ts
|
37 |
+
dev
|
38 |
+
|
39 |
+
.vscode
|
40 |
+
.idea
|
41 |
+
|
42 |
+
# docker-compose env files
|
43 |
+
.env
|
44 |
+
|
45 |
+
*.key
|
46 |
+
*.key.pub
|
NeuroGPT/.gitpod.yml
ADDED
@@ -0,0 +1,11 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# This configuration file was automatically generated by Gitpod.
|
2 |
+
# Please adjust to your needs (see https://www.gitpod.io/docs/introduction/learn-gitpod/gitpod-yaml)
|
3 |
+
# and commit this file to your remote git repository to share the goodness with others.
|
4 |
+
|
5 |
+
# Learn more from ready-to-use templates: https://www.gitpod.io/docs/introduction/getting-started/quickstart
|
6 |
+
|
7 |
+
tasks:
|
8 |
+
- init: yarn install && yarn run dev
|
9 |
+
command: yarn run dev
|
10 |
+
|
11 |
+
|
NeuroGPT/.lintstagedrc.json
ADDED
@@ -0,0 +1,6 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
{
|
2 |
+
"./app/**/*.{js,ts,jsx,tsx,json,html,css,md}": [
|
3 |
+
"eslint --fix",
|
4 |
+
"prettier --write"
|
5 |
+
]
|
6 |
+
}
|
NeuroGPT/.prettierrc.js
ADDED
@@ -0,0 +1,10 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
module.exports = {
|
2 |
+
printWidth: 80,
|
3 |
+
tabWidth: 2,
|
4 |
+
useTabs: false,
|
5 |
+
semi: true,
|
6 |
+
singleQuote: false,
|
7 |
+
trailingComma: 'all',
|
8 |
+
bracketSpacing: true,
|
9 |
+
arrowParens: 'always',
|
10 |
+
};
|
NeuroGPT/CODE_OF_CONDUCT.md
ADDED
@@ -0,0 +1,128 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# Contributor Covenant Code of Conduct
|
2 |
+
|
3 |
+
## Our Pledge
|
4 |
+
|
5 |
+
We as members, contributors, and leaders pledge to make participation in our
|
6 |
+
community a harassment-free experience for everyone, regardless of age, body
|
7 |
+
size, visible or invisible disability, ethnicity, sex characteristics, gender
|
8 |
+
identity and expression, level of experience, education, socio-economic status,
|
9 |
+
nationality, personal appearance, race, religion, or sexual identity
|
10 |
+
and orientation.
|
11 |
+
|
12 |
+
We pledge to act and interact in ways that contribute to an open, welcoming,
|
13 |
+
diverse, inclusive, and healthy community.
|
14 |
+
|
15 |
+
## Our Standards
|
16 |
+
|
17 |
+
Examples of behavior that contributes to a positive environment for our
|
18 |
+
community include:
|
19 |
+
|
20 |
+
* Demonstrating empathy and kindness toward other people
|
21 |
+
* Being respectful of differing opinions, viewpoints, and experiences
|
22 |
+
* Giving and gracefully accepting constructive feedback
|
23 |
+
* Accepting responsibility and apologizing to those affected by our mistakes,
|
24 |
+
and learning from the experience
|
25 |
+
* Focusing on what is best not just for us as individuals, but for the
|
26 |
+
overall community
|
27 |
+
|
28 |
+
Examples of unacceptable behavior include:
|
29 |
+
|
30 |
+
* The use of sexualized language or imagery, and sexual attention or
|
31 |
+
advances of any kind
|
32 |
+
* Trolling, insulting or derogatory comments, and personal or political attacks
|
33 |
+
* Public or private harassment
|
34 |
+
* Publishing others' private information, such as a physical or email
|
35 |
+
address, without their explicit permission
|
36 |
+
* Other conduct which could reasonably be considered inappropriate in a
|
37 |
+
professional setting
|
38 |
+
|
39 |
+
## Enforcement Responsibilities
|
40 |
+
|
41 |
+
Community leaders are responsible for clarifying and enforcing our standards of
|
42 |
+
acceptable behavior and will take appropriate and fair corrective action in
|
43 |
+
response to any behavior that they deem inappropriate, threatening, offensive,
|
44 |
+
or harmful.
|
45 |
+
|
46 |
+
Community leaders have the right and responsibility to remove, edit, or reject
|
47 |
+
comments, commits, code, wiki edits, issues, and other contributions that are
|
48 |
+
not aligned to this Code of Conduct, and will communicate reasons for moderation
|
49 |
+
decisions when appropriate.
|
50 |
+
|
51 |
+
## Scope
|
52 |
+
|
53 |
+
This Code of Conduct applies within all community spaces, and also applies when
|
54 |
+
an individual is officially representing the community in public spaces.
|
55 |
+
Examples of representing our community include using an official e-mail address,
|
56 |
+
posting via an official social media account, or acting as an appointed
|
57 |
+
representative at an online or offline event.
|
58 |
+
|
59 |
+
## Enforcement
|
60 |
+
|
61 |
+
Instances of abusive, harassing, or otherwise unacceptable behavior may be
|
62 |
+
reported to the community leaders responsible for enforcement at
|
63 |
+
flynn.zhang@foxmail.com.
|
64 |
+
All complaints will be reviewed and investigated promptly and fairly.
|
65 |
+
|
66 |
+
All community leaders are obligated to respect the privacy and security of the
|
67 |
+
reporter of any incident.
|
68 |
+
|
69 |
+
## Enforcement Guidelines
|
70 |
+
|
71 |
+
Community leaders will follow these Community Impact Guidelines in determining
|
72 |
+
the consequences for any action they deem in violation of this Code of Conduct:
|
73 |
+
|
74 |
+
### 1. Correction
|
75 |
+
|
76 |
+
**Community Impact**: Use of inappropriate language or other behavior deemed
|
77 |
+
unprofessional or unwelcome in the community.
|
78 |
+
|
79 |
+
**Consequence**: A private, written warning from community leaders, providing
|
80 |
+
clarity around the nature of the violation and an explanation of why the
|
81 |
+
behavior was inappropriate. A public apology may be requested.
|
82 |
+
|
83 |
+
### 2. Warning
|
84 |
+
|
85 |
+
**Community Impact**: A violation through a single incident or series
|
86 |
+
of actions.
|
87 |
+
|
88 |
+
**Consequence**: A warning with consequences for continued behavior. No
|
89 |
+
interaction with the people involved, including unsolicited interaction with
|
90 |
+
those enforcing the Code of Conduct, for a specified period of time. This
|
91 |
+
includes avoiding interactions in community spaces as well as external channels
|
92 |
+
like social media. Violating these terms may lead to a temporary or
|
93 |
+
permanent ban.
|
94 |
+
|
95 |
+
### 3. Temporary Ban
|
96 |
+
|
97 |
+
**Community Impact**: A serious violation of community standards, including
|
98 |
+
sustained inappropriate behavior.
|
99 |
+
|
100 |
+
**Consequence**: A temporary ban from any sort of interaction or public
|
101 |
+
communication with the community for a specified period of time. No public or
|
102 |
+
private interaction with the people involved, including unsolicited interaction
|
103 |
+
with those enforcing the Code of Conduct, is allowed during this period.
|
104 |
+
Violating these terms may lead to a permanent ban.
|
105 |
+
|
106 |
+
### 4. Permanent Ban
|
107 |
+
|
108 |
+
**Community Impact**: Demonstrating a pattern of violation of community
|
109 |
+
standards, including sustained inappropriate behavior, harassment of an
|
110 |
+
individual, or aggression toward or disparagement of classes of individuals.
|
111 |
+
|
112 |
+
**Consequence**: A permanent ban from any sort of public interaction within
|
113 |
+
the community.
|
114 |
+
|
115 |
+
## Attribution
|
116 |
+
|
117 |
+
This Code of Conduct is adapted from the [Contributor Covenant][homepage],
|
118 |
+
version 2.0, available at
|
119 |
+
https://www.contributor-covenant.org/version/2/0/code_of_conduct.html.
|
120 |
+
|
121 |
+
Community Impact Guidelines were inspired by [Mozilla's code of conduct
|
122 |
+
enforcement ladder](https://github.com/mozilla/diversity).
|
123 |
+
|
124 |
+
[homepage]: https://www.contributor-covenant.org
|
125 |
+
|
126 |
+
For answers to common questions about this code of conduct, see the FAQ at
|
127 |
+
https://www.contributor-covenant.org/faq. Translations are available at
|
128 |
+
https://www.contributor-covenant.org/translations.
|
NeuroGPT/LICENSE
ADDED
@@ -0,0 +1,21 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
MIT License
|
2 |
+
|
3 |
+
Copyright (c) 2023 Zhang Yifei
|
4 |
+
|
5 |
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6 |
+
of this software and associated documentation files (the "Software"), to deal
|
7 |
+
in the Software without restriction, including without limitation the rights
|
8 |
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9 |
+
copies of the Software, and to permit persons to whom the Software is
|
10 |
+
furnished to do so, subject to the following conditions:
|
11 |
+
|
12 |
+
The above copyright notice and this permission notice shall be included in all
|
13 |
+
copies or substantial portions of the Software.
|
14 |
+
|
15 |
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16 |
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17 |
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18 |
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19 |
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20 |
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
21 |
+
SOFTWARE.
|
NeuroGPT/app/api/auth.ts
ADDED
@@ -0,0 +1,63 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import { NextRequest } from "next/server";
|
2 |
+
import { getServerSideConfig } from "../config/server";
|
3 |
+
import binary from "spark-md5";
|
4 |
+
import { ACCESS_CODE_PREFIX } from "../constant";
|
5 |
+
|
6 |
+
function getIP(req: NextRequest) {
|
7 |
+
let ip = req.ip ?? req.headers.get("x-real-ip");
|
8 |
+
const forwardedFor = req.headers.get("x-forwarded-for");
|
9 |
+
|
10 |
+
if (!ip && forwardedFor) {
|
11 |
+
ip = forwardedFor.split(",").at(0) ?? "";
|
12 |
+
}
|
13 |
+
|
14 |
+
return ip;
|
15 |
+
}
|
16 |
+
|
17 |
+
function parseApiKey(bearToken: string) {
|
18 |
+
const token = bearToken.trim().replaceAll("Bearer ", "").trim();
|
19 |
+
const isOpenAiKey = !token.startsWith(ACCESS_CODE_PREFIX);
|
20 |
+
|
21 |
+
return {
|
22 |
+
accessCode: isOpenAiKey ? "" : token.slice(ACCESS_CODE_PREFIX.length),
|
23 |
+
apiKey: isOpenAiKey ? token : "",
|
24 |
+
};
|
25 |
+
}
|
26 |
+
|
27 |
+
export function auth(req: NextRequest) {
|
28 |
+
const authToken = req.headers.get("Authorization") ?? "";
|
29 |
+
|
30 |
+
// check if it is openai api key or user token
|
31 |
+
const { accessCode, apiKey: token } = parseApiKey(authToken);
|
32 |
+
|
33 |
+
const hashedCode = binary.hash(accessCode ?? "").trim();
|
34 |
+
|
35 |
+
const serverConfig = getServerSideConfig();
|
36 |
+
console.log("[Auth] allowed hashed codes: ", [...serverConfig.codes]);
|
37 |
+
console.log("[Auth] got access code:", accessCode);
|
38 |
+
console.log("[Auth] hashed access code:", hashedCode);
|
39 |
+
console.log("[User IP] ", getIP(req));
|
40 |
+
console.log("[Time] ", new Date().toLocaleString());
|
41 |
+
|
42 |
+
if (serverConfig.needCode && !serverConfig.codes.has(hashedCode) && !token) {
|
43 |
+
return {
|
44 |
+
error: true,
|
45 |
+
msg: !accessCode ? "empty access code" : "wrong access code",
|
46 |
+
};
|
47 |
+
}
|
48 |
+
|
49 |
+
// Check if the access code has a corresponding API key
|
50 |
+
const apiKey = serverConfig.apiKeys.get(hashedCode);
|
51 |
+
if (apiKey) {
|
52 |
+
console.log("[Auth] use access code-specific API key");
|
53 |
+
req.headers.set("Authorization", `Bearer ${apiKey}`);
|
54 |
+
} else if (token) {
|
55 |
+
console.log("[Auth] use user API key");
|
56 |
+
} else {
|
57 |
+
console.log("[Auth] admin did not provide an API key");
|
58 |
+
}
|
59 |
+
|
60 |
+
return {
|
61 |
+
error: false,
|
62 |
+
};
|
63 |
+
}
|
NeuroGPT/app/api/common.ts
ADDED
@@ -0,0 +1,102 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import { NextRequest, NextResponse } from "next/server";
|
2 |
+
import { getServerSideConfig } from "../config/server";
|
3 |
+
import { DEFAULT_MODELS, OPENAI_BASE_URL } from "../constant";
|
4 |
+
import { collectModelTable, collectModels } from "../utils/model";
|
5 |
+
|
6 |
+
const serverConfig = getServerSideConfig();
|
7 |
+
|
8 |
+
export async function requestOpenai(req: NextRequest) {
|
9 |
+
const controller = new AbortController();
|
10 |
+
const authValue = req.headers.get("Authorization") ?? "";
|
11 |
+
const openaiPath = `${req.nextUrl.pathname}${req.nextUrl.search}`.replaceAll(
|
12 |
+
"/api/openai/",
|
13 |
+
"",
|
14 |
+
);
|
15 |
+
|
16 |
+
let baseUrl = serverConfig.baseUrl ?? OPENAI_BASE_URL;
|
17 |
+
|
18 |
+
if (!baseUrl.startsWith("http")) {
|
19 |
+
baseUrl = `https://${baseUrl}`;
|
20 |
+
}
|
21 |
+
|
22 |
+
if (baseUrl.endsWith("/")) {
|
23 |
+
baseUrl = baseUrl.slice(0, -1);
|
24 |
+
}
|
25 |
+
|
26 |
+
console.log("[Proxy] ", openaiPath);
|
27 |
+
console.log("[Base Url]", baseUrl);
|
28 |
+
console.log("[Org ID]", serverConfig.openaiOrgId);
|
29 |
+
|
30 |
+
const timeoutId = setTimeout(
|
31 |
+
() => {
|
32 |
+
controller.abort();
|
33 |
+
},
|
34 |
+
10 * 60 * 1000,
|
35 |
+
);
|
36 |
+
|
37 |
+
const fetchUrl = `${baseUrl}/${openaiPath}`;
|
38 |
+
const fetchOptions: RequestInit = {
|
39 |
+
headers: {
|
40 |
+
"Content-Type": "application/json",
|
41 |
+
"Cache-Control": "no-store",
|
42 |
+
Authorization: authValue,
|
43 |
+
...(process.env.OPENAI_ORG_ID && {
|
44 |
+
"OpenAI-Organization": process.env.OPENAI_ORG_ID,
|
45 |
+
}),
|
46 |
+
},
|
47 |
+
method: req.method,
|
48 |
+
body: req.body,
|
49 |
+
// to fix #2485: https://stackoverflow.com/questions/55920957/cloudflare-worker-typeerror-one-time-use-body
|
50 |
+
redirect: "manual",
|
51 |
+
// @ts-ignore
|
52 |
+
duplex: "half",
|
53 |
+
signal: controller.signal,
|
54 |
+
};
|
55 |
+
|
56 |
+
// #1815 try to refuse gpt4 request
|
57 |
+
if (serverConfig.customModels && req.body) {
|
58 |
+
try {
|
59 |
+
const modelTable = collectModelTable(
|
60 |
+
DEFAULT_MODELS,
|
61 |
+
serverConfig.customModels,
|
62 |
+
);
|
63 |
+
const clonedBody = await req.text();
|
64 |
+
fetchOptions.body = clonedBody;
|
65 |
+
|
66 |
+
const jsonBody = JSON.parse(clonedBody) as { model?: string };
|
67 |
+
|
68 |
+
// not undefined and is false
|
69 |
+
if (modelTable[jsonBody?.model ?? ""] === false) {
|
70 |
+
return NextResponse.json(
|
71 |
+
{
|
72 |
+
error: true,
|
73 |
+
message: `you are not allowed to use ${jsonBody?.model} model`,
|
74 |
+
},
|
75 |
+
{
|
76 |
+
status: 403,
|
77 |
+
},
|
78 |
+
);
|
79 |
+
}
|
80 |
+
} catch (e) {
|
81 |
+
console.error("[OpenAI] gpt4 filter", e);
|
82 |
+
}
|
83 |
+
}
|
84 |
+
|
85 |
+
try {
|
86 |
+
const res = await fetch(fetchUrl, fetchOptions);
|
87 |
+
|
88 |
+
// to prevent browser prompt for credentials
|
89 |
+
const newHeaders = new Headers(res.headers);
|
90 |
+
newHeaders.delete("www-authenticate");
|
91 |
+
// to disable nginx buffering
|
92 |
+
newHeaders.set("X-Accel-Buffering", "no");
|
93 |
+
|
94 |
+
return new Response(res.body, {
|
95 |
+
status: res.status,
|
96 |
+
statusText: res.statusText,
|
97 |
+
headers: newHeaders,
|
98 |
+
});
|
99 |
+
} finally {
|
100 |
+
clearTimeout(timeoutId);
|
101 |
+
}
|
102 |
+
}
|
NeuroGPT/app/api/config/route.ts
ADDED
@@ -0,0 +1,29 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import { NextResponse } from "next/server";
|
2 |
+
|
3 |
+
import { getServerSideConfig } from "../../config/server";
|
4 |
+
|
5 |
+
const serverConfig = getServerSideConfig();
|
6 |
+
|
7 |
+
// Danger! Do not hard code any secret value here!
|
8 |
+
// 警告!不要在这里写入任何敏感信息!
|
9 |
+
const DANGER_CONFIG = {
|
10 |
+
needCode: serverConfig.needCode,
|
11 |
+
hideUserApiKey: serverConfig.hideUserApiKey,
|
12 |
+
disableGPT4: serverConfig.disableGPT4,
|
13 |
+
hideBalanceQuery: serverConfig.hideBalanceQuery,
|
14 |
+
disableFastLink: serverConfig.disableFastLink,
|
15 |
+
customModels: serverConfig.customModels,
|
16 |
+
};
|
17 |
+
|
18 |
+
declare global {
|
19 |
+
type DangerConfig = typeof DANGER_CONFIG;
|
20 |
+
}
|
21 |
+
|
22 |
+
async function handle() {
|
23 |
+
return NextResponse.json(DANGER_CONFIG);
|
24 |
+
}
|
25 |
+
|
26 |
+
export const GET = handle;
|
27 |
+
export const POST = handle;
|
28 |
+
|
29 |
+
export const runtime = "edge";
|
NeuroGPT/app/api/cors/[...path]/route.ts
ADDED
@@ -0,0 +1,70 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import { NextRequest, NextResponse } from "next/server";
|
2 |
+
|
3 |
+
async function handle(
|
4 |
+
req: NextRequest,
|
5 |
+
{ params }: { params: { path: string[] } },
|
6 |
+
) {
|
7 |
+
if (req.method === "OPTIONS") {
|
8 |
+
return NextResponse.json({ body: "OK" }, { status: 200 });
|
9 |
+
}
|
10 |
+
|
11 |
+
const [protocol, ...subpath] = params.path;
|
12 |
+
const targetUrl = `${protocol}://${subpath.join("/")}`;
|
13 |
+
|
14 |
+
const method = req.headers.get("method") ?? undefined;
|
15 |
+
const shouldNotHaveBody = ["get", "head"].includes(
|
16 |
+
method?.toLowerCase() ?? "",
|
17 |
+
);
|
18 |
+
|
19 |
+
function isRealDevicez(userAgent: string | null): boolean {
|
20 |
+
// Author : @H0llyW00dzZ
|
21 |
+
// Note : This just an experiment for a prevent suspicious bot
|
22 |
+
// Modify this function to define your logic for determining if the user-agent belongs to a real device
|
23 |
+
// For example, you can check if the user-agent contains certain keywords or patterns that indicate a real device
|
24 |
+
if (userAgent) {
|
25 |
+
return userAgent.includes("AppleWebKit") && !userAgent.includes("Headless");
|
26 |
+
}
|
27 |
+
return false;
|
28 |
+
}
|
29 |
+
|
30 |
+
|
31 |
+
const userAgent = req.headers.get("User-Agent");
|
32 |
+
const isRealDevice = isRealDevicez(userAgent);
|
33 |
+
|
34 |
+
if (!isRealDevice) {
|
35 |
+
return NextResponse.json(
|
36 |
+
{
|
37 |
+
error: true,
|
38 |
+
msg: "Access Forbidden",
|
39 |
+
},
|
40 |
+
{
|
41 |
+
status: 403,
|
42 |
+
},
|
43 |
+
);
|
44 |
+
}
|
45 |
+
|
46 |
+
const fetchOptions: RequestInit = {
|
47 |
+
headers: {
|
48 |
+
authorization: req.headers.get("authorization") ?? "",
|
49 |
+
},
|
50 |
+
body: shouldNotHaveBody ? null : req.body,
|
51 |
+
method,
|
52 |
+
// @ts-ignore
|
53 |
+
duplex: "half",
|
54 |
+
};
|
55 |
+
|
56 |
+
const fetchResult = await fetch(targetUrl, fetchOptions);
|
57 |
+
|
58 |
+
console.log("[Cloud Sync]", targetUrl, {
|
59 |
+
status: fetchResult.status,
|
60 |
+
statusText: fetchResult.statusText,
|
61 |
+
});
|
62 |
+
|
63 |
+
return fetchResult;
|
64 |
+
}
|
65 |
+
|
66 |
+
export const POST = handle;
|
67 |
+
export const GET = handle;
|
68 |
+
export const OPTIONS = handle;
|
69 |
+
|
70 |
+
export const runtime = "edge";
|
NeuroGPT/app/api/model-config/route.ts
ADDED
@@ -0,0 +1,14 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import { NextResponse } from "next/server";
|
2 |
+
import { DEFAULT_MODELS } from "@/app/constant";
|
3 |
+
|
4 |
+
async function handle() {
|
5 |
+
const model_list = DEFAULT_MODELS.map((model) => {
|
6 |
+
return {
|
7 |
+
name: model.name,
|
8 |
+
available: model.available,
|
9 |
+
};
|
10 |
+
});
|
11 |
+
return NextResponse.json({ model_list });
|
12 |
+
}
|
13 |
+
|
14 |
+
export const GET = handle;
|
NeuroGPT/app/api/openai/[...path]/route.ts
ADDED
@@ -0,0 +1,104 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import { type OpenAIListModelResponse } from "@/app/client/platforms/openai";
|
2 |
+
import { getServerSideConfig } from "@/app/config/server";
|
3 |
+
import { OpenaiPath } from "@/app/constant";
|
4 |
+
import { prettyObject } from "@/app/utils/format";
|
5 |
+
import { NextRequest, NextResponse } from "next/server";
|
6 |
+
import { auth } from "../../auth";
|
7 |
+
import { requestOpenai } from "../../common";
|
8 |
+
|
9 |
+
const ALLOWED_PATH = new Set(Object.values(OpenaiPath));
|
10 |
+
|
11 |
+
function getModels(remoteModelRes: OpenAIListModelResponse) {
|
12 |
+
const config = getServerSideConfig();
|
13 |
+
|
14 |
+
if (config.disableGPT4) {
|
15 |
+
remoteModelRes.data = remoteModelRes.data.filter(
|
16 |
+
(m) => !m.id.startsWith("gpt-4"),
|
17 |
+
);
|
18 |
+
}
|
19 |
+
|
20 |
+
return remoteModelRes;
|
21 |
+
}
|
22 |
+
|
23 |
+
async function handle(
|
24 |
+
req: NextRequest,
|
25 |
+
{ params }: { params: { path: string[] } },
|
26 |
+
) {
|
27 |
+
console.log("[OpenAI Route] params ", params);
|
28 |
+
|
29 |
+
if (req.method === "OPTIONS") {
|
30 |
+
return NextResponse.json({ body: "OK" }, { status: 200 });
|
31 |
+
}
|
32 |
+
|
33 |
+
const subpath = params.path.join("/");
|
34 |
+
|
35 |
+
if (!ALLOWED_PATH.has(subpath)) {
|
36 |
+
console.log("[OpenAI Route] forbidden path ", subpath);
|
37 |
+
return NextResponse.json(
|
38 |
+
{
|
39 |
+
error: true,
|
40 |
+
msg: "you are not allowed to request " + subpath,
|
41 |
+
},
|
42 |
+
{
|
43 |
+
status: 403,
|
44 |
+
},
|
45 |
+
);
|
46 |
+
}
|
47 |
+
|
48 |
+
function isRealDevicez(userAgent: string | null): boolean {
|
49 |
+
// Author : @H0llyW00dzZ
|
50 |
+
// Note : This just an experiment for a prevent suspicious bot
|
51 |
+
// Modify this function to define your logic for determining if the user-agent belongs to a real device
|
52 |
+
// For example, you can check if the user-agent contains certain keywords or patterns that indicate a real device
|
53 |
+
if (userAgent) {
|
54 |
+
return userAgent.includes("AppleWebKit") && !userAgent.includes("Headless");
|
55 |
+
}
|
56 |
+
return false;
|
57 |
+
}
|
58 |
+
|
59 |
+
|
60 |
+
const userAgent = req.headers.get("User-Agent");
|
61 |
+
const isRealDevice = isRealDevicez(userAgent);
|
62 |
+
|
63 |
+
if (!isRealDevice) {
|
64 |
+
return NextResponse.json(
|
65 |
+
{
|
66 |
+
error: true,
|
67 |
+
msg: "Access Forbidden",
|
68 |
+
},
|
69 |
+
{
|
70 |
+
status: 403,
|
71 |
+
},
|
72 |
+
);
|
73 |
+
}
|
74 |
+
|
75 |
+
const authResult = auth(req);
|
76 |
+
if (authResult.error) {
|
77 |
+
return NextResponse.json(authResult, {
|
78 |
+
status: 401,
|
79 |
+
});
|
80 |
+
}
|
81 |
+
|
82 |
+
try {
|
83 |
+
const response = await requestOpenai(req);
|
84 |
+
|
85 |
+
// list models
|
86 |
+
if (subpath === OpenaiPath.ListModelPath && response.status === 200) {
|
87 |
+
const resJson = (await response.json()) as OpenAIListModelResponse;
|
88 |
+
const availableModels = getModels(resJson);
|
89 |
+
return NextResponse.json(availableModels, {
|
90 |
+
status: response.status,
|
91 |
+
});
|
92 |
+
}
|
93 |
+
|
94 |
+
return response;
|
95 |
+
} catch (e) {
|
96 |
+
console.error("[OpenAI] ", e);
|
97 |
+
return NextResponse.json(prettyObject(e));
|
98 |
+
}
|
99 |
+
}
|
100 |
+
|
101 |
+
export const GET = handle;
|
102 |
+
export const POST = handle;
|
103 |
+
|
104 |
+
export const runtime = "edge";
|
NeuroGPT/app/azure.ts
ADDED
@@ -0,0 +1,9 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
export function makeAzurePath(path: string, apiVersion: string) {
|
2 |
+
// should omit /v1 prefix
|
3 |
+
path = path.replaceAll("v1/", "");
|
4 |
+
|
5 |
+
// should add api-key to query string
|
6 |
+
path += `${path.includes("?") ? "&" : "?"}api-version=${apiVersion}`;
|
7 |
+
|
8 |
+
return path;
|
9 |
+
}
|
NeuroGPT/app/client/api.ts
ADDED
@@ -0,0 +1,156 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import { getClientConfig } from "../config/client";
|
2 |
+
import { ACCESS_CODE_PREFIX, Azure, ServiceProvider } from "../constant";
|
3 |
+
import { ChatMessage, ModelType, useAccessStore } from "../store";
|
4 |
+
import { ChatGPTApi } from "./platforms/openai";
|
5 |
+
|
6 |
+
export const ROLES = ["system", "user", "assistant"] as const;
|
7 |
+
export type MessageRole = (typeof ROLES)[number];
|
8 |
+
|
9 |
+
export const Models = ["gpt-3.5-turbo", "gpt-4"] as const;
|
10 |
+
export type ChatModel = ModelType;
|
11 |
+
|
12 |
+
export interface RequestMessage {
|
13 |
+
role: MessageRole;
|
14 |
+
content: string;
|
15 |
+
}
|
16 |
+
|
17 |
+
export interface LLMConfig {
|
18 |
+
model: string;
|
19 |
+
temperature?: number;
|
20 |
+
top_p?: number;
|
21 |
+
stream?: boolean;
|
22 |
+
presence_penalty?: number;
|
23 |
+
frequency_penalty?: number;
|
24 |
+
}
|
25 |
+
|
26 |
+
export interface ChatOptions {
|
27 |
+
messages: RequestMessage[];
|
28 |
+
config: LLMConfig;
|
29 |
+
whitelist: boolean;
|
30 |
+
|
31 |
+
onUpdate?: (message: string, chunk: string) => void;
|
32 |
+
onFinish: (message: string) => void;
|
33 |
+
onError?: (err: Error) => void;
|
34 |
+
onController?: (controller: AbortController) => void;
|
35 |
+
}
|
36 |
+
|
37 |
+
export interface LLMUsage {
|
38 |
+
used: number;
|
39 |
+
total: number;
|
40 |
+
}
|
41 |
+
|
42 |
+
export interface LLMModel {
|
43 |
+
name: string;
|
44 |
+
available: boolean;
|
45 |
+
}
|
46 |
+
|
47 |
+
export abstract class LLMApi {
|
48 |
+
abstract chat(options: ChatOptions): Promise<void>;
|
49 |
+
abstract usage(): Promise<LLMUsage>;
|
50 |
+
abstract models(): Promise<LLMModel[]>;
|
51 |
+
}
|
52 |
+
|
53 |
+
type ProviderName = "openai" | "azure" | "claude" | "palm";
|
54 |
+
|
55 |
+
interface Model {
|
56 |
+
name: string;
|
57 |
+
provider: ProviderName;
|
58 |
+
ctxlen: number;
|
59 |
+
}
|
60 |
+
|
61 |
+
interface ChatProvider {
|
62 |
+
name: ProviderName;
|
63 |
+
apiConfig: {
|
64 |
+
baseUrl: string;
|
65 |
+
apiKey: string;
|
66 |
+
summaryModel: Model;
|
67 |
+
};
|
68 |
+
models: Model[];
|
69 |
+
|
70 |
+
chat: () => void;
|
71 |
+
usage: () => void;
|
72 |
+
}
|
73 |
+
|
74 |
+
export class ClientApi {
|
75 |
+
public llm: LLMApi;
|
76 |
+
|
77 |
+
constructor() {
|
78 |
+
this.llm = new ChatGPTApi();
|
79 |
+
}
|
80 |
+
|
81 |
+
config() {}
|
82 |
+
|
83 |
+
prompts() {}
|
84 |
+
|
85 |
+
masks() {}
|
86 |
+
|
87 |
+
async share(messages: ChatMessage[], avatarUrl: string | null = null) {
|
88 |
+
const msgs = messages
|
89 |
+
.map((m) => ({
|
90 |
+
from: m.role === "user" ? "human" : "gpt",
|
91 |
+
value: m.content,
|
92 |
+
}))
|
93 |
+
.concat([
|
94 |
+
{
|
95 |
+
from: "human",
|
96 |
+
value:
|
97 |
+
"Share from [NeuroGPT Web]: https://github.com/Yidadaa/NeuroGPT",
|
98 |
+
},
|
99 |
+
]);
|
100 |
+
// 敬告二开开发者们,为了开源大模型的发展,请不要修改上述消息,此消息用于后续数据清洗使用
|
101 |
+
// Please do not modify this message
|
102 |
+
|
103 |
+
console.log("[Share]", messages, msgs);
|
104 |
+
const clientConfig = getClientConfig();
|
105 |
+
const proxyUrl = "/sharegpt";
|
106 |
+
const rawUrl = "https://sharegpt.com/api/conversations";
|
107 |
+
const shareUrl = clientConfig?.isApp ? rawUrl : proxyUrl;
|
108 |
+
const res = await fetch(shareUrl, {
|
109 |
+
body: JSON.stringify({
|
110 |
+
avatarUrl,
|
111 |
+
items: msgs,
|
112 |
+
}),
|
113 |
+
headers: {
|
114 |
+
"Content-Type": "application/json",
|
115 |
+
},
|
116 |
+
method: "POST",
|
117 |
+
});
|
118 |
+
|
119 |
+
const resJson = await res.json();
|
120 |
+
console.log("[Share]", resJson);
|
121 |
+
if (resJson.id) {
|
122 |
+
return `https://shareg.pt/${resJson.id}`;
|
123 |
+
}
|
124 |
+
}
|
125 |
+
}
|
126 |
+
|
127 |
+
export const api = new ClientApi();
|
128 |
+
|
129 |
+
export function getHeaders() {
|
130 |
+
const accessStore = useAccessStore.getState();
|
131 |
+
const headers: Record<string, string> = {
|
132 |
+
"Content-Type": "application/json",
|
133 |
+
"x-requested-with": "XMLHttpRequest",
|
134 |
+
};
|
135 |
+
|
136 |
+
const isAzure = accessStore.provider === ServiceProvider.Azure;
|
137 |
+
const authHeader = isAzure ? "api-key" : "Authorization";
|
138 |
+
const apiKey = isAzure ? accessStore.azureApiKey : accessStore.openaiApiKey;
|
139 |
+
|
140 |
+
const makeBearer = (s: string) => `${isAzure ? "" : "Bearer "}${s.trim()}`;
|
141 |
+
const validString = (x: string) => x && x.length > 0;
|
142 |
+
|
143 |
+
// use user's api key first
|
144 |
+
if (validString(apiKey)) {
|
145 |
+
headers[authHeader] = makeBearer(apiKey);
|
146 |
+
} else if (
|
147 |
+
accessStore.enabledAccessControl() &&
|
148 |
+
validString(accessStore.accessCode)
|
149 |
+
) {
|
150 |
+
headers[authHeader] = makeBearer(
|
151 |
+
ACCESS_CODE_PREFIX + accessStore.accessCode,
|
152 |
+
);
|
153 |
+
}
|
154 |
+
|
155 |
+
return headers;
|
156 |
+
}
|
NeuroGPT/app/client/controller.ts
ADDED
@@ -0,0 +1,37 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
// To store message streaming controller
|
2 |
+
export const ChatControllerPool = {
|
3 |
+
controllers: {} as Record<string, AbortController>,
|
4 |
+
|
5 |
+
addController(
|
6 |
+
sessionId: string,
|
7 |
+
messageId: string,
|
8 |
+
controller: AbortController,
|
9 |
+
) {
|
10 |
+
const key = this.key(sessionId, messageId);
|
11 |
+
this.controllers[key] = controller;
|
12 |
+
return key;
|
13 |
+
},
|
14 |
+
|
15 |
+
stop(sessionId: string, messageId: string) {
|
16 |
+
const key = this.key(sessionId, messageId);
|
17 |
+
const controller = this.controllers[key];
|
18 |
+
controller?.abort();
|
19 |
+
},
|
20 |
+
|
21 |
+
stopAll() {
|
22 |
+
Object.values(this.controllers).forEach((v) => v.abort());
|
23 |
+
},
|
24 |
+
|
25 |
+
hasPending() {
|
26 |
+
return Object.values(this.controllers).length > 0;
|
27 |
+
},
|
28 |
+
|
29 |
+
remove(sessionId: string, messageId: string) {
|
30 |
+
const key = this.key(sessionId, messageId);
|
31 |
+
delete this.controllers[key];
|
32 |
+
},
|
33 |
+
|
34 |
+
key(sessionId: string, messageIndex: string) {
|
35 |
+
return `${sessionId},${messageIndex}`;
|
36 |
+
},
|
37 |
+
};
|
NeuroGPT/app/client/platforms/openai.ts
ADDED
@@ -0,0 +1,661 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import {
|
2 |
+
DEFAULT_API_HOST,
|
3 |
+
DEFAULT_MODELS,
|
4 |
+
OpenaiPath,
|
5 |
+
REQUEST_TIMEOUT_MS,
|
6 |
+
} from "@/app/constant";
|
7 |
+
import { useAccessStore, useAppConfig, useChatStore } from "@/app/store";
|
8 |
+
|
9 |
+
import { ChatOptions, getHeaders, LLMApi, LLMModel, LLMUsage } from "../api";
|
10 |
+
import Locale from "../../locales";
|
11 |
+
import {
|
12 |
+
EventStreamContentType,
|
13 |
+
fetchEventSource,
|
14 |
+
} from "@fortaine/fetch-event-source";
|
15 |
+
import { prettyObject } from "@/app/utils/format";
|
16 |
+
import { getClientConfig } from "@/app/config/client";
|
17 |
+
|
18 |
+
/**
|
19 |
+
* Models Text-Moderations OpenAI
|
20 |
+
* Author: @H0llyW00dzZ
|
21 |
+
**/
|
22 |
+
interface ModerationResponse {
|
23 |
+
flagged: boolean;
|
24 |
+
categories: Record<string, boolean>;
|
25 |
+
}
|
26 |
+
|
27 |
+
export interface OpenAIListModelResponse {
|
28 |
+
object: string;
|
29 |
+
data: Array<{
|
30 |
+
id: string;
|
31 |
+
object: string;
|
32 |
+
root: string;
|
33 |
+
}>;
|
34 |
+
}
|
35 |
+
|
36 |
+
export class ChatGPTApi implements LLMApi {
|
37 |
+
private disableListModels = true;
|
38 |
+
|
39 |
+
path(path: string): string {
|
40 |
+
let openaiUrl = useAccessStore.getState().openaiUrl;
|
41 |
+
const apiPath = "/api/openai";
|
42 |
+
|
43 |
+
if (openaiUrl.length === 0) {
|
44 |
+
const isApp = !!getClientConfig()?.isApp;
|
45 |
+
openaiUrl = isApp ? DEFAULT_API_HOST : apiPath;
|
46 |
+
}
|
47 |
+
if (openaiUrl.endsWith("/")) {
|
48 |
+
openaiUrl = openaiUrl.slice(0, openaiUrl.length - 1);
|
49 |
+
}
|
50 |
+
if (!openaiUrl.startsWith("http") && !openaiUrl.startsWith(apiPath)) {
|
51 |
+
openaiUrl = "https://" + openaiUrl;
|
52 |
+
}
|
53 |
+
return [openaiUrl, path].join("/");
|
54 |
+
}
|
55 |
+
|
56 |
+
extractMessage(res: any) {
|
57 |
+
return res.choices?.at(0)?.message?.content ?? "";
|
58 |
+
}
|
59 |
+
|
60 |
+
/** System Fingerprint & Max Tokens
|
61 |
+
* Author : @H0llyW00dzZ
|
62 |
+
* This method should be a member of the ChatGPTApi class, not nested inside another method
|
63 |
+
**/
|
64 |
+
private getNewStuff(
|
65 |
+
model: string,
|
66 |
+
max_tokens?: number,
|
67 |
+
system_fingerprint?: string
|
68 |
+
): { max_tokens?: number; system_fingerprint?: string; isNewModel: boolean } {
|
69 |
+
const modelConfig = {
|
70 |
+
...useAppConfig.getState().modelConfig,
|
71 |
+
...useChatStore.getState().currentSession().mask.modelConfig,
|
72 |
+
};
|
73 |
+
const isNewModel = model.endsWith("-preview");
|
74 |
+
if (isNewModel) {
|
75 |
+
return {
|
76 |
+
max_tokens: max_tokens !== undefined ? max_tokens : modelConfig.max_tokens,
|
77 |
+
system_fingerprint:
|
78 |
+
system_fingerprint !== undefined
|
79 |
+
? system_fingerprint
|
80 |
+
: modelConfig.system_fingerprint,
|
81 |
+
isNewModel: true,
|
82 |
+
};
|
83 |
+
} else {
|
84 |
+
return {
|
85 |
+
isNewModel: false,
|
86 |
+
};
|
87 |
+
}
|
88 |
+
}
|
89 |
+
|
90 |
+
async chat(options: ChatOptions) {
|
91 |
+
const textmoderation = useAppConfig.getState().textmoderation;
|
92 |
+
const latest = OpenaiPath.TextModerationModels.latest;
|
93 |
+
|
94 |
+
if (textmoderation && options.whitelist !== true) {
|
95 |
+
const messages = options.messages.map((v) => ({
|
96 |
+
role: v.role,
|
97 |
+
content: v.content,
|
98 |
+
}));
|
99 |
+
|
100 |
+
const userMessages = messages.filter((msg) => msg.role === "user");
|
101 |
+
const userMessage = userMessages[userMessages.length - 1]?.content;
|
102 |
+
|
103 |
+
if (userMessage) {
|
104 |
+
const moderationPath = this.path(OpenaiPath.ModerationPath);
|
105 |
+
const moderationPayload = {
|
106 |
+
input: userMessage,
|
107 |
+
model: latest,
|
108 |
+
};
|
109 |
+
|
110 |
+
try {
|
111 |
+
const moderationResponse = await this.sendModerationRequest(
|
112 |
+
moderationPath,
|
113 |
+
moderationPayload
|
114 |
+
);
|
115 |
+
|
116 |
+
if (moderationResponse.flagged) {
|
117 |
+
const flaggedCategories = Object.entries(
|
118 |
+
moderationResponse.categories
|
119 |
+
)
|
120 |
+
.filter(([category, flagged]) => flagged)
|
121 |
+
.map(([category]) => category);
|
122 |
+
|
123 |
+
if (flaggedCategories.length > 0) {
|
124 |
+
const translatedReasons = flaggedCategories.map((category) => {
|
125 |
+
const translation =
|
126 |
+
(Locale.Error.Content_Policy.Reason as any)[category];
|
127 |
+
return translation ? translation : category; // Use category name if translation is not available
|
128 |
+
});
|
129 |
+
const translatedReasonText = translatedReasons.join(", ");
|
130 |
+
const responseText = `${Locale.Error.Content_Policy.Title}\n${Locale.Error.Content_Policy.Reason.Title}: ${translatedReasonText}\n${Locale.Error.Content_Policy.SubTitle}\n`;
|
131 |
+
|
132 |
+
const responseWithGraph = responseText;
|
133 |
+
options.onFinish(responseWithGraph);
|
134 |
+
return;
|
135 |
+
}
|
136 |
+
}
|
137 |
+
} catch (e) {
|
138 |
+
console.log("[Request] failed to make a moderation request", e);
|
139 |
+
const error = {
|
140 |
+
error: (e as Error).message,
|
141 |
+
stack: (e as Error).stack,
|
142 |
+
};
|
143 |
+
options.onFinish(JSON.stringify(error));
|
144 |
+
return;
|
145 |
+
}
|
146 |
+
}
|
147 |
+
}
|
148 |
+
|
149 |
+
const messages = options.messages.map((v) => ({
|
150 |
+
role: v.role,
|
151 |
+
content: v.content,
|
152 |
+
}));
|
153 |
+
|
154 |
+
const modelConfig = {
|
155 |
+
...useAppConfig.getState().modelConfig,
|
156 |
+
...useChatStore.getState().currentSession().mask.modelConfig,
|
157 |
+
...{
|
158 |
+
model: options.config.model,
|
159 |
+
},
|
160 |
+
};
|
161 |
+
|
162 |
+
const defaultModel = modelConfig.model;
|
163 |
+
|
164 |
+
const userMessages = messages.filter((msg) => msg.role === "user");
|
165 |
+
const userMessage = userMessages[userMessages.length - 1]?.content;
|
166 |
+
/**
|
167 |
+
* DALL·E Models
|
168 |
+
* Author: @H0llyW00dzZ
|
169 |
+
* Usage in this chat: prompt
|
170 |
+
* Example: A Best Picture of Andromeda Galaxy
|
171 |
+
**/
|
172 |
+
const actualModel = this.getModelForInstructVersion(modelConfig.model);
|
173 |
+
const { max_tokens, system_fingerprint } = this.getNewStuff(
|
174 |
+
modelConfig.model,
|
175 |
+
modelConfig.max_tokens,
|
176 |
+
modelConfig.system_fingerprint
|
177 |
+
);
|
178 |
+
|
179 |
+
const requestPayloads = {
|
180 |
+
chat: {
|
181 |
+
messages,
|
182 |
+
stream: options.config.stream,
|
183 |
+
model: modelConfig.model,
|
184 |
+
temperature: modelConfig.temperature,
|
185 |
+
presence_penalty: modelConfig.presence_penalty,
|
186 |
+
frequency_penalty: modelConfig.frequency_penalty,
|
187 |
+
top_p: modelConfig.top_p,
|
188 |
+
// beta test for new model's since it consumed much tokens
|
189 |
+
// max is 4096
|
190 |
+
...{ max_tokens }, // Spread the max_tokens value
|
191 |
+
// not yet ready
|
192 |
+
//...{ system_fingerprint }, // Spread the system_fingerprint value
|
193 |
+
},
|
194 |
+
image: {
|
195 |
+
model: actualModel,
|
196 |
+
prompt: userMessage,
|
197 |
+
n: modelConfig.n,
|
198 |
+
quality: modelConfig.quality,
|
199 |
+
style: modelConfig.style,
|
200 |
+
size: modelConfig.size,
|
201 |
+
},
|
202 |
+
};
|
203 |
+
|
204 |
+
/** Magic TypeScript payload parameter 🎩 🪄
|
205 |
+
* Author : @H0llyW00dzZ
|
206 |
+
**/
|
207 |
+
const magicPayload = this.getNewStuff(defaultModel);
|
208 |
+
|
209 |
+
if (defaultModel.startsWith("dall-e") || defaultModel.startsWith("sd-")) {
|
210 |
+
console.log("[Request] openai payload: ", {
|
211 |
+
image: requestPayloads.image,
|
212 |
+
});
|
213 |
+
} else if (magicPayload.isNewModel) {
|
214 |
+
console.log("[Request] openai payload: ", {
|
215 |
+
chat: requestPayloads.chat,
|
216 |
+
});
|
217 |
+
} else {
|
218 |
+
const { max_tokens, ...oldChatPayload } = requestPayloads.chat;
|
219 |
+
console.log("[Request] openai payload: ", {
|
220 |
+
chat: oldChatPayload,
|
221 |
+
});
|
222 |
+
}
|
223 |
+
|
224 |
+
const shouldStream = !!options.config.stream;
|
225 |
+
const controller = new AbortController();
|
226 |
+
options.onController?.(controller);
|
227 |
+
|
228 |
+
try {
|
229 |
+
const dallemodels =
|
230 |
+
defaultModel.startsWith("dall-e") || defaultModel.startsWith("sd-");
|
231 |
+
|
232 |
+
let chatPath = dallemodels
|
233 |
+
? this.path(OpenaiPath.ImageCreationPath)
|
234 |
+
: this.path(OpenaiPath.ChatPath);
|
235 |
+
|
236 |
+
let requestPayload;
|
237 |
+
if (dallemodels) {
|
238 |
+
/**
|
239 |
+
* Author : @H0llyW00dzZ
|
240 |
+
* Use the image payload structure
|
241 |
+
*/
|
242 |
+
if (defaultModel.includes("dall-e-2")) {
|
243 |
+
/**
|
244 |
+
* Magic TypeScript payload parameter 🎩 🪄
|
245 |
+
**/
|
246 |
+
const { quality, style, ...imagePayload } = requestPayloads.image;
|
247 |
+
requestPayload = imagePayload;
|
248 |
+
} else {
|
249 |
+
requestPayload = requestPayloads.image;
|
250 |
+
}
|
251 |
+
} else {
|
252 |
+
/**
|
253 |
+
* Use the chat model payload structure
|
254 |
+
*/
|
255 |
+
requestPayload = requestPayloads.chat;
|
256 |
+
}
|
257 |
+
|
258 |
+
const chatPayload = {
|
259 |
+
method: "POST",
|
260 |
+
body: JSON.stringify(requestPayload),
|
261 |
+
signal: controller.signal,
|
262 |
+
headers: getHeaders(),
|
263 |
+
};
|
264 |
+
|
265 |
+
// make a fetch request
|
266 |
+
const requestTimeoutId = setTimeout(
|
267 |
+
() => controller.abort(),
|
268 |
+
REQUEST_TIMEOUT_MS,
|
269 |
+
);
|
270 |
+
|
271 |
+
if (shouldStream) {
|
272 |
+
let responseText = "";
|
273 |
+
let finished = false;
|
274 |
+
|
275 |
+
const finish = () => {
|
276 |
+
if (!finished) {
|
277 |
+
options.onFinish(responseText);
|
278 |
+
finished = true;
|
279 |
+
}
|
280 |
+
};
|
281 |
+
|
282 |
+
controller.signal.onabort = finish;
|
283 |
+
|
284 |
+
const isApp = !!getClientConfig()?.isApp;
|
285 |
+
const apiPath = "api/openai/";
|
286 |
+
|
287 |
+
fetchEventSource(chatPath, {
|
288 |
+
...chatPayload,
|
289 |
+
async onopen(res) {
|
290 |
+
clearTimeout(requestTimeoutId);
|
291 |
+
const contentType = res.headers.get("content-type");
|
292 |
+
console.log("[OpenAI] request response content type: ", contentType);
|
293 |
+
|
294 |
+
if (contentType?.startsWith("text/plain")) {
|
295 |
+
responseText = await res.clone().text();
|
296 |
+
} else if (contentType?.startsWith("application/json")) {
|
297 |
+
const jsonResponse = await res.clone().json();
|
298 |
+
const imageUrl = jsonResponse.data?.[0]?.url;
|
299 |
+
const prompt = requestPayloads.image.prompt;
|
300 |
+
const revised_prompt = jsonResponse.data?.[0]?.revised_prompt;
|
301 |
+
const index = requestPayloads.image.n - 1;
|
302 |
+
const size = requestPayloads.image.size;
|
303 |
+
const InstrucModel = defaultModel.endsWith("-vision");
|
304 |
+
|
305 |
+
if (defaultModel.includes("dall-e-3")) {
|
306 |
+
const imageDescription = `| ![${prompt}](${imageUrl}) |\n|---|\n| Size: ${size} |\n| [Download Here](${imageUrl}) |\n| 🎩 🪄 Revised Prompt (${index + 1}): ${revised_prompt} |\n| 🤖 AI Models: ${defaultModel} |`;
|
307 |
+
|
308 |
+
responseText = `${imageDescription}`;
|
309 |
+
} else {
|
310 |
+
const imageDescription = `#### ${prompt} (${index + 1})\n\n\n | ![${prompt}](${imageUrl}) |\n|---|\n| Size: ${size} |\n| [Download Here](${imageUrl}) |\n| 🤖 AI Models: ${defaultModel} |`;
|
311 |
+
|
312 |
+
responseText = `${imageDescription}`;
|
313 |
+
}
|
314 |
+
|
315 |
+
if (InstrucModel) {
|
316 |
+
const instructx = await fetch(
|
317 |
+
(isApp ? DEFAULT_API_HOST : apiPath) + OpenaiPath.ChatPath, // Pass the path parameter
|
318 |
+
{
|
319 |
+
method: "POST",
|
320 |
+
body: JSON.stringify({
|
321 |
+
messages: [
|
322 |
+
...messages,
|
323 |
+
],
|
324 |
+
model: "gpt-4-vision-preview",
|
325 |
+
temperature: modelConfig.temperature,
|
326 |
+
presence_penalty: modelConfig.presence_penalty,
|
327 |
+
frequency_penalty: modelConfig.frequency_penalty,
|
328 |
+
top_p: modelConfig.top_p,
|
329 |
+
// have to add this max_tokens for dall-e instruct
|
330 |
+
max_tokens: modelConfig.max_tokens,
|
331 |
+
}),
|
332 |
+
headers: getHeaders(),
|
333 |
+
}
|
334 |
+
);
|
335 |
+
clearTimeout(requestTimeoutId);
|
336 |
+
const instructxx = await instructx.json();
|
337 |
+
|
338 |
+
const instructionDelta = instructxx.choices?.[0]?.message?.content;
|
339 |
+
const instructionPayload = {
|
340 |
+
messages: [
|
341 |
+
...messages,
|
342 |
+
{
|
343 |
+
role: "system",
|
344 |
+
content: instructionDelta,
|
345 |
+
},
|
346 |
+
],
|
347 |
+
model: "gpt-4-vision-preview",
|
348 |
+
temperature: modelConfig.temperature,
|
349 |
+
presence_penalty: modelConfig.presence_penalty,
|
350 |
+
frequency_penalty: modelConfig.frequency_penalty,
|
351 |
+
top_p: modelConfig.top_p,
|
352 |
+
max_tokens: modelConfig.max_tokens,
|
353 |
+
};
|
354 |
+
|
355 |
+
const instructionResponse = await fetch(
|
356 |
+
(isApp ? DEFAULT_API_HOST : apiPath) + OpenaiPath.ChatPath,
|
357 |
+
{
|
358 |
+
method: "POST",
|
359 |
+
body: JSON.stringify(instructionPayload),
|
360 |
+
headers: getHeaders(),
|
361 |
+
}
|
362 |
+
);
|
363 |
+
|
364 |
+
const instructionJson = await instructionResponse.json();
|
365 |
+
const instructionMessage = instructionJson.choices?.[0]?.message?.content; // Access the appropriate property containing the message
|
366 |
+
const imageDescription = `| ![${prompt}](${imageUrl}) |\n|---|\n| Size: ${size} |\n| [Download Here](${imageUrl}) |\n| 🤖 AI Models: ${defaultModel} |`;
|
367 |
+
|
368 |
+
responseText = `${imageDescription}\n\n${instructionMessage}`;
|
369 |
+
}
|
370 |
+
|
371 |
+
if (
|
372 |
+
!res.ok ||
|
373 |
+
!res.headers
|
374 |
+
.get("content-type")
|
375 |
+
?.startsWith(EventStreamContentType) ||
|
376 |
+
res.status !== 200
|
377 |
+
) {
|
378 |
+
let anyinfo = await res.clone().text();
|
379 |
+
try {
|
380 |
+
const infJson = await res.clone().json();
|
381 |
+
anyinfo = prettyObject(infJson);
|
382 |
+
} catch { }
|
383 |
+
if (res.status === 401) {
|
384 |
+
responseText = "\n\n" + Locale.Error.Unauthorized;
|
385 |
+
}
|
386 |
+
if (res.status !== 200) {
|
387 |
+
if (anyinfo) {
|
388 |
+
responseText += "\n\n" + anyinfo;
|
389 |
+
}
|
390 |
+
}
|
391 |
+
return;
|
392 |
+
}
|
393 |
+
}
|
394 |
+
if (
|
395 |
+
!res.ok ||
|
396 |
+
!res.headers
|
397 |
+
.get("content-type")
|
398 |
+
?.startsWith(EventStreamContentType) ||
|
399 |
+
res.status !== 200
|
400 |
+
) {
|
401 |
+
const responseTexts = [responseText];
|
402 |
+
let extraInfo = await res.clone().text();
|
403 |
+
try {
|
404 |
+
const resJson = await res.clone().json();
|
405 |
+
extraInfo = prettyObject(resJson);
|
406 |
+
} catch {}
|
407 |
+
|
408 |
+
if (res.status === 401) {
|
409 |
+
responseTexts.push(Locale.Error.Unauthorized);
|
410 |
+
}
|
411 |
+
|
412 |
+
if (extraInfo) {
|
413 |
+
responseTexts.push(extraInfo);
|
414 |
+
}
|
415 |
+
|
416 |
+
responseText = responseTexts.join("\n\n");
|
417 |
+
|
418 |
+
return finish();
|
419 |
+
}
|
420 |
+
},
|
421 |
+
onmessage(msg) {
|
422 |
+
if (msg.data === "[DONE]" || finished) {
|
423 |
+
return finish();
|
424 |
+
}
|
425 |
+
const text = msg.data;
|
426 |
+
try {
|
427 |
+
const json = JSON.parse(text);
|
428 |
+
const delta = json.choices[0].delta.content;
|
429 |
+
if (delta) {
|
430 |
+
responseText += delta;
|
431 |
+
options.onUpdate?.(responseText, delta);
|
432 |
+
}
|
433 |
+
} catch (e) {
|
434 |
+
console.error("[Request] parse error", text, msg);
|
435 |
+
}
|
436 |
+
},
|
437 |
+
onclose() {
|
438 |
+
finish();
|
439 |
+
},
|
440 |
+
onerror(e) {
|
441 |
+
options.onError?.(e);
|
442 |
+
throw e;
|
443 |
+
},
|
444 |
+
openWhenHidden: true,
|
445 |
+
});
|
446 |
+
} else {
|
447 |
+
const res = await fetch(chatPath, chatPayload);
|
448 |
+
clearTimeout(requestTimeoutId);
|
449 |
+
|
450 |
+
const resJson = await res.json();
|
451 |
+
const message = this.extractMessage(resJson);
|
452 |
+
options.onFinish(message);
|
453 |
+
}
|
454 |
+
} catch (e) {
|
455 |
+
console.log("[Request] failed to make a chat request", e);
|
456 |
+
options.onError?.(e as Error);
|
457 |
+
}
|
458 |
+
}
|
459 |
+
|
460 |
+
async usage() {
|
461 |
+
const formatDate = (d: Date) =>
|
462 |
+
`${d.getFullYear()}-${(d.getMonth() + 1).toString().padStart(2, "0")}-${d
|
463 |
+
.getDate()
|
464 |
+
.toString()
|
465 |
+
.padStart(2, "0")}`;
|
466 |
+
const ONE_DAY = 1 * 24 * 60 * 60 * 1000;
|
467 |
+
const now = new Date();
|
468 |
+
const startOfMonth = new Date(now.getFullYear(), now.getMonth(), 1);
|
469 |
+
const startDate = formatDate(startOfMonth);
|
470 |
+
const endDate = formatDate(new Date(Date.now() + ONE_DAY));
|
471 |
+
|
472 |
+
const [used, subs] = await Promise.all([
|
473 |
+
fetch(
|
474 |
+
this.path(
|
475 |
+
`${OpenaiPath.UsagePath}?start_date=${startDate}&end_date=${endDate}`
|
476 |
+
),
|
477 |
+
{
|
478 |
+
method: "GET",
|
479 |
+
headers: getHeaders(),
|
480 |
+
}
|
481 |
+
),
|
482 |
+
fetch(this.path(OpenaiPath.SubsPath), {
|
483 |
+
method: "GET",
|
484 |
+
headers: getHeaders(),
|
485 |
+
}),
|
486 |
+
]);
|
487 |
+
|
488 |
+
if (used.status === 401) {
|
489 |
+
throw new Error(Locale.Error.Unauthorized);
|
490 |
+
}
|
491 |
+
|
492 |
+
if (!used.ok || !subs.ok) {
|
493 |
+
throw new Error("Failed to query usage from openai");
|
494 |
+
}
|
495 |
+
|
496 |
+
const response = (await used.json()) as {
|
497 |
+
total_usage?: number;
|
498 |
+
error?: {
|
499 |
+
type: string;
|
500 |
+
message: string;
|
501 |
+
};
|
502 |
+
};
|
503 |
+
|
504 |
+
const total = (await subs.json()) as {
|
505 |
+
hard_limit_usd?: number;
|
506 |
+
system_hard_limit_usd?: number;
|
507 |
+
};
|
508 |
+
|
509 |
+
if (response.error && response.error.type) {
|
510 |
+
throw Error(response.error.message);
|
511 |
+
}
|
512 |
+
|
513 |
+
if (response.total_usage) {
|
514 |
+
response.total_usage = Math.round(response.total_usage) / 100;
|
515 |
+
}
|
516 |
+
|
517 |
+
if (total.hard_limit_usd) {
|
518 |
+
total.hard_limit_usd = Math.round(total.hard_limit_usd * 100) / 100;
|
519 |
+
}
|
520 |
+
|
521 |
+
if (total.system_hard_limit_usd) {
|
522 |
+
total.system_hard_limit_usd =
|
523 |
+
Math.round(total.system_hard_limit_usd * 100) / 100;
|
524 |
+
}
|
525 |
+
|
526 |
+
return {
|
527 |
+
used: response.total_usage,
|
528 |
+
total: {
|
529 |
+
hard_limit_usd: total.hard_limit_usd,
|
530 |
+
system_hard_limit_usd: total.system_hard_limit_usd,
|
531 |
+
},
|
532 |
+
} as unknown as LLMUsage;
|
533 |
+
}
|
534 |
+
|
535 |
+
async models(): Promise<LLMModel[]> {
|
536 |
+
if (this.disableListModels) {
|
537 |
+
return DEFAULT_MODELS.slice();
|
538 |
+
}
|
539 |
+
|
540 |
+
const res = await fetch(this.path(OpenaiPath.ListModelPath), {
|
541 |
+
method: "GET",
|
542 |
+
headers: {
|
543 |
+
...getHeaders(),
|
544 |
+
},
|
545 |
+
});
|
546 |
+
|
547 |
+
const resJson = (await res.json()) as OpenAIListModelResponse;
|
548 |
+
const chatModels = resJson.data?.filter((m) => m.id.startsWith("gpt-"));
|
549 |
+
console.log("[Models]", chatModels);
|
550 |
+
|
551 |
+
if (!chatModels) {
|
552 |
+
return [];
|
553 |
+
}
|
554 |
+
|
555 |
+
return chatModels.map((m) => ({
|
556 |
+
name: m.id,
|
557 |
+
available: true,
|
558 |
+
}));
|
559 |
+
}
|
560 |
+
|
561 |
+
/**
|
562 |
+
* Models Text-Moderations OpenAI
|
563 |
+
* Author: @H0llyW00dzZ
|
564 |
+
**/
|
565 |
+
|
566 |
+
private async sendModerationRequest(
|
567 |
+
moderationPath: string,
|
568 |
+
moderationPayload: any
|
569 |
+
): Promise<ModerationResponse> {
|
570 |
+
try {
|
571 |
+
const moderationResponse = await fetch(moderationPath, {
|
572 |
+
method: "POST",
|
573 |
+
body: JSON.stringify(moderationPayload),
|
574 |
+
headers: getHeaders(),
|
575 |
+
});
|
576 |
+
|
577 |
+
const moderationJson = await moderationResponse.json();
|
578 |
+
|
579 |
+
if (moderationJson.results && moderationJson.results.length > 0) {
|
580 |
+
let moderationResult = moderationJson.results[0]; // Access the first element of the array
|
581 |
+
|
582 |
+
if (!moderationResult.flagged) {
|
583 |
+
const stable = OpenaiPath.TextModerationModels.stable; // Fall back to "stable" if "latest" is still false
|
584 |
+
moderationPayload.model = stable;
|
585 |
+
const fallbackModerationResponse = await fetch(moderationPath, {
|
586 |
+
method: "POST",
|
587 |
+
body: JSON.stringify(moderationPayload),
|
588 |
+
headers: getHeaders(),
|
589 |
+
});
|
590 |
+
|
591 |
+
const fallbackModerationJson =
|
592 |
+
await fallbackModerationResponse.json();
|
593 |
+
|
594 |
+
if (
|
595 |
+
fallbackModerationJson.results &&
|
596 |
+
fallbackModerationJson.results.length > 0
|
597 |
+
) {
|
598 |
+
moderationResult = fallbackModerationJson.results[0]; // Access the first element of the array
|
599 |
+
}
|
600 |
+
}
|
601 |
+
|
602 |
+
console.log("[Text Moderation] flagged:", moderationResult.flagged); // Log the flagged result
|
603 |
+
|
604 |
+
if (moderationResult.flagged) {
|
605 |
+
const flaggedCategories = Object.entries(moderationResult.categories)
|
606 |
+
.filter(([category, flagged]) => flagged)
|
607 |
+
.map(([category]) => category);
|
608 |
+
|
609 |
+
console.log("[Text Moderation] flagged categories:", flaggedCategories); // Log the flagged categories
|
610 |
+
}
|
611 |
+
|
612 |
+
return moderationResult as ModerationResponse;
|
613 |
+
} else {
|
614 |
+
console.error("Moderation response is empty");
|
615 |
+
throw new Error("Failed to get moderation response");
|
616 |
+
}
|
617 |
+
} catch (e) {
|
618 |
+
console.error("[Request] failed to make a moderation request", e);
|
619 |
+
return {} as ModerationResponse;
|
620 |
+
}
|
621 |
+
}
|
622 |
+
/**
|
623 |
+
* DALL·E Instruct
|
624 |
+
* Author : @H0llyW00dzZ
|
625 |
+
* Still WIP
|
626 |
+
*/
|
627 |
+
|
628 |
+
private getModelForInstructVersion(inputModel: string): string {
|
629 |
+
const modelMap: Record<string, string> = {
|
630 |
+
"dall-e-2-beta-instruct-vision": "dall-e-2",
|
631 |
+
"dall-e-3-beta-instruct-vision": "dall-e-3",
|
632 |
+
};
|
633 |
+
return modelMap[inputModel] || inputModel;
|
634 |
+
}
|
635 |
+
/**
|
636 |
+
* DALL·E Models
|
637 |
+
* Author : @H0llyW00dzZ
|
638 |
+
* Todo : Function to save an image from a response json object and make it accessible locally
|
639 |
+
*/
|
640 |
+
|
641 |
+
private async saveImageFromResponse(imageResponse: any, filename: string): Promise<void> {
|
642 |
+
try {
|
643 |
+
const blob = await imageResponse.blob();
|
644 |
+
|
645 |
+
const url = URL.createObjectURL(blob);
|
646 |
+
|
647 |
+
const link = document.createElement('a');
|
648 |
+
link.href = url;
|
649 |
+
link.download = filename;
|
650 |
+
link.click();
|
651 |
+
|
652 |
+
URL.revokeObjectURL(url);
|
653 |
+
|
654 |
+
console.log('Image saved successfully:', filename);
|
655 |
+
} catch (e) {
|
656 |
+
console.error('Failed to save image:', e);
|
657 |
+
}
|
658 |
+
}
|
659 |
+
}
|
660 |
+
|
661 |
+
export { OpenaiPath };
|
NeuroGPT/app/command.ts
ADDED
@@ -0,0 +1,90 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import { useEffect } from "react";
|
2 |
+
import { useSearchParams } from "react-router-dom";
|
3 |
+
import Locale from "./locales";
|
4 |
+
import { getClientConfig } from "@/app/config/client";
|
5 |
+
|
6 |
+
const isApp = !!getClientConfig()?.isApp;
|
7 |
+
|
8 |
+
type Command = (param: string) => void;
|
9 |
+
interface Commands {
|
10 |
+
fill?: Command;
|
11 |
+
submit?: Command;
|
12 |
+
mask?: Command;
|
13 |
+
code?: Command;
|
14 |
+
settings?: Command;
|
15 |
+
}
|
16 |
+
|
17 |
+
export function useCommand(commands: Commands = {}) {
|
18 |
+
const [searchParams, setSearchParams] = useSearchParams();
|
19 |
+
|
20 |
+
useEffect(() => {
|
21 |
+
let shouldUpdate = false;
|
22 |
+
searchParams.forEach((param, name) => {
|
23 |
+
const commandName = name as keyof Commands;
|
24 |
+
if (typeof commands[commandName] === "function") {
|
25 |
+
commands[commandName]!(param);
|
26 |
+
searchParams.delete(name);
|
27 |
+
shouldUpdate = true;
|
28 |
+
}
|
29 |
+
});
|
30 |
+
|
31 |
+
if (shouldUpdate) {
|
32 |
+
setSearchParams(searchParams);
|
33 |
+
}
|
34 |
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
35 |
+
}, [searchParams, commands]);
|
36 |
+
}
|
37 |
+
|
38 |
+
interface ChatCommands {
|
39 |
+
new?: Command;
|
40 |
+
newm?: Command;
|
41 |
+
next?: Command;
|
42 |
+
prev?: Command;
|
43 |
+
restart?: Command;
|
44 |
+
clear?: Command;
|
45 |
+
del?: Command;
|
46 |
+
save?: Command;
|
47 |
+
load?: Command;
|
48 |
+
copymemoryai?: Command;
|
49 |
+
updatemasks?: Command;
|
50 |
+
summarize?: Command;
|
51 |
+
}
|
52 |
+
|
53 |
+
export const ChatCommandPrefix = ":";
|
54 |
+
|
55 |
+
export function useChatCommand(commands: ChatCommands = {}) {
|
56 |
+
const chatCommands = { ...commands };
|
57 |
+
|
58 |
+
if (!isApp) {
|
59 |
+
delete chatCommands.restart;
|
60 |
+
}
|
61 |
+
|
62 |
+
function extract(userInput: string) {
|
63 |
+
return (
|
64 |
+
userInput.startsWith(ChatCommandPrefix) ? userInput.slice(1) : userInput
|
65 |
+
) as keyof ChatCommands;
|
66 |
+
}
|
67 |
+
|
68 |
+
function search(userInput: string) {
|
69 |
+
const input = extract(userInput);
|
70 |
+
const desc = Locale.Chat.Commands;
|
71 |
+
return Object.keys(chatCommands)
|
72 |
+
.filter((c) => c.startsWith(input))
|
73 |
+
.map((c) => ({
|
74 |
+
title: desc[c as keyof ChatCommands],
|
75 |
+
content: ChatCommandPrefix + c,
|
76 |
+
}));
|
77 |
+
}
|
78 |
+
|
79 |
+
function match(userInput: string) {
|
80 |
+
const command = extract(userInput);
|
81 |
+
const matched = typeof chatCommands[command] === "function";
|
82 |
+
|
83 |
+
return {
|
84 |
+
matched,
|
85 |
+
invoke: () => matched && chatCommands[command]!(userInput),
|
86 |
+
};
|
87 |
+
}
|
88 |
+
|
89 |
+
return { match, search };
|
90 |
+
}
|
NeuroGPT/app/components/auth.module.scss
ADDED
@@ -0,0 +1,36 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
.auth-page {
|
2 |
+
display: flex;
|
3 |
+
justify-content: center;
|
4 |
+
align-items: center;
|
5 |
+
height: 100%;
|
6 |
+
width: 100%;
|
7 |
+
flex-direction: column;
|
8 |
+
|
9 |
+
.auth-logo {
|
10 |
+
transform: scale(1.4);
|
11 |
+
}
|
12 |
+
|
13 |
+
.auth-title {
|
14 |
+
font-size: 24px;
|
15 |
+
font-weight: bold;
|
16 |
+
line-height: 2;
|
17 |
+
}
|
18 |
+
|
19 |
+
.auth-tips {
|
20 |
+
font-size: 14px;
|
21 |
+
}
|
22 |
+
|
23 |
+
.auth-input {
|
24 |
+
margin: 3vh 0;
|
25 |
+
}
|
26 |
+
|
27 |
+
.auth-actions {
|
28 |
+
display: flex;
|
29 |
+
justify-content: center;
|
30 |
+
flex-direction: column;
|
31 |
+
|
32 |
+
button:not(:last-child) {
|
33 |
+
margin-bottom: 10px;
|
34 |
+
}
|
35 |
+
}
|
36 |
+
}
|
NeuroGPT/app/components/auth.tsx
ADDED
@@ -0,0 +1,116 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import styles from "./auth.module.scss";
|
2 |
+
import { IconButton } from "./button";
|
3 |
+
|
4 |
+
import { useNavigate } from "react-router-dom";
|
5 |
+
import { Path } from "../constant";
|
6 |
+
import { useAccessStore } from "../store";
|
7 |
+
import Locale from "../locales";
|
8 |
+
|
9 |
+
import BotIcon from "../icons/bot.svg";
|
10 |
+
import { getClientConfig } from "../config/client";
|
11 |
+
|
12 |
+
export function AuthPage() {
|
13 |
+
const navigate = useNavigate();
|
14 |
+
const accessStore = useAccessStore();
|
15 |
+
|
16 |
+
const goHome = () => navigate(Path.Home);
|
17 |
+
const resetAccessCode = () => {
|
18 |
+
accessStore.update((access) => {
|
19 |
+
access.openaiApiKey = "";
|
20 |
+
access.accessCode = "";
|
21 |
+
});
|
22 |
+
}; // Reset access code to empty string
|
23 |
+
const goPrivacy = () => navigate(Path.PrivacyPage);
|
24 |
+
const isApp = getClientConfig()?.isApp;
|
25 |
+
const isSysHasOpenaiApiKey = getClientConfig()?.isSysHasOpenaiApiKey;
|
26 |
+
|
27 |
+
return (
|
28 |
+
<div className={styles["auth-page"]}>
|
29 |
+
<div className={`no-dark ${styles["auth-logo"]}`}>
|
30 |
+
<BotIcon />
|
31 |
+
</div>
|
32 |
+
|
33 |
+
<div className={styles["auth-title"]}>{Locale.Auth.Title}</div>
|
34 |
+
<div className={styles["auth-tips"]}>{Locale.Auth.Tips}</div>
|
35 |
+
|
36 |
+
{!isApp && ( // Conditionally render the input access code based on whether it's not an app
|
37 |
+
<>
|
38 |
+
{isSysHasOpenaiApiKey ? (
|
39 |
+
<>
|
40 |
+
<input
|
41 |
+
className={styles["auth-input"]}
|
42 |
+
type="password"
|
43 |
+
placeholder={Locale.Auth.Input}
|
44 |
+
value={accessStore.accessCode}
|
45 |
+
onChange={(e) => {
|
46 |
+
accessStore.update(
|
47 |
+
(access) => (access.accessCode = e.currentTarget.value),
|
48 |
+
);
|
49 |
+
}}
|
50 |
+
/>
|
51 |
+
<div className={styles["auth-tips"]}>{Locale.Auth.SubTips}</div>
|
52 |
+
<input
|
53 |
+
className={styles["auth-input"]}
|
54 |
+
type="password"
|
55 |
+
placeholder={Locale.Settings.Token.Placeholder}
|
56 |
+
value={accessStore.openaiApiKey}
|
57 |
+
onChange={(e) => {
|
58 |
+
accessStore.update(
|
59 |
+
(access) => (access.openaiApiKey = e.currentTarget.value),
|
60 |
+
);
|
61 |
+
}}
|
62 |
+
/>
|
63 |
+
</>
|
64 |
+
) : (
|
65 |
+
<>
|
66 |
+
<div className={styles["auth-tips"]}>{Locale.Auth.SubTips}</div>
|
67 |
+
<input
|
68 |
+
className={styles["auth-input"]}
|
69 |
+
type="password"
|
70 |
+
placeholder={Locale.Settings.Token.Placeholder}
|
71 |
+
value={accessStore.openaiApiKey}
|
72 |
+
onChange={(e) => {
|
73 |
+
accessStore.update(
|
74 |
+
(access) => (access.openaiApiKey = e.currentTarget.value),
|
75 |
+
);
|
76 |
+
}}
|
77 |
+
/>
|
78 |
+
</>
|
79 |
+
)}
|
80 |
+
</>
|
81 |
+
)}
|
82 |
+
|
83 |
+
{isApp && ( // Conditionally render the input access token based on whether it's an app
|
84 |
+
<>
|
85 |
+
<div className={styles["auth-tips"]}>{Locale.Auth.SubTips}</div>
|
86 |
+
<input
|
87 |
+
className={styles["auth-input"]}
|
88 |
+
type="password"
|
89 |
+
placeholder={Locale.Settings.Access.OpenAI.ApiKey.Placeholder}
|
90 |
+
value={accessStore.openaiApiKey}
|
91 |
+
onChange={(e) => {
|
92 |
+
accessStore.update(
|
93 |
+
(access) => (access.openaiApiKey = e.currentTarget.value),
|
94 |
+
);
|
95 |
+
}}
|
96 |
+
/>
|
97 |
+
</>
|
98 |
+
)}
|
99 |
+
|
100 |
+
<div className={styles["auth-actions"]}>
|
101 |
+
<IconButton
|
102 |
+
text={Locale.Auth.Confirm}
|
103 |
+
type="primary"
|
104 |
+
onClick={goPrivacy}
|
105 |
+
/>
|
106 |
+
<IconButton
|
107 |
+
text={Locale.Auth.Later}
|
108 |
+
onClick={() => {
|
109 |
+
resetAccessCode();
|
110 |
+
goHome();
|
111 |
+
}}
|
112 |
+
/>
|
113 |
+
</div>
|
114 |
+
</div>
|
115 |
+
);
|
116 |
+
}
|
NeuroGPT/app/components/button.module.scss
ADDED
@@ -0,0 +1,83 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
.icon-button {
|
2 |
+
background-color: var(--white);
|
3 |
+
border-radius: 10px;
|
4 |
+
display: flex;
|
5 |
+
align-items: center;
|
6 |
+
justify-content: center;
|
7 |
+
padding: 10px;
|
8 |
+
|
9 |
+
cursor: pointer;
|
10 |
+
transition: all 0.3s ease;
|
11 |
+
overflow: hidden;
|
12 |
+
user-select: none;
|
13 |
+
outline: none;
|
14 |
+
border: none;
|
15 |
+
color: var(--black);
|
16 |
+
|
17 |
+
&[disabled] {
|
18 |
+
cursor: not-allowed;
|
19 |
+
opacity: 0.5;
|
20 |
+
}
|
21 |
+
|
22 |
+
&.primary {
|
23 |
+
background-color: var(--primary);
|
24 |
+
color: white;
|
25 |
+
|
26 |
+
path {
|
27 |
+
fill: white !important;
|
28 |
+
}
|
29 |
+
}
|
30 |
+
|
31 |
+
&.danger {
|
32 |
+
color: rgba($color: red, $alpha: 0.8);
|
33 |
+
border-color: rgba($color: red, $alpha: 0.5);
|
34 |
+
background-color: rgba($color: red, $alpha: 0.05);
|
35 |
+
|
36 |
+
&:hover {
|
37 |
+
border-color: red;
|
38 |
+
background-color: rgba($color: red, $alpha: 0.1);
|
39 |
+
}
|
40 |
+
|
41 |
+
path {
|
42 |
+
fill: red !important;
|
43 |
+
}
|
44 |
+
}
|
45 |
+
|
46 |
+
&:hover,
|
47 |
+
&:focus {
|
48 |
+
border-color: var(--primary);
|
49 |
+
}
|
50 |
+
}
|
51 |
+
|
52 |
+
.shadow {
|
53 |
+
box-shadow: var(--card-shadow);
|
54 |
+
}
|
55 |
+
|
56 |
+
.border {
|
57 |
+
border: var(--border-in-light);
|
58 |
+
}
|
59 |
+
|
60 |
+
.icon-button-icon {
|
61 |
+
width: 16px;
|
62 |
+
height: 16px;
|
63 |
+
display: flex;
|
64 |
+
justify-content: center;
|
65 |
+
align-items: center;
|
66 |
+
}
|
67 |
+
|
68 |
+
@media only screen and (max-width: 600px) {
|
69 |
+
.icon-button {
|
70 |
+
padding: 16px;
|
71 |
+
}
|
72 |
+
}
|
73 |
+
|
74 |
+
.icon-button-text {
|
75 |
+
font-size: 12px;
|
76 |
+
overflow: hidden;
|
77 |
+
text-overflow: ellipsis;
|
78 |
+
white-space: nowrap;
|
79 |
+
|
80 |
+
&:not(:first-child) {
|
81 |
+
margin-left: 5px;
|
82 |
+
}
|
83 |
+
}
|
NeuroGPT/app/components/button.tsx
ADDED
@@ -0,0 +1,51 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import * as React from "react";
|
2 |
+
|
3 |
+
import styles from "./button.module.scss";
|
4 |
+
|
5 |
+
export type ButtonType = "primary" | "danger" | null;
|
6 |
+
|
7 |
+
export function IconButton(props: {
|
8 |
+
onClick?: () => void;
|
9 |
+
icon?: JSX.Element;
|
10 |
+
type?: ButtonType;
|
11 |
+
text?: string;
|
12 |
+
bordered?: boolean;
|
13 |
+
shadow?: boolean;
|
14 |
+
className?: string;
|
15 |
+
title?: string;
|
16 |
+
disabled?: boolean;
|
17 |
+
tabIndex?: number;
|
18 |
+
autoFocus?: boolean;
|
19 |
+
}) {
|
20 |
+
return (
|
21 |
+
<button
|
22 |
+
className={
|
23 |
+
styles["icon-button"] +
|
24 |
+
` ${props.bordered && styles.border} ${props.shadow && styles.shadow} ${
|
25 |
+
props.className ?? ""
|
26 |
+
} clickable ${styles[props.type ?? ""]}`
|
27 |
+
}
|
28 |
+
onClick={props.onClick}
|
29 |
+
title={props.title}
|
30 |
+
disabled={props.disabled}
|
31 |
+
role="button"
|
32 |
+
tabIndex={props.tabIndex}
|
33 |
+
autoFocus={props.autoFocus}
|
34 |
+
>
|
35 |
+
{props.icon && (
|
36 |
+
<div
|
37 |
+
className={
|
38 |
+
styles["icon-button-icon"] +
|
39 |
+
` ${props.type === "primary" && "no-dark"}`
|
40 |
+
}
|
41 |
+
>
|
42 |
+
{props.icon}
|
43 |
+
</div>
|
44 |
+
)}
|
45 |
+
|
46 |
+
{props.text && (
|
47 |
+
<div className={styles["icon-button-text"]}>{props.text}</div>
|
48 |
+
)}
|
49 |
+
</button>
|
50 |
+
);
|
51 |
+
}
|
NeuroGPT/app/components/changelog.module.scss
ADDED
@@ -0,0 +1,108 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
.changelog-page {
|
2 |
+
display: flex;
|
3 |
+
justify-content: center;
|
4 |
+
align-items: center;
|
5 |
+
height: 100%;
|
6 |
+
width: 100%;
|
7 |
+
flex-direction: column;
|
8 |
+
|
9 |
+
.changelog-logo {
|
10 |
+
width: 100px; /* Set the desired width */
|
11 |
+
height: 100px; /* Set the desired height */
|
12 |
+
margin-bottom: 20px; /* Add margin for spacing */
|
13 |
+
}
|
14 |
+
|
15 |
+
.changelog-title {
|
16 |
+
font-size: 24px;
|
17 |
+
font-weight: bold;
|
18 |
+
line-height: 3;
|
19 |
+
position: relative;
|
20 |
+
}
|
21 |
+
|
22 |
+
.changelog-tips {
|
23 |
+
box-sizing: border-box;
|
24 |
+
max-width: 100%;
|
25 |
+
margin-top: 10px;
|
26 |
+
border-radius: 10px;
|
27 |
+
background-color: rgba(0, 0, 0, 0.05);
|
28 |
+
padding: 10px;
|
29 |
+
font-size: 14px;
|
30 |
+
user-select: text;
|
31 |
+
word-break: break-word;
|
32 |
+
border: var(--border-in-light);
|
33 |
+
position: relative;
|
34 |
+
transition: all ease 0.3s;
|
35 |
+
}
|
36 |
+
|
37 |
+
.changelog-input {
|
38 |
+
margin: 3vh 0;
|
39 |
+
}
|
40 |
+
|
41 |
+
.changelog-content {
|
42 |
+
max-height: 78%; /* Set the desired max height */
|
43 |
+
overflow: auto; /* Enable scrolling */
|
44 |
+
width: 95%; /* Set the desired width */
|
45 |
+
margin-top: auto; /* Add margin for spacing */
|
46 |
+
}
|
47 |
+
|
48 |
+
.changelog-actions {
|
49 |
+
display: flex;
|
50 |
+
justify-content: center;
|
51 |
+
flex-direction: column;
|
52 |
+
height: 10%;
|
53 |
+
width: 10%;
|
54 |
+
margin-top: auto; /* Add margin for spacing */
|
55 |
+
padding-top: 20px;
|
56 |
+
padding-bottom: 20px;
|
57 |
+
position: relative;
|
58 |
+
|
59 |
+
button:not(:last-child) {
|
60 |
+
margin-bottom: auto;
|
61 |
+
}
|
62 |
+
|
63 |
+
// Media query for small screens
|
64 |
+
@media (max-width: 600px) {
|
65 |
+
flex-direction: row; /* Change to horizontal layout */
|
66 |
+
justify-content: space-evenly; /* Spread the buttons evenly */
|
67 |
+
align-items: center; /* Center align the buttons vertically */
|
68 |
+
margin-top: auto; /* Adjust the margin */
|
69 |
+
button {
|
70 |
+
width: auto; /* Set a fixed width for the buttons */
|
71 |
+
}
|
72 |
+
}
|
73 |
+
|
74 |
+
// Media query for non-fullscreen mode
|
75 |
+
@media not all and (display-mode: fullscreen) {
|
76 |
+
flex-direction: row; /* Change to horizontal layout */
|
77 |
+
justify-content: space-evenly; /* Spread the buttons evenly */
|
78 |
+
align-items: center; /* Center align the buttons vertically */
|
79 |
+
margin-top: auto; /* Adjust the margin */
|
80 |
+
position: relative;
|
81 |
+
button {
|
82 |
+
width: auto; /* Reset the width to auto */
|
83 |
+
}
|
84 |
+
}
|
85 |
+
}
|
86 |
+
|
87 |
+
.scroll-title {
|
88 |
+
font-size: 18px; /* Set the desired font size */
|
89 |
+
font-weight: bold;
|
90 |
+
line-height: 2;
|
91 |
+
color: #888; /* Set the desired color */
|
92 |
+
}
|
93 |
+
|
94 |
+
// Media query for small screens
|
95 |
+
@media (max-width: 600px) {
|
96 |
+
.changelog-actions {
|
97 |
+
flex-direction: row; /* Change to horizontal layout */
|
98 |
+
justify-content: space-evenly; /* Spread the buttons evenly */
|
99 |
+
align-items: center; /* Center align the buttons vertically */
|
100 |
+
margin-top: auto; /* Adjust the margin */
|
101 |
+
position: relative;
|
102 |
+
}
|
103 |
+
|
104 |
+
button {
|
105 |
+
width: auto; /* Set a fixed width for the buttons */
|
106 |
+
}
|
107 |
+
}
|
108 |
+
}
|
NeuroGPT/app/components/changelog.tsx
ADDED
@@ -0,0 +1,93 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import styles from "./changelog.module.scss";
|
2 |
+
import { IconButton } from "./button";
|
3 |
+
import { getClientConfig } from "@/app/config/client";
|
4 |
+
|
5 |
+
import { useNavigate } from "react-router-dom";
|
6 |
+
import { useEffect, useState } from "react";
|
7 |
+
import { Path } from "../constant";
|
8 |
+
import Locale from "../locales";
|
9 |
+
|
10 |
+
import ConfirmIcon from "../icons/confirm.svg";
|
11 |
+
import LoadingIcon from "../icons/three-dots.svg";
|
12 |
+
import dynamic from "next/dynamic";
|
13 |
+
|
14 |
+
const Markdown = dynamic(async () => (await import("./markdown")).Markdown, {
|
15 |
+
loading: () => <LoadingIcon />,
|
16 |
+
});
|
17 |
+
|
18 |
+
export function ChangeLog(props: { onClose?: () => void }) {
|
19 |
+
const navigate = useNavigate();
|
20 |
+
const [mdText, setMdText] = useState("");
|
21 |
+
const [pageTitle] = useState("📌 Change Log 📝");
|
22 |
+
|
23 |
+
useEffect(() => {
|
24 |
+
const fetchData = async () => {
|
25 |
+
const commitInfo = getClientConfig();
|
26 |
+
|
27 |
+
let table = `## 🚀 What's Changed ? ${commitInfo?.commitDate
|
28 |
+
? new Date(parseInt(commitInfo.commitDate)).toLocaleString()
|
29 |
+
: "Unknown Date"
|
30 |
+
} 🗓️\n`;
|
31 |
+
|
32 |
+
if (commitInfo?.commitMessage.description) {
|
33 |
+
const author = commitInfo.Author?.replace(/\s/g, "") || "Unknown Author";
|
34 |
+
const coAuthored = commitInfo.commitMessage["Co-authored-by"] || [];
|
35 |
+
const uniqueCoAuthored = [...new Set(coAuthored)]; // Filter out duplicates
|
36 |
+
const changes = commitInfo.commitMessage.description.filter(
|
37 |
+
(change: string) => !change.startsWith("...")
|
38 |
+
);
|
39 |
+
const changesFormatted = changes
|
40 |
+
.map((change: string) => `\n\n\n ${change}\n\n`)
|
41 |
+
.join("\n\n\n");
|
42 |
+
|
43 |
+
let coAuthorsSection = "";
|
44 |
+
if (uniqueCoAuthored.length > 0) {
|
45 |
+
const coAuthorLinks = uniqueCoAuthored.map(
|
46 |
+
(coAuthor: string) =>
|
47 |
+
`[${coAuthor.replace("[bot]", "").replace(/\s/g, "")}](https://github.com/${coAuthor.replace("[bot]", "").replace(/\s/g, "")})`
|
48 |
+
);
|
49 |
+
coAuthorsSection = `(Co-Authored by ${coAuthorLinks.join(", ")})`;
|
50 |
+
}
|
51 |
+
|
52 |
+
const authorSection = `[${author.replace("[bot]", "").replace(/\s/g, "")}](https://github.com/${author.replace("[bot]", "").replace(/\s/g, "")}) ${coAuthorsSection}`;
|
53 |
+
const prLinkRegex = /#(\d+)/g; // Regular expression to match '#<number>' ref : autolinks github
|
54 |
+
const prLink = commitInfo?.commitMessage.summary.replace(prLinkRegex, '[$&](https://github.com/H0llyW00dzZ/NeuroGPT/pull/$1/commits)');
|
55 |
+
const descriptionWithLinks = commitInfo?.commitMessage.description.map((change: string) =>
|
56 |
+
change.replace(prLinkRegex, '[$&](https://github.com/H0llyW00dzZ/NeuroGPT/pull/$1/commits)')
|
57 |
+
).join('\n\n\n');
|
58 |
+
|
59 |
+
table += `\n\n\n![GitHub contributors](https://img.shields.io/github/contributors/Yidadaa/NeuroGPT.svg) ![GitHub commits](https://badgen.net/github/commits/H0llyW00dzZ/NeuroGPT) ![GitHub license](https://img.shields.io/github/license/H0llyW00dzZ/NeuroGPT) [![GitHub forks](https://img.shields.io/github/forks/Yidadaa/NeuroGPT.svg)](https://github.com/Yidadaa/NeuroGPT/network/members) [![GitHub stars](https://img.shields.io/github/stars/Yidadaa/NeuroGPT.svg)](https://github.com/Yidadaa/NeuroGPT/stargazers) [![Github All Releases](https://img.shields.io/github/downloads/Yidadaa/NeuroGPT/total.svg)](https://github.com/Yidadaa/NeuroGPT/releases/) [![CI: CodeQL Unit Testing Advanced](https://github.com/H0llyW00dzZ/NeuroGPT/actions/workflows/codeql.yml/badge.svg)](https://github.com/H0llyW00dzZ/NeuroGPT/actions/workflows/codeql.yml) \n\n\n [![GitHub](https://img.shields.io/badge/--181717?logo=github&logoColor=ffffff)](https://github.com/${author}) ![${author.replace("[bot]", "")}](https://github.com/${author.replace("[bot]", "")}.png?size=25) ${authorSection} :\n\n${prLink}\n\n\n${descriptionWithLinks}\n\n\n\n\n\n`;
|
60 |
+
} else {
|
61 |
+
table += `###${commitInfo?.commitMessage.summary}###\nNo changes\n\n`;
|
62 |
+
}
|
63 |
+
|
64 |
+
setMdText(table);
|
65 |
+
};
|
66 |
+
|
67 |
+
fetchData();
|
68 |
+
|
69 |
+
}, []);
|
70 |
+
|
71 |
+
return (
|
72 |
+
<div className={styles["changelog-page"]}>
|
73 |
+
<div className={`changelog-title ${styles["changelog-title"]}`}>
|
74 |
+
{pageTitle}
|
75 |
+
</div>
|
76 |
+
<div className={styles["changelog-content"]}>
|
77 |
+
<div className={styles["markdown-body"]}>
|
78 |
+
<Markdown content={mdText} />
|
79 |
+
</div>
|
80 |
+
</div>
|
81 |
+
<div className={styles["changelog-actions"]}>
|
82 |
+
<div className={styles["changelog-actions-button"]}>
|
83 |
+
<IconButton
|
84 |
+
text={Locale.UI.Close}
|
85 |
+
icon={<ConfirmIcon />}
|
86 |
+
onClick={() => navigate(-1)}
|
87 |
+
bordered
|
88 |
+
/>
|
89 |
+
</div>
|
90 |
+
</div>
|
91 |
+
</div>
|
92 |
+
);
|
93 |
+
}
|
NeuroGPT/app/components/chat-list.tsx
ADDED
@@ -0,0 +1,167 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import DeleteIcon from "../icons/delete.svg";
|
2 |
+
import BotIcon from "../icons/bot.svg";
|
3 |
+
|
4 |
+
import styles from "./home.module.scss";
|
5 |
+
import {
|
6 |
+
DragDropContext,
|
7 |
+
Droppable,
|
8 |
+
Draggable,
|
9 |
+
OnDragEndResponder,
|
10 |
+
} from "@hello-pangea/dnd";
|
11 |
+
|
12 |
+
import { useChatStore } from "../store";
|
13 |
+
|
14 |
+
import Locale from "../locales";
|
15 |
+
import { Link, useNavigate } from "react-router-dom";
|
16 |
+
import { Path } from "../constant";
|
17 |
+
import { MaskAvatar } from "./mask";
|
18 |
+
import { Mask } from "../store/mask";
|
19 |
+
import { useRef, useEffect } from "react";
|
20 |
+
import { showConfirm } from "./ui-lib";
|
21 |
+
import { useMobileScreen } from "../utils";
|
22 |
+
|
23 |
+
export function ChatItem(props: {
|
24 |
+
onClick?: () => void;
|
25 |
+
onDelete?: () => void;
|
26 |
+
title: string;
|
27 |
+
count: number;
|
28 |
+
time: string;
|
29 |
+
selected: boolean;
|
30 |
+
id: string;
|
31 |
+
index: number;
|
32 |
+
narrow?: boolean;
|
33 |
+
mask: Mask;
|
34 |
+
}) {
|
35 |
+
const draggableRef = useRef<HTMLDivElement | null>(null);
|
36 |
+
useEffect(() => {
|
37 |
+
if (props.selected && draggableRef.current) {
|
38 |
+
draggableRef.current?.scrollIntoView({
|
39 |
+
block: "center",
|
40 |
+
});
|
41 |
+
}
|
42 |
+
}, [props.selected]);
|
43 |
+
return (
|
44 |
+
<Draggable draggableId={`${props.id}`} index={props.index}>
|
45 |
+
{(provided) => (
|
46 |
+
<div
|
47 |
+
className={`${styles["chat-item"]} ${
|
48 |
+
props.selected && styles["chat-item-selected"]
|
49 |
+
}`}
|
50 |
+
onClick={props.onClick}
|
51 |
+
ref={(ele) => {
|
52 |
+
draggableRef.current = ele;
|
53 |
+
provided.innerRef(ele);
|
54 |
+
}}
|
55 |
+
{...provided.draggableProps}
|
56 |
+
{...provided.dragHandleProps}
|
57 |
+
title={`${props.title}\n${Locale.ChatItem.ChatItemCount(
|
58 |
+
props.count,
|
59 |
+
)}`}
|
60 |
+
>
|
61 |
+
{props.narrow ? (
|
62 |
+
<div className={styles["chat-item-narrow"]}>
|
63 |
+
<div className={styles["chat-item-avatar"] + " no-dark"}>
|
64 |
+
<MaskAvatar mask={props.mask} />
|
65 |
+
</div>
|
66 |
+
<div className={styles["chat-item-narrow-count"]}>
|
67 |
+
{props.count}
|
68 |
+
</div>
|
69 |
+
</div>
|
70 |
+
) : (
|
71 |
+
<>
|
72 |
+
<div className={styles["chat-item-title"]}>{props.title}</div>
|
73 |
+
<div className={styles["chat-item-info"]}>
|
74 |
+
<div className={styles["chat-item-count"]}>
|
75 |
+
{Locale.ChatItem.ChatItemCount(props.count)}
|
76 |
+
</div>
|
77 |
+
<div className={styles["chat-item-date"]}>{props.time}</div>
|
78 |
+
</div>
|
79 |
+
</>
|
80 |
+
)}
|
81 |
+
|
82 |
+
<div
|
83 |
+
className={styles["chat-item-delete"]}
|
84 |
+
onClickCapture={(e) => {
|
85 |
+
props.onDelete?.();
|
86 |
+
e.preventDefault();
|
87 |
+
e.stopPropagation();
|
88 |
+
}}
|
89 |
+
>
|
90 |
+
<DeleteIcon />
|
91 |
+
</div>
|
92 |
+
</div>
|
93 |
+
)}
|
94 |
+
</Draggable>
|
95 |
+
);
|
96 |
+
}
|
97 |
+
|
98 |
+
export function ChatList(props: { narrow?: boolean }) {
|
99 |
+
const [sessions, selectedIndex, selectSession, moveSession] = useChatStore(
|
100 |
+
(state) => [
|
101 |
+
state.sessions,
|
102 |
+
state.currentSessionIndex,
|
103 |
+
state.selectSession,
|
104 |
+
state.moveSession,
|
105 |
+
],
|
106 |
+
);
|
107 |
+
const chatStore = useChatStore();
|
108 |
+
const navigate = useNavigate();
|
109 |
+
const isMobileScreen = useMobileScreen();
|
110 |
+
|
111 |
+
const onDragEnd: OnDragEndResponder = (result) => {
|
112 |
+
const { destination, source } = result;
|
113 |
+
if (!destination) {
|
114 |
+
return;
|
115 |
+
}
|
116 |
+
|
117 |
+
if (
|
118 |
+
destination.droppableId === source.droppableId &&
|
119 |
+
destination.index === source.index
|
120 |
+
) {
|
121 |
+
return;
|
122 |
+
}
|
123 |
+
|
124 |
+
moveSession(source.index, destination.index);
|
125 |
+
};
|
126 |
+
|
127 |
+
return (
|
128 |
+
<DragDropContext onDragEnd={onDragEnd}>
|
129 |
+
<Droppable droppableId="chat-list">
|
130 |
+
{(provided) => (
|
131 |
+
<div
|
132 |
+
className={styles["chat-list"]}
|
133 |
+
ref={provided.innerRef}
|
134 |
+
{...provided.droppableProps}
|
135 |
+
>
|
136 |
+
{sessions.map((item, i) => (
|
137 |
+
<ChatItem
|
138 |
+
title={item.topic}
|
139 |
+
time={new Date(item.lastUpdate).toLocaleString()}
|
140 |
+
count={item.messages.length}
|
141 |
+
key={item.id}
|
142 |
+
id={item.id}
|
143 |
+
index={i}
|
144 |
+
selected={i === selectedIndex}
|
145 |
+
onClick={() => {
|
146 |
+
navigate(Path.Chat);
|
147 |
+
selectSession(i);
|
148 |
+
}}
|
149 |
+
onDelete={async () => {
|
150 |
+
if (
|
151 |
+
(!props.narrow && !isMobileScreen) ||
|
152 |
+
(await showConfirm(Locale.Home.DeleteChat))
|
153 |
+
) {
|
154 |
+
chatStore.deleteSession(i);
|
155 |
+
}
|
156 |
+
}}
|
157 |
+
narrow={props.narrow}
|
158 |
+
mask={item.mask}
|
159 |
+
/>
|
160 |
+
))}
|
161 |
+
{provided.placeholder}
|
162 |
+
</div>
|
163 |
+
)}
|
164 |
+
</Droppable>
|
165 |
+
</DragDropContext>
|
166 |
+
);
|
167 |
+
}
|
NeuroGPT/app/components/chat.module.scss
ADDED
@@ -0,0 +1,518 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
@import "../styles/animation.scss";
|
2 |
+
|
3 |
+
.chat-input-actions {
|
4 |
+
display: flex;
|
5 |
+
flex-wrap: wrap;
|
6 |
+
|
7 |
+
.chat-input-action {
|
8 |
+
display: inline-flex;
|
9 |
+
border-radius: 20px;
|
10 |
+
font-size: 12px;
|
11 |
+
background-color: var(--white);
|
12 |
+
color: var(--black);
|
13 |
+
border: var(--border-in-light);
|
14 |
+
padding: 4px 10px;
|
15 |
+
animation: slide-in ease 0.3s;
|
16 |
+
box-shadow: var(--card-shadow);
|
17 |
+
transition: width ease 0.3s;
|
18 |
+
align-items: center;
|
19 |
+
height: 16px;
|
20 |
+
width: var(--icon-width);
|
21 |
+
overflow: hidden;
|
22 |
+
|
23 |
+
&:not(:last-child) {
|
24 |
+
margin-right: 5px;
|
25 |
+
}
|
26 |
+
|
27 |
+
.text {
|
28 |
+
white-space: nowrap;
|
29 |
+
padding-left: 5px;
|
30 |
+
opacity: 0;
|
31 |
+
transform: translateX(-5px);
|
32 |
+
transition: all ease 0.3s;
|
33 |
+
pointer-events: none;
|
34 |
+
}
|
35 |
+
|
36 |
+
&:hover {
|
37 |
+
--delay: 0.5s;
|
38 |
+
width: var(--full-width);
|
39 |
+
transition-delay: var(--delay);
|
40 |
+
|
41 |
+
.text {
|
42 |
+
transition-delay: var(--delay);
|
43 |
+
opacity: 1;
|
44 |
+
transform: translate(0);
|
45 |
+
}
|
46 |
+
}
|
47 |
+
|
48 |
+
.text,
|
49 |
+
.icon {
|
50 |
+
display: flex;
|
51 |
+
align-items: center;
|
52 |
+
justify-content: center;
|
53 |
+
}
|
54 |
+
}
|
55 |
+
}
|
56 |
+
|
57 |
+
.prompt-toast {
|
58 |
+
position: absolute;
|
59 |
+
bottom: -50px;
|
60 |
+
z-index: 999;
|
61 |
+
display: flex;
|
62 |
+
justify-content: center;
|
63 |
+
width: calc(100% - 40px);
|
64 |
+
|
65 |
+
.prompt-toast-inner {
|
66 |
+
display: flex;
|
67 |
+
justify-content: center;
|
68 |
+
align-items: center;
|
69 |
+
font-size: 12px;
|
70 |
+
background-color: var(--white);
|
71 |
+
color: var(--black);
|
72 |
+
|
73 |
+
border: var(--border-in-light);
|
74 |
+
box-shadow: var(--card-shadow);
|
75 |
+
padding: 10px 20px;
|
76 |
+
border-radius: 100px;
|
77 |
+
|
78 |
+
animation: slide-in-from-top ease 0.3s;
|
79 |
+
|
80 |
+
.prompt-toast-content {
|
81 |
+
margin-left: 10px;
|
82 |
+
}
|
83 |
+
}
|
84 |
+
}
|
85 |
+
|
86 |
+
.section-title {
|
87 |
+
font-size: 12px;
|
88 |
+
font-weight: bold;
|
89 |
+
margin-bottom: 10px;
|
90 |
+
display: flex;
|
91 |
+
justify-content: space-between;
|
92 |
+
align-items: center;
|
93 |
+
|
94 |
+
.section-title-action {
|
95 |
+
display: flex;
|
96 |
+
align-items: center;
|
97 |
+
}
|
98 |
+
}
|
99 |
+
|
100 |
+
.context-prompt {
|
101 |
+
.context-prompt-insert {
|
102 |
+
display: flex;
|
103 |
+
justify-content: center;
|
104 |
+
padding: 4px;
|
105 |
+
opacity: 0.2;
|
106 |
+
transition: all ease 0.3s;
|
107 |
+
background-color: rgba(0, 0, 0, 0);
|
108 |
+
cursor: pointer;
|
109 |
+
border-radius: 4px;
|
110 |
+
margin-top: 4px;
|
111 |
+
margin-bottom: 4px;
|
112 |
+
|
113 |
+
&:hover {
|
114 |
+
opacity: 1;
|
115 |
+
background-color: rgba(0, 0, 0, 0.05);
|
116 |
+
}
|
117 |
+
}
|
118 |
+
|
119 |
+
.context-prompt-row {
|
120 |
+
display: flex;
|
121 |
+
justify-content: center;
|
122 |
+
width: 100%;
|
123 |
+
|
124 |
+
&:hover {
|
125 |
+
.context-drag {
|
126 |
+
opacity: 1;
|
127 |
+
}
|
128 |
+
}
|
129 |
+
|
130 |
+
.context-drag {
|
131 |
+
display: flex;
|
132 |
+
align-items: center;
|
133 |
+
opacity: 0.5;
|
134 |
+
transition: all ease 0.3s;
|
135 |
+
}
|
136 |
+
|
137 |
+
.context-role {
|
138 |
+
margin-right: 10px;
|
139 |
+
}
|
140 |
+
|
141 |
+
.context-content {
|
142 |
+
flex: 1;
|
143 |
+
max-width: 100%;
|
144 |
+
text-align: left;
|
145 |
+
}
|
146 |
+
|
147 |
+
.context-delete-button {
|
148 |
+
margin-left: 10px;
|
149 |
+
}
|
150 |
+
}
|
151 |
+
|
152 |
+
.context-prompt-button {
|
153 |
+
flex: 1;
|
154 |
+
}
|
155 |
+
}
|
156 |
+
|
157 |
+
.memory-prompt {
|
158 |
+
margin: 20px 0;
|
159 |
+
|
160 |
+
.memory-prompt-content {
|
161 |
+
background-color: var(--white);
|
162 |
+
color: var(--black);
|
163 |
+
border: var(--border-in-light);
|
164 |
+
border-radius: 10px;
|
165 |
+
padding: 10px;
|
166 |
+
font-size: 12px;
|
167 |
+
user-select: text;
|
168 |
+
}
|
169 |
+
}
|
170 |
+
|
171 |
+
.clear-context {
|
172 |
+
margin: 20px 0 0 0;
|
173 |
+
padding: 4px 0;
|
174 |
+
|
175 |
+
border-top: var(--border-in-light);
|
176 |
+
border-bottom: var(--border-in-light);
|
177 |
+
box-shadow: var(--card-shadow) inset;
|
178 |
+
|
179 |
+
display: flex;
|
180 |
+
justify-content: center;
|
181 |
+
align-items: center;
|
182 |
+
|
183 |
+
color: var(--black);
|
184 |
+
transition: all ease 0.3s;
|
185 |
+
cursor: pointer;
|
186 |
+
overflow: hidden;
|
187 |
+
position: relative;
|
188 |
+
font-size: 12px;
|
189 |
+
|
190 |
+
animation: slide-in ease 0.3s;
|
191 |
+
|
192 |
+
$linear: linear-gradient(
|
193 |
+
to right,
|
194 |
+
rgba(0, 0, 0, 0),
|
195 |
+
rgba(0, 0, 0, 1),
|
196 |
+
rgba(0, 0, 0, 0)
|
197 |
+
);
|
198 |
+
mask-image: $linear;
|
199 |
+
|
200 |
+
@mixin show {
|
201 |
+
transform: translateY(0);
|
202 |
+
position: relative;
|
203 |
+
transition: all ease 0.3s;
|
204 |
+
opacity: 1;
|
205 |
+
}
|
206 |
+
|
207 |
+
@mixin hide {
|
208 |
+
transform: translateY(-50%);
|
209 |
+
position: absolute;
|
210 |
+
transition: all ease 0.1s;
|
211 |
+
opacity: 0;
|
212 |
+
}
|
213 |
+
|
214 |
+
&-tips {
|
215 |
+
@include show;
|
216 |
+
opacity: 0.5;
|
217 |
+
}
|
218 |
+
|
219 |
+
&-revert-btn {
|
220 |
+
color: var(--primary);
|
221 |
+
@include hide;
|
222 |
+
}
|
223 |
+
|
224 |
+
&:hover {
|
225 |
+
opacity: 1;
|
226 |
+
border-color: var(--primary);
|
227 |
+
|
228 |
+
.clear-context-tips {
|
229 |
+
@include hide;
|
230 |
+
}
|
231 |
+
|
232 |
+
.clear-context-revert-btn {
|
233 |
+
@include show;
|
234 |
+
}
|
235 |
+
}
|
236 |
+
}
|
237 |
+
|
238 |
+
.chat {
|
239 |
+
display: flex;
|
240 |
+
flex-direction: column;
|
241 |
+
position: relative;
|
242 |
+
height: 100%;
|
243 |
+
}
|
244 |
+
|
245 |
+
.chat-body {
|
246 |
+
flex: 1;
|
247 |
+
overflow: auto;
|
248 |
+
overflow-x: hidden;
|
249 |
+
padding: 20px;
|
250 |
+
padding-bottom: 40px;
|
251 |
+
position: relative;
|
252 |
+
overscroll-behavior: none;
|
253 |
+
}
|
254 |
+
|
255 |
+
.chat-body-main-title {
|
256 |
+
cursor: pointer;
|
257 |
+
|
258 |
+
&:hover {
|
259 |
+
text-decoration: underline;
|
260 |
+
}
|
261 |
+
}
|
262 |
+
|
263 |
+
@media only screen and (max-width: 600px) {
|
264 |
+
.chat-body-title {
|
265 |
+
text-align: center;
|
266 |
+
}
|
267 |
+
}
|
268 |
+
|
269 |
+
.chat-message {
|
270 |
+
display: flex;
|
271 |
+
flex-direction: row;
|
272 |
+
|
273 |
+
&:last-child {
|
274 |
+
animation: slide-in ease 0.3s;
|
275 |
+
}
|
276 |
+
}
|
277 |
+
|
278 |
+
.chat-message-user {
|
279 |
+
display: flex;
|
280 |
+
flex-direction: row-reverse;
|
281 |
+
|
282 |
+
.chat-message-header {
|
283 |
+
flex-direction: row-reverse;
|
284 |
+
}
|
285 |
+
}
|
286 |
+
|
287 |
+
.chat-message-header {
|
288 |
+
margin-top: 20px;
|
289 |
+
display: flex;
|
290 |
+
align-items: center;
|
291 |
+
|
292 |
+
.chat-message-actions {
|
293 |
+
display: flex;
|
294 |
+
box-sizing: border-box;
|
295 |
+
font-size: 12px;
|
296 |
+
align-items: flex-end;
|
297 |
+
justify-content: space-between;
|
298 |
+
transition: all ease 0.3s;
|
299 |
+
transform: scale(0.9) translateY(5px);
|
300 |
+
margin: 0 10px;
|
301 |
+
opacity: 0;
|
302 |
+
pointer-events: none;
|
303 |
+
|
304 |
+
.chat-input-actions {
|
305 |
+
display: flex;
|
306 |
+
flex-wrap: nowrap;
|
307 |
+
}
|
308 |
+
}
|
309 |
+
}
|
310 |
+
|
311 |
+
.chat-message-container {
|
312 |
+
max-width: var(--message-max-width);
|
313 |
+
display: flex;
|
314 |
+
flex-direction: column;
|
315 |
+
align-items: flex-start;
|
316 |
+
|
317 |
+
&:hover {
|
318 |
+
.chat-message-edit {
|
319 |
+
opacity: 0.9;
|
320 |
+
}
|
321 |
+
|
322 |
+
.chat-message-actions {
|
323 |
+
opacity: 1;
|
324 |
+
pointer-events: all;
|
325 |
+
transform: scale(1) translateY(0);
|
326 |
+
}
|
327 |
+
}
|
328 |
+
}
|
329 |
+
|
330 |
+
.chat-message-user > .chat-message-container {
|
331 |
+
align-items: flex-end;
|
332 |
+
}
|
333 |
+
|
334 |
+
.chat-message-avatar {
|
335 |
+
position: relative;
|
336 |
+
|
337 |
+
.chat-message-edit {
|
338 |
+
position: absolute;
|
339 |
+
height: 100%;
|
340 |
+
width: 100%;
|
341 |
+
overflow: hidden;
|
342 |
+
display: flex;
|
343 |
+
align-items: center;
|
344 |
+
justify-content: center;
|
345 |
+
opacity: 0;
|
346 |
+
transition: all ease 0.3s;
|
347 |
+
|
348 |
+
button {
|
349 |
+
padding: 7px;
|
350 |
+
}
|
351 |
+
}
|
352 |
+
/* Specific styles for iOS devices */
|
353 |
+
@media screen and (max-device-width: 812px) and (-webkit-min-device-pixel-ratio: 2) {
|
354 |
+
@supports (-webkit-touch-callout: none) {
|
355 |
+
.chat-message-edit {
|
356 |
+
top: -8%;
|
357 |
+
}
|
358 |
+
}
|
359 |
+
}
|
360 |
+
}
|
361 |
+
|
362 |
+
.chat-message-status {
|
363 |
+
font-size: 12px;
|
364 |
+
color: #aaa;
|
365 |
+
line-height: 1.5;
|
366 |
+
margin-top: 5px;
|
367 |
+
}
|
368 |
+
|
369 |
+
.chat-message-item {
|
370 |
+
box-sizing: border-box;
|
371 |
+
max-width: 100%;
|
372 |
+
margin-top: 10px;
|
373 |
+
border-radius: 10px;
|
374 |
+
background-color: rgba(0, 0, 0, 0.05);
|
375 |
+
padding: 10px;
|
376 |
+
font-size: 14px;
|
377 |
+
user-select: text;
|
378 |
+
word-break: break-word;
|
379 |
+
border: var(--border-in-light);
|
380 |
+
position: relative;
|
381 |
+
transition: all ease 0.3s;
|
382 |
+
}
|
383 |
+
|
384 |
+
.chat-message-action-date {
|
385 |
+
font-size: 12px;
|
386 |
+
opacity: 0.2;
|
387 |
+
white-space: nowrap;
|
388 |
+
transition: all ease 0.6s;
|
389 |
+
color: var(--black);
|
390 |
+
text-align: right;
|
391 |
+
width: 100%;
|
392 |
+
box-sizing: border-box;
|
393 |
+
padding-right: 10px;
|
394 |
+
pointer-events: none;
|
395 |
+
z-index: 1;
|
396 |
+
}
|
397 |
+
|
398 |
+
.chat-message-user > .chat-message-container > .chat-message-item {
|
399 |
+
background-color: var(--second);
|
400 |
+
|
401 |
+
&:hover {
|
402 |
+
min-width: 0;
|
403 |
+
}
|
404 |
+
}
|
405 |
+
|
406 |
+
.chat-input-panel {
|
407 |
+
position: relative;
|
408 |
+
width: 100%;
|
409 |
+
padding: 20px;
|
410 |
+
padding-top: 10px;
|
411 |
+
box-sizing: border-box;
|
412 |
+
flex-direction: column;
|
413 |
+
border-top: var(--border-in-light);
|
414 |
+
box-shadow: var(--card-shadow);
|
415 |
+
|
416 |
+
.chat-input-actions {
|
417 |
+
.chat-input-action {
|
418 |
+
margin-bottom: 10px;
|
419 |
+
}
|
420 |
+
}
|
421 |
+
}
|
422 |
+
|
423 |
+
@mixin single-line {
|
424 |
+
white-space: nowrap;
|
425 |
+
overflow: hidden;
|
426 |
+
text-overflow: ellipsis;
|
427 |
+
}
|
428 |
+
|
429 |
+
.prompt-hints {
|
430 |
+
min-height: 20px;
|
431 |
+
width: 100%;
|
432 |
+
max-height: 50vh;
|
433 |
+
overflow: auto;
|
434 |
+
display: flex;
|
435 |
+
flex-direction: column-reverse;
|
436 |
+
|
437 |
+
background-color: var(--white);
|
438 |
+
border: var(--border-in-light);
|
439 |
+
border-radius: 10px;
|
440 |
+
margin-bottom: 10px;
|
441 |
+
box-shadow: var(--shadow);
|
442 |
+
|
443 |
+
.prompt-hint {
|
444 |
+
color: var(--black);
|
445 |
+
padding: 6px 10px;
|
446 |
+
animation: slide-in ease 0.3s;
|
447 |
+
cursor: pointer;
|
448 |
+
transition: all ease 0.3s;
|
449 |
+
border: transparent 1px solid;
|
450 |
+
margin: 4px;
|
451 |
+
border-radius: 8px;
|
452 |
+
|
453 |
+
&:not(:last-child) {
|
454 |
+
margin-top: 0;
|
455 |
+
}
|
456 |
+
|
457 |
+
.hint-title {
|
458 |
+
font-size: 12px;
|
459 |
+
font-weight: bolder;
|
460 |
+
|
461 |
+
@include single-line();
|
462 |
+
}
|
463 |
+
.hint-content {
|
464 |
+
font-size: 12px;
|
465 |
+
|
466 |
+
@include single-line();
|
467 |
+
}
|
468 |
+
|
469 |
+
&-selected,
|
470 |
+
&:hover {
|
471 |
+
border-color: var(--primary);
|
472 |
+
}
|
473 |
+
}
|
474 |
+
}
|
475 |
+
|
476 |
+
.chat-input-panel-inner {
|
477 |
+
display: flex;
|
478 |
+
flex: 1;
|
479 |
+
}
|
480 |
+
|
481 |
+
.chat-input {
|
482 |
+
height: 100%;
|
483 |
+
width: 100%;
|
484 |
+
border-radius: 10px;
|
485 |
+
border: var(--border-in-light);
|
486 |
+
box-shadow: 0 -2px 5px rgba(0, 0, 0, 0.03);
|
487 |
+
background-color: var(--white);
|
488 |
+
color: var(--black);
|
489 |
+
font-family: inherit;
|
490 |
+
padding: 10px 90px 10px 14px;
|
491 |
+
resize: none;
|
492 |
+
outline: none;
|
493 |
+
box-sizing: border-box;
|
494 |
+
min-height: 68px;
|
495 |
+
}
|
496 |
+
|
497 |
+
.chat-input:focus {
|
498 |
+
border: 1px solid var(--primary);
|
499 |
+
}
|
500 |
+
|
501 |
+
.chat-input-send {
|
502 |
+
background-color: var(--primary);
|
503 |
+
color: white;
|
504 |
+
|
505 |
+
position: absolute;
|
506 |
+
right: 30px;
|
507 |
+
bottom: 32px;
|
508 |
+
}
|
509 |
+
|
510 |
+
@media only screen and (max-width: 600px) {
|
511 |
+
.chat-input {
|
512 |
+
font-size: 16px;
|
513 |
+
}
|
514 |
+
|
515 |
+
.chat-input-send {
|
516 |
+
bottom: 30px;
|
517 |
+
}
|
518 |
+
}
|
NeuroGPT/app/components/chat.tsx
ADDED
@@ -0,0 +1,1453 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import { useDebouncedCallback } from "use-debounce";
|
2 |
+
import React, {
|
3 |
+
useState,
|
4 |
+
useRef,
|
5 |
+
useEffect,
|
6 |
+
useMemo,
|
7 |
+
useCallback,
|
8 |
+
Fragment,
|
9 |
+
} from "react";
|
10 |
+
|
11 |
+
import SendWhiteIcon from "../icons/send-white.svg";
|
12 |
+
import BrainIcon from "../icons/brain.svg";
|
13 |
+
import RenameIcon from "../icons/rename.svg";
|
14 |
+
import ExportIcon from "../icons/share.svg";
|
15 |
+
import ReturnIcon from "../icons/return.svg";
|
16 |
+
import CopyIcon from "../icons/copy.svg";
|
17 |
+
import LoadingIcon from "../icons/three-dots.svg";
|
18 |
+
import PromptIcon from "../icons/prompt.svg";
|
19 |
+
import MaskIcon from "../icons/mask.svg";
|
20 |
+
import MaxIcon from "../icons/max.svg";
|
21 |
+
import MinIcon from "../icons/min.svg";
|
22 |
+
import ResetIcon from "../icons/reload.svg";
|
23 |
+
import BreakIcon from "../icons/break.svg";
|
24 |
+
import SettingsIcon from "../icons/chat-settings.svg";
|
25 |
+
import DeleteIcon from "../icons/clear.svg";
|
26 |
+
import PinIcon from "../icons/pin.svg";
|
27 |
+
import EditIcon from "../icons/rename.svg";
|
28 |
+
import ConfirmIcon from "../icons/confirm.svg";
|
29 |
+
import CancelIcon from "../icons/cancel.svg";
|
30 |
+
import DownloadIcon from "../icons/download.svg";
|
31 |
+
import UploadIcon from "../icons/upload.svg";
|
32 |
+
|
33 |
+
import LightIcon from "../icons/light.svg";
|
34 |
+
import DarkIcon from "../icons/dark.svg";
|
35 |
+
import AutoIcon from "../icons/auto.svg";
|
36 |
+
import BottomIcon from "../icons/bottom.svg";
|
37 |
+
import StopIcon from "../icons/pause.svg";
|
38 |
+
import RobotIcon from "../icons/robot.svg";
|
39 |
+
import EyeOnIcon from "../icons/eye.svg";
|
40 |
+
import EyeOffIcon from "../icons/eye-off.svg";
|
41 |
+
import { escapeRegExp } from "lodash";
|
42 |
+
|
43 |
+
import {
|
44 |
+
ChatMessage,
|
45 |
+
SubmitKey,
|
46 |
+
useChatStore,
|
47 |
+
BOT_HELLO,
|
48 |
+
createMessage,
|
49 |
+
useAccessStore,
|
50 |
+
Theme,
|
51 |
+
useAppConfig,
|
52 |
+
DEFAULT_TOPIC,
|
53 |
+
ModelType,
|
54 |
+
} from "../store";
|
55 |
+
|
56 |
+
import {
|
57 |
+
copyToClipboard,
|
58 |
+
selectOrCopy,
|
59 |
+
autoGrowTextArea,
|
60 |
+
useMobileScreen,
|
61 |
+
downloadAs,
|
62 |
+
readFromFile,
|
63 |
+
} from "../utils";
|
64 |
+
|
65 |
+
import dynamic from "next/dynamic";
|
66 |
+
|
67 |
+
import { ChatControllerPool } from "../client/controller";
|
68 |
+
import { Prompt, usePromptStore } from "../store/prompt";
|
69 |
+
import Locale from "../locales";
|
70 |
+
|
71 |
+
import { IconButton } from "./button";
|
72 |
+
import styles from "./chat.module.scss";
|
73 |
+
|
74 |
+
import {
|
75 |
+
List,
|
76 |
+
ListItem,
|
77 |
+
Modal,
|
78 |
+
Selector,
|
79 |
+
showConfirm,
|
80 |
+
showPrompt,
|
81 |
+
showToast,
|
82 |
+
} from "./ui-lib";
|
83 |
+
import { useNavigate } from "react-router-dom";
|
84 |
+
import {
|
85 |
+
CHAT_PAGE_SIZE,
|
86 |
+
LAST_INPUT_KEY,
|
87 |
+
Path,
|
88 |
+
REQUEST_TIMEOUT_MS,
|
89 |
+
UNFINISHED_INPUT,
|
90 |
+
} from "../constant";
|
91 |
+
import { Avatar } from "./emoji";
|
92 |
+
import { ContextPrompts, MaskAvatar, MaskConfig } from "./mask";
|
93 |
+
import { useMaskStore } from "../store/mask";
|
94 |
+
import { ChatCommandPrefix, useChatCommand, useCommand } from "../command";
|
95 |
+
import { prettyObject } from "../utils/format";
|
96 |
+
import { ExportMessageModal } from "./exporter";
|
97 |
+
import { getClientConfig } from "../config/client";
|
98 |
+
import { useAllModels } from "../utils/hooks";
|
99 |
+
|
100 |
+
const Markdown = dynamic(async () => (await import("./markdown")).Markdown, {
|
101 |
+
loading: () => <LoadingIcon />,
|
102 |
+
});
|
103 |
+
|
104 |
+
export function SessionConfigModel(props: { onClose: () => void }) {
|
105 |
+
const chatStore = useChatStore();
|
106 |
+
const session = chatStore.currentSession();
|
107 |
+
const maskStore = useMaskStore();
|
108 |
+
const navigate = useNavigate();
|
109 |
+
|
110 |
+
const [exporting, setExporting] = useState(false);
|
111 |
+
const isApp = !!getClientConfig()?.isApp;
|
112 |
+
|
113 |
+
const handleExport = async () => {
|
114 |
+
if (exporting) return;
|
115 |
+
setExporting(true);
|
116 |
+
const currentDate = new Date();
|
117 |
+
const currentSession = chatStore.currentSession();
|
118 |
+
const messageCount = currentSession.messages.length;
|
119 |
+
const datePart = isApp
|
120 |
+
? `${currentDate.toLocaleDateString().replace(/\//g, '_')} ${currentDate.toLocaleTimeString().replace(/:/g, '_')}`
|
121 |
+
: `${currentDate.toLocaleString().replace(/:/g, '_')}`;
|
122 |
+
|
123 |
+
const formattedMessageCount = Locale.ChatItem.ChatItemCount(messageCount); // Format the message count using the translation function
|
124 |
+
const fileName = `${session.topic}-(${formattedMessageCount})-${datePart}.json`;
|
125 |
+
await downloadAs(session, fileName);
|
126 |
+
setExporting(false);
|
127 |
+
};
|
128 |
+
|
129 |
+
const importchat = async () => {
|
130 |
+
await readFromFile().then((content) => {
|
131 |
+
try {
|
132 |
+
const importedData = JSON.parse(content);
|
133 |
+
chatStore.updateCurrentSession((session) => {
|
134 |
+
Object.assign(session, importedData);
|
135 |
+
});
|
136 |
+
} catch (e) {
|
137 |
+
console.error("[Import] Failed to import JSON file:", e);
|
138 |
+
showToast(Locale.Settings.Sync.ImportFailed);
|
139 |
+
}
|
140 |
+
});
|
141 |
+
};
|
142 |
+
|
143 |
+
return (
|
144 |
+
<div className="modal-mask">
|
145 |
+
<Modal
|
146 |
+
title={Locale.Context.Edit}
|
147 |
+
onClose={() => props.onClose()}
|
148 |
+
actions={[
|
149 |
+
<IconButton
|
150 |
+
key="export"
|
151 |
+
icon={<DownloadIcon />}
|
152 |
+
bordered
|
153 |
+
text={Locale.UI.Export}
|
154 |
+
onClick={handleExport}
|
155 |
+
disabled={exporting}
|
156 |
+
/>,
|
157 |
+
<IconButton
|
158 |
+
key="import"
|
159 |
+
icon={<UploadIcon />}
|
160 |
+
bordered
|
161 |
+
text={Locale.UI.Import}
|
162 |
+
onClick={importchat}
|
163 |
+
/>,
|
164 |
+
<IconButton
|
165 |
+
key="reset"
|
166 |
+
icon={<ResetIcon />}
|
167 |
+
bordered
|
168 |
+
text={Locale.Chat.Config.Reset}
|
169 |
+
onClick={async () => {
|
170 |
+
if (await showConfirm(Locale.Memory.ResetConfirm)) {
|
171 |
+
chatStore.updateCurrentSession(
|
172 |
+
(session) => (session.memoryPrompt = ""),
|
173 |
+
);
|
174 |
+
}
|
175 |
+
}}
|
176 |
+
/>,
|
177 |
+
<IconButton
|
178 |
+
key="copy"
|
179 |
+
icon={<CopyIcon />}
|
180 |
+
bordered
|
181 |
+
text={Locale.Chat.Config.SaveAs}
|
182 |
+
onClick={() => {
|
183 |
+
navigate(Path.Masks);
|
184 |
+
setTimeout(() => {
|
185 |
+
maskStore.create(session.mask);
|
186 |
+
}, 500);
|
187 |
+
}}
|
188 |
+
/>,
|
189 |
+
]}
|
190 |
+
>
|
191 |
+
<MaskConfig
|
192 |
+
mask={session.mask}
|
193 |
+
updateMask={(updater) => {
|
194 |
+
const mask = { ...session.mask };
|
195 |
+
updater(mask);
|
196 |
+
chatStore.updateCurrentSession((session) => (session.mask = mask));
|
197 |
+
}}
|
198 |
+
shouldSyncFromGlobal
|
199 |
+
extraListItems={
|
200 |
+
session.mask.modelConfig.sendMemory ? (
|
201 |
+
<ListItem
|
202 |
+
className="copyable"
|
203 |
+
title={`${Locale.Memory.Title} (${session.lastSummarizeIndex} of ${session.messages.length})`}
|
204 |
+
subTitle={session.memoryPrompt || Locale.Memory.EmptyContent}
|
205 |
+
></ListItem>
|
206 |
+
) : (
|
207 |
+
<></>
|
208 |
+
)
|
209 |
+
}
|
210 |
+
></MaskConfig>
|
211 |
+
</Modal>
|
212 |
+
</div>
|
213 |
+
);
|
214 |
+
}
|
215 |
+
|
216 |
+
function PromptToast(props: {
|
217 |
+
showToast?: boolean;
|
218 |
+
showModal?: boolean;
|
219 |
+
setShowModal: (_: boolean) => void;
|
220 |
+
}) {
|
221 |
+
const chatStore = useChatStore();
|
222 |
+
const session = chatStore.currentSession();
|
223 |
+
const context = session.mask.context;
|
224 |
+
|
225 |
+
return (
|
226 |
+
<div className={styles["prompt-toast"]} key="prompt-toast">
|
227 |
+
{props.showToast && (
|
228 |
+
<div
|
229 |
+
className={styles["prompt-toast-inner"] + " clickable"}
|
230 |
+
role="button"
|
231 |
+
onClick={() => props.setShowModal(true)}
|
232 |
+
>
|
233 |
+
<BrainIcon />
|
234 |
+
<span className={styles["prompt-toast-content"]}>
|
235 |
+
{Locale.Context.Toast(context.length)}
|
236 |
+
</span>
|
237 |
+
</div>
|
238 |
+
)}
|
239 |
+
{props.showModal && (
|
240 |
+
<SessionConfigModel onClose={() => props.setShowModal(false)} />
|
241 |
+
)}
|
242 |
+
</div>
|
243 |
+
);
|
244 |
+
}
|
245 |
+
|
246 |
+
function useSubmitHandler() {
|
247 |
+
const config = useAppConfig();
|
248 |
+
const submitKey = config.submitKey;
|
249 |
+
const isComposing = useRef(false);
|
250 |
+
|
251 |
+
useEffect(() => {
|
252 |
+
const onCompositionStart = () => {
|
253 |
+
isComposing.current = true;
|
254 |
+
};
|
255 |
+
const onCompositionEnd = () => {
|
256 |
+
isComposing.current = false;
|
257 |
+
};
|
258 |
+
|
259 |
+
window.addEventListener("compositionstart", onCompositionStart);
|
260 |
+
window.addEventListener("compositionend", onCompositionEnd);
|
261 |
+
|
262 |
+
return () => {
|
263 |
+
window.removeEventListener("compositionstart", onCompositionStart);
|
264 |
+
window.removeEventListener("compositionend", onCompositionEnd);
|
265 |
+
};
|
266 |
+
}, []);
|
267 |
+
|
268 |
+
const shouldSubmit = (e: React.KeyboardEvent<HTMLTextAreaElement>) => {
|
269 |
+
if (e.key !== "Enter") return false;
|
270 |
+
if (e.key === "Enter" && (e.nativeEvent.isComposing || isComposing.current))
|
271 |
+
return false;
|
272 |
+
return (
|
273 |
+
(config.submitKey === SubmitKey.AltEnter && e.altKey) ||
|
274 |
+
(config.submitKey === SubmitKey.CtrlEnter && e.ctrlKey) ||
|
275 |
+
(config.submitKey === SubmitKey.ShiftEnter && e.shiftKey) ||
|
276 |
+
(config.submitKey === SubmitKey.MetaEnter && e.metaKey) ||
|
277 |
+
(config.submitKey === SubmitKey.Enter &&
|
278 |
+
!e.altKey &&
|
279 |
+
!e.ctrlKey &&
|
280 |
+
!e.shiftKey &&
|
281 |
+
!e.metaKey)
|
282 |
+
);
|
283 |
+
};
|
284 |
+
|
285 |
+
return {
|
286 |
+
submitKey,
|
287 |
+
shouldSubmit,
|
288 |
+
};
|
289 |
+
}
|
290 |
+
|
291 |
+
export type RenderPompt = Pick<Prompt, "title" | "content">;
|
292 |
+
|
293 |
+
export function PromptHints(props: {
|
294 |
+
prompts: RenderPompt[];
|
295 |
+
onPromptSelect: (prompt: RenderPompt) => void;
|
296 |
+
}) {
|
297 |
+
const noPrompts = props.prompts.length === 0;
|
298 |
+
const [selectIndex, setSelectIndex] = useState(0);
|
299 |
+
const selectedRef = useRef<HTMLDivElement>(null);
|
300 |
+
|
301 |
+
useEffect(() => {
|
302 |
+
setSelectIndex(0);
|
303 |
+
}, [props.prompts.length]);
|
304 |
+
|
305 |
+
useEffect(() => {
|
306 |
+
const onKeyDown = (e: KeyboardEvent) => {
|
307 |
+
if (noPrompts || e.metaKey || e.altKey || e.ctrlKey) {
|
308 |
+
return;
|
309 |
+
}
|
310 |
+
// arrow up / down to select prompt
|
311 |
+
const changeIndex = (delta: number) => {
|
312 |
+
e.stopPropagation();
|
313 |
+
e.preventDefault();
|
314 |
+
const nextIndex = Math.max(
|
315 |
+
0,
|
316 |
+
Math.min(props.prompts.length - 1, selectIndex + delta),
|
317 |
+
);
|
318 |
+
setSelectIndex(nextIndex);
|
319 |
+
selectedRef.current?.scrollIntoView({
|
320 |
+
block: "center",
|
321 |
+
});
|
322 |
+
};
|
323 |
+
|
324 |
+
if (e.key === "ArrowUp") {
|
325 |
+
changeIndex(1);
|
326 |
+
} else if (e.key === "ArrowDown") {
|
327 |
+
changeIndex(-1);
|
328 |
+
} else if (e.key === "Enter") {
|
329 |
+
const selectedPrompt = props.prompts.at(selectIndex);
|
330 |
+
if (selectedPrompt) {
|
331 |
+
props.onPromptSelect(selectedPrompt);
|
332 |
+
}
|
333 |
+
}
|
334 |
+
};
|
335 |
+
|
336 |
+
window.addEventListener("keydown", onKeyDown);
|
337 |
+
|
338 |
+
return () => window.removeEventListener("keydown", onKeyDown);
|
339 |
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
340 |
+
}, [props.prompts.length, selectIndex]);
|
341 |
+
|
342 |
+
if (noPrompts) return null;
|
343 |
+
return (
|
344 |
+
<div className={styles["prompt-hints"]}>
|
345 |
+
{props.prompts.map((prompt, i) => (
|
346 |
+
<div
|
347 |
+
ref={i === selectIndex ? selectedRef : null}
|
348 |
+
className={
|
349 |
+
styles["prompt-hint"] +
|
350 |
+
` ${i === selectIndex ? styles["prompt-hint-selected"] : ""}`
|
351 |
+
}
|
352 |
+
key={prompt.title + i.toString()}
|
353 |
+
onClick={() => props.onPromptSelect(prompt)}
|
354 |
+
onMouseEnter={() => setSelectIndex(i)}
|
355 |
+
>
|
356 |
+
<div className={styles["hint-title"]}>{prompt.title}</div>
|
357 |
+
<div className={styles["hint-content"]}>{prompt.content}</div>
|
358 |
+
</div>
|
359 |
+
))}
|
360 |
+
</div>
|
361 |
+
);
|
362 |
+
}
|
363 |
+
|
364 |
+
function ClearContextDivider() {
|
365 |
+
const chatStore = useChatStore();
|
366 |
+
|
367 |
+
return (
|
368 |
+
<div
|
369 |
+
className={styles["clear-context"]}
|
370 |
+
onClick={() =>
|
371 |
+
chatStore.updateCurrentSession(
|
372 |
+
(session) => (session.clearContextIndex = undefined),
|
373 |
+
)
|
374 |
+
}
|
375 |
+
>
|
376 |
+
<div className={styles["clear-context-tips"]}>{Locale.Context.Clear}</div>
|
377 |
+
<div className={styles["clear-context-revert-btn"]}>
|
378 |
+
{Locale.Context.Revert}
|
379 |
+
</div>
|
380 |
+
</div>
|
381 |
+
);
|
382 |
+
}
|
383 |
+
|
384 |
+
function ChatAction(props: {
|
385 |
+
text: string;
|
386 |
+
icon: JSX.Element;
|
387 |
+
onClick: () => void;
|
388 |
+
}) {
|
389 |
+
const iconRef = useRef<HTMLDivElement>(null);
|
390 |
+
const textRef = useRef<HTMLDivElement>(null);
|
391 |
+
const [width, setWidth] = useState({
|
392 |
+
full: 16,
|
393 |
+
icon: 16,
|
394 |
+
});
|
395 |
+
|
396 |
+
function updateWidth() {
|
397 |
+
if (!iconRef.current || !textRef.current) return;
|
398 |
+
const getWidth = (dom: HTMLDivElement) => dom.getBoundingClientRect().width;
|
399 |
+
const textWidth = getWidth(textRef.current);
|
400 |
+
const iconWidth = getWidth(iconRef.current);
|
401 |
+
setWidth({
|
402 |
+
full: textWidth + iconWidth,
|
403 |
+
icon: iconWidth,
|
404 |
+
});
|
405 |
+
}
|
406 |
+
|
407 |
+
return (
|
408 |
+
<div
|
409 |
+
className={`${styles["chat-input-action"]} clickable`}
|
410 |
+
onClick={() => {
|
411 |
+
props.onClick();
|
412 |
+
setTimeout(updateWidth, 1);
|
413 |
+
}}
|
414 |
+
onMouseEnter={updateWidth}
|
415 |
+
onTouchStart={updateWidth}
|
416 |
+
style={
|
417 |
+
{
|
418 |
+
"--icon-width": `${width.icon}px`,
|
419 |
+
"--full-width": `${width.full}px`,
|
420 |
+
} as React.CSSProperties
|
421 |
+
}
|
422 |
+
>
|
423 |
+
<div ref={iconRef} className={styles["icon"]}>
|
424 |
+
{props.icon}
|
425 |
+
</div>
|
426 |
+
<div className={styles["text"]} ref={textRef}>
|
427 |
+
{props.text}
|
428 |
+
</div>
|
429 |
+
</div>
|
430 |
+
);
|
431 |
+
}
|
432 |
+
|
433 |
+
function useScrollToBottom() {
|
434 |
+
// for auto-scroll
|
435 |
+
const scrollRef = useRef<HTMLDivElement>(null);
|
436 |
+
const [autoScroll, setAutoScroll] = useState(true);
|
437 |
+
|
438 |
+
function scrollDomToBottom() {
|
439 |
+
const dom = scrollRef.current;
|
440 |
+
if (dom) {
|
441 |
+
requestAnimationFrame(() => {
|
442 |
+
setAutoScroll(true);
|
443 |
+
dom.scrollTo(0, dom.scrollHeight);
|
444 |
+
});
|
445 |
+
}
|
446 |
+
}
|
447 |
+
|
448 |
+
// auto scroll
|
449 |
+
useEffect(() => {
|
450 |
+
if (autoScroll) {
|
451 |
+
scrollDomToBottom();
|
452 |
+
}
|
453 |
+
});
|
454 |
+
|
455 |
+
return {
|
456 |
+
scrollRef,
|
457 |
+
autoScroll,
|
458 |
+
setAutoScroll,
|
459 |
+
scrollDomToBottom,
|
460 |
+
};
|
461 |
+
}
|
462 |
+
|
463 |
+
export function ChatActions(props: {
|
464 |
+
showPromptModal: () => void;
|
465 |
+
scrollToBottom: () => void;
|
466 |
+
showPromptHints: () => void;
|
467 |
+
hitBottom: boolean;
|
468 |
+
showContextPrompts: boolean;
|
469 |
+
toggleContextPrompts: () => void;
|
470 |
+
}) {
|
471 |
+
const config = useAppConfig();
|
472 |
+
const navigate = useNavigate();
|
473 |
+
const chatStore = useChatStore();
|
474 |
+
|
475 |
+
// switch themes
|
476 |
+
const theme = config.theme;
|
477 |
+
function nextTheme() {
|
478 |
+
const themes = [Theme.Auto, Theme.Light, Theme.Dark];
|
479 |
+
const themeIndex = themes.indexOf(theme);
|
480 |
+
const nextIndex = (themeIndex + 1) % themes.length;
|
481 |
+
const nextTheme = themes[nextIndex];
|
482 |
+
config.update((config) => (config.theme = nextTheme));
|
483 |
+
}
|
484 |
+
|
485 |
+
// stop all responses
|
486 |
+
const couldStop = ChatControllerPool.hasPending();
|
487 |
+
const stopAll = () => ChatControllerPool.stopAll();
|
488 |
+
|
489 |
+
// switch model
|
490 |
+
const currentModel = chatStore.currentSession().mask.modelConfig.model;
|
491 |
+
const models = useAllModels()
|
492 |
+
.filter((m) => m.available)
|
493 |
+
.map((m) => m.name);
|
494 |
+
const [showModelSelector, setShowModelSelector] = useState(false);
|
495 |
+
|
496 |
+
return (
|
497 |
+
<div className={styles["chat-input-actions"]}>
|
498 |
+
{couldStop && (
|
499 |
+
<ChatAction
|
500 |
+
onClick={stopAll}
|
501 |
+
text={Locale.Chat.InputActions.Stop}
|
502 |
+
icon={<StopIcon />}
|
503 |
+
/>
|
504 |
+
)}
|
505 |
+
{!props.hitBottom && (
|
506 |
+
<ChatAction
|
507 |
+
onClick={props.scrollToBottom}
|
508 |
+
text={Locale.Chat.InputActions.ToBottom}
|
509 |
+
icon={<BottomIcon />}
|
510 |
+
/>
|
511 |
+
)}
|
512 |
+
{props.hitBottom && (
|
513 |
+
<ChatAction
|
514 |
+
onClick={props.showPromptModal}
|
515 |
+
text={Locale.Chat.InputActions.Settings}
|
516 |
+
icon={<SettingsIcon />}
|
517 |
+
/>
|
518 |
+
)}
|
519 |
+
|
520 |
+
<ChatAction
|
521 |
+
onClick={nextTheme}
|
522 |
+
text={Locale.Chat.InputActions.Theme[theme]}
|
523 |
+
icon={
|
524 |
+
<>
|
525 |
+
{theme === Theme.Auto ? (
|
526 |
+
<AutoIcon />
|
527 |
+
) : theme === Theme.Light ? (
|
528 |
+
<LightIcon />
|
529 |
+
) : theme === Theme.Dark ? (
|
530 |
+
<DarkIcon />
|
531 |
+
) : null}
|
532 |
+
</>
|
533 |
+
}
|
534 |
+
/>
|
535 |
+
|
536 |
+
<ChatAction
|
537 |
+
onClick={props.showPromptHints}
|
538 |
+
text={Locale.Chat.InputActions.Prompt}
|
539 |
+
icon={<PromptIcon />}
|
540 |
+
/>
|
541 |
+
|
542 |
+
<ChatAction
|
543 |
+
onClick={props.toggleContextPrompts}
|
544 |
+
text={
|
545 |
+
props.showContextPrompts
|
546 |
+
? Locale.Mask.Config.HideContext.UnHide
|
547 |
+
: Locale.Mask.Config.HideContext.Hide
|
548 |
+
}
|
549 |
+
icon={
|
550 |
+
props.showContextPrompts ? (
|
551 |
+
<EyeOffIcon />
|
552 |
+
) : (
|
553 |
+
<EyeOnIcon />
|
554 |
+
)
|
555 |
+
}
|
556 |
+
/>
|
557 |
+
|
558 |
+
<ChatAction
|
559 |
+
onClick={() => {
|
560 |
+
navigate(Path.Masks);
|
561 |
+
}}
|
562 |
+
text={Locale.Chat.InputActions.Masks}
|
563 |
+
icon={<MaskIcon />}
|
564 |
+
/>
|
565 |
+
|
566 |
+
<ChatAction
|
567 |
+
text={Locale.Chat.InputActions.Clear}
|
568 |
+
icon={<BreakIcon />}
|
569 |
+
onClick={() => {
|
570 |
+
chatStore.updateCurrentSession((session) => {
|
571 |
+
if (session.clearContextIndex === session.messages.length) {
|
572 |
+
session.clearContextIndex = undefined;
|
573 |
+
} else {
|
574 |
+
session.clearContextIndex = session.messages.length;
|
575 |
+
session.memoryPrompt = ""; // will clear memory
|
576 |
+
}
|
577 |
+
});
|
578 |
+
}}
|
579 |
+
/>
|
580 |
+
|
581 |
+
<ChatAction
|
582 |
+
onClick={() => setShowModelSelector(true)}
|
583 |
+
text={currentModel}
|
584 |
+
icon={<RobotIcon />}
|
585 |
+
/>
|
586 |
+
|
587 |
+
{showModelSelector && (
|
588 |
+
<Selector
|
589 |
+
defaultSelectedValue={currentModel}
|
590 |
+
items={models.map((m) => ({
|
591 |
+
title: m,
|
592 |
+
value: m,
|
593 |
+
}))}
|
594 |
+
onClose={() => setShowModelSelector(false)}
|
595 |
+
onSelection={(s) => {
|
596 |
+
if (s.length === 0) return;
|
597 |
+
chatStore.updateCurrentSession((session) => {
|
598 |
+
session.mask.modelConfig.model = s[0] as ModelType;
|
599 |
+
session.mask.syncGlobalConfig = false;
|
600 |
+
});
|
601 |
+
showToast(s[0]);
|
602 |
+
}}
|
603 |
+
/>
|
604 |
+
)}
|
605 |
+
</div>
|
606 |
+
);
|
607 |
+
}
|
608 |
+
|
609 |
+
export function EditMessageModal(props: { onClose: () => void }) {
|
610 |
+
const chatStore = useChatStore();
|
611 |
+
const session = chatStore.currentSession();
|
612 |
+
const [messages, setMessages] = useState(session.messages.slice());
|
613 |
+
|
614 |
+
return (
|
615 |
+
<div className="modal-mask">
|
616 |
+
<Modal
|
617 |
+
title={Locale.Chat.EditMessage.Title}
|
618 |
+
onClose={props.onClose}
|
619 |
+
actions={[
|
620 |
+
<IconButton
|
621 |
+
text={Locale.UI.Cancel}
|
622 |
+
icon={<CancelIcon />}
|
623 |
+
key="cancel"
|
624 |
+
onClick={() => {
|
625 |
+
props.onClose();
|
626 |
+
}}
|
627 |
+
/>,
|
628 |
+
<IconButton
|
629 |
+
type="primary"
|
630 |
+
text={Locale.UI.Confirm}
|
631 |
+
icon={<ConfirmIcon />}
|
632 |
+
key="ok"
|
633 |
+
onClick={() => {
|
634 |
+
chatStore.updateCurrentSession(
|
635 |
+
(session) => (session.messages = messages),
|
636 |
+
);
|
637 |
+
props.onClose();
|
638 |
+
}}
|
639 |
+
/>,
|
640 |
+
]}
|
641 |
+
>
|
642 |
+
<List>
|
643 |
+
<ListItem
|
644 |
+
title={Locale.Chat.EditMessage.Topic.Title}
|
645 |
+
subTitle={Locale.Chat.EditMessage.Topic.SubTitle}
|
646 |
+
>
|
647 |
+
<input
|
648 |
+
type="text"
|
649 |
+
value={session.topic}
|
650 |
+
onInput={(e) =>
|
651 |
+
chatStore.updateCurrentSession(
|
652 |
+
(session) => (session.topic = e.currentTarget.value),
|
653 |
+
)
|
654 |
+
}
|
655 |
+
></input>
|
656 |
+
</ListItem>
|
657 |
+
</List>
|
658 |
+
<ContextPrompts
|
659 |
+
context={messages}
|
660 |
+
updateContext={(updater) => {
|
661 |
+
const newMessages = messages.slice();
|
662 |
+
updater(newMessages);
|
663 |
+
setMessages(newMessages);
|
664 |
+
}}
|
665 |
+
/>
|
666 |
+
</Modal>
|
667 |
+
</div>
|
668 |
+
);
|
669 |
+
}
|
670 |
+
|
671 |
+
function _Chat() {
|
672 |
+
type RenderMessage = ChatMessage & { preview?: boolean };
|
673 |
+
|
674 |
+
const chatStore = useChatStore();
|
675 |
+
const session = chatStore.currentSession();
|
676 |
+
const config = useAppConfig();
|
677 |
+
const fontSize = config.fontSize;
|
678 |
+
|
679 |
+
const [showExport, setShowExport] = useState(false);
|
680 |
+
|
681 |
+
const inputRef = useRef<HTMLTextAreaElement>(null);
|
682 |
+
const [userInput, setUserInput] = useState("");
|
683 |
+
const [isLoading, setIsLoading] = useState(false);
|
684 |
+
const { submitKey, shouldSubmit } = useSubmitHandler();
|
685 |
+
const { scrollRef, setAutoScroll, scrollDomToBottom } = useScrollToBottom();
|
686 |
+
const [hitBottom, setHitBottom] = useState(true);
|
687 |
+
const isMobileScreen = useMobileScreen();
|
688 |
+
const navigate = useNavigate();
|
689 |
+
|
690 |
+
// prompt hints
|
691 |
+
const promptStore = usePromptStore();
|
692 |
+
const [promptHints, setPromptHints] = useState<RenderPompt[]>([]);
|
693 |
+
const onSearch = useDebouncedCallback(
|
694 |
+
(text: string) => {
|
695 |
+
const matchedPrompts = promptStore.search(text);
|
696 |
+
setPromptHints(matchedPrompts);
|
697 |
+
},
|
698 |
+
100,
|
699 |
+
{ leading: true, trailing: true },
|
700 |
+
);
|
701 |
+
|
702 |
+
// auto grow input
|
703 |
+
const [inputRows, setInputRows] = useState(2);
|
704 |
+
const measure = useDebouncedCallback(
|
705 |
+
() => {
|
706 |
+
const rows = inputRef.current ? autoGrowTextArea(inputRef.current) : 1;
|
707 |
+
const inputRows = Math.min(
|
708 |
+
20,
|
709 |
+
Math.max(2 + Number(!isMobileScreen), rows),
|
710 |
+
);
|
711 |
+
setInputRows(inputRows);
|
712 |
+
},
|
713 |
+
100,
|
714 |
+
{
|
715 |
+
leading: true,
|
716 |
+
trailing: true,
|
717 |
+
},
|
718 |
+
);
|
719 |
+
|
720 |
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
721 |
+
useEffect(measure, [userInput]);
|
722 |
+
|
723 |
+
const loadchat = () => {
|
724 |
+
readFromFile().then((content) => {
|
725 |
+
try {
|
726 |
+
const importedData = JSON.parse(content);
|
727 |
+
chatStore.updateCurrentSession((session) => {
|
728 |
+
Object.assign(session, importedData);
|
729 |
+
// Set any other properties you want to update in the session
|
730 |
+
});
|
731 |
+
} catch (e) {
|
732 |
+
console.error("[Import] Failed to import JSON file:", e);
|
733 |
+
showToast(Locale.Settings.Sync.ImportFailed);
|
734 |
+
}
|
735 |
+
});
|
736 |
+
};
|
737 |
+
|
738 |
+
// chat commands shortcuts
|
739 |
+
const chatCommands = useChatCommand({
|
740 |
+
new: () => chatStore.newSession(),
|
741 |
+
newm: () => navigate(Path.NewChat),
|
742 |
+
prev: () => chatStore.nextSession(-1),
|
743 |
+
next: () => chatStore.nextSession(1),
|
744 |
+
restart: () => window.__TAURI__?.process.relaunch(),
|
745 |
+
clear: () =>
|
746 |
+
chatStore.updateCurrentSession(
|
747 |
+
(session) => (session.clearContextIndex = session.messages.length),
|
748 |
+
),
|
749 |
+
del: () => chatStore.deleteSession(chatStore.currentSessionIndex),
|
750 |
+
save: () =>
|
751 |
+
downloadAs((session), `${session.topic}.json`),
|
752 |
+
load: loadchat,
|
753 |
+
copymemoryai: () => {
|
754 |
+
const memoryPrompt = chatStore.currentSession().memoryPrompt;
|
755 |
+
if (memoryPrompt.trim() !== "") {
|
756 |
+
copyToClipboard(memoryPrompt);
|
757 |
+
showToast(Locale.Copy.Success);
|
758 |
+
} else {
|
759 |
+
showToast(Locale.Copy.Failed);
|
760 |
+
}
|
761 |
+
},
|
762 |
+
updatemasks: () => {
|
763 |
+
chatStore.updateCurrentSession((session) => {
|
764 |
+
const memoryPrompt = session.memoryPrompt;
|
765 |
+
const currentDate = new Date().toLocaleString(); // Get the current date and time as a string
|
766 |
+
const existingContext = session.mask.context;
|
767 |
+
let currentContext = existingContext[0]; // Get the current context message
|
768 |
+
|
769 |
+
if (!currentContext || currentContext.role !== "system") {
|
770 |
+
// If the current context message doesn't exist or doesn't have the role "system"
|
771 |
+
currentContext = {
|
772 |
+
role: "system",
|
773 |
+
content: memoryPrompt,
|
774 |
+
date: currentDate,
|
775 |
+
id: "", // Generate or set the ID for the new message
|
776 |
+
// Add any other properties you want to set for the context messages
|
777 |
+
};
|
778 |
+
existingContext.unshift(currentContext); // Add the new message at the beginning of the context array
|
779 |
+
showToast(Locale.Chat.Commands.UI.MasksSuccess);
|
780 |
+
} else {
|
781 |
+
// If the current context message already exists and has the role "system"
|
782 |
+
currentContext.content = memoryPrompt; // Update the content
|
783 |
+
currentContext.date = currentDate; // Update the date
|
784 |
+
// You can update other properties of the current context message here
|
785 |
+
}
|
786 |
+
|
787 |
+
// Set any other properties you want to update in the session
|
788 |
+
session.mask.context = existingContext;
|
789 |
+
showToast(Locale.Chat.Commands.UI.MasksSuccess);
|
790 |
+
});
|
791 |
+
},
|
792 |
+
// Currently the feature to summarize a conversation as manual is not yet enabled.
|
793 |
+
summarize: () => chatStore.summarizeSession(),
|
794 |
+
});
|
795 |
+
|
796 |
+
// only search prompts when user input is short
|
797 |
+
const SEARCH_TEXT_LIMIT = 30;
|
798 |
+
const onInput = (text: string) => {
|
799 |
+
setUserInput(text);
|
800 |
+
const n = text.trim().length;
|
801 |
+
|
802 |
+
// clear search results
|
803 |
+
if (n === 0) {
|
804 |
+
setPromptHints([]);
|
805 |
+
} else if (text.startsWith(ChatCommandPrefix)) {
|
806 |
+
setPromptHints(chatCommands.search(text));
|
807 |
+
} else if (!config.disablePromptHint && n < SEARCH_TEXT_LIMIT) {
|
808 |
+
// check if need to trigger auto completion
|
809 |
+
if (text.startsWith("/")) {
|
810 |
+
let searchText = text.slice(1);
|
811 |
+
onSearch(searchText);
|
812 |
+
}
|
813 |
+
}
|
814 |
+
};
|
815 |
+
|
816 |
+
const doSubmit = (userInput: string) => {
|
817 |
+
if (userInput.trim() === "") return;
|
818 |
+
|
819 |
+
// reduce a zod cve CVE-2023-4316
|
820 |
+
const escapedInput = escapeRegExp(userInput);
|
821 |
+
|
822 |
+
const matchCommand = chatCommands.match(escapedInput);
|
823 |
+
|
824 |
+
if (matchCommand.matched) {
|
825 |
+
setUserInput("");
|
826 |
+
setPromptHints([]);
|
827 |
+
matchCommand.invoke();
|
828 |
+
return;
|
829 |
+
}
|
830 |
+
setIsLoading(true);
|
831 |
+
chatStore.onUserInput(userInput).then(() => setIsLoading(false));
|
832 |
+
localStorage.setItem(LAST_INPUT_KEY, userInput);
|
833 |
+
setUserInput("");
|
834 |
+
setPromptHints([]);
|
835 |
+
if (!isMobileScreen) inputRef.current?.focus();
|
836 |
+
setAutoScroll(true);
|
837 |
+
};
|
838 |
+
|
839 |
+
const onPromptSelect = (prompt: RenderPompt) => {
|
840 |
+
setTimeout(() => {
|
841 |
+
setPromptHints([]);
|
842 |
+
|
843 |
+
const matchedChatCommand = chatCommands.match(prompt.content);
|
844 |
+
if (matchedChatCommand.matched) {
|
845 |
+
// if user is selecting a chat command, just trigger it
|
846 |
+
matchedChatCommand.invoke();
|
847 |
+
setUserInput("");
|
848 |
+
} else {
|
849 |
+
// or fill the prompt
|
850 |
+
setUserInput(prompt.content);
|
851 |
+
}
|
852 |
+
inputRef.current?.focus();
|
853 |
+
}, 30);
|
854 |
+
};
|
855 |
+
|
856 |
+
// stop response
|
857 |
+
const onUserStop = (messageId: string) => {
|
858 |
+
ChatControllerPool.stop(session.id, messageId);
|
859 |
+
};
|
860 |
+
|
861 |
+
useEffect(() => {
|
862 |
+
chatStore.updateCurrentSession((session) => {
|
863 |
+
const stopTiming = Date.now() - REQUEST_TIMEOUT_MS;
|
864 |
+
session.messages.forEach((m) => {
|
865 |
+
// check if should stop all stale messages
|
866 |
+
if (m.isError || new Date(m.date).getTime() < stopTiming) {
|
867 |
+
if (m.streaming) {
|
868 |
+
m.streaming = false;
|
869 |
+
}
|
870 |
+
|
871 |
+
if (m.content.length === 0) {
|
872 |
+
m.isError = true;
|
873 |
+
m.content = prettyObject({
|
874 |
+
error: true,
|
875 |
+
message: "empty response",
|
876 |
+
});
|
877 |
+
}
|
878 |
+
}
|
879 |
+
});
|
880 |
+
|
881 |
+
// auto sync mask config from global config
|
882 |
+
if (session.mask.syncGlobalConfig) {
|
883 |
+
console.log("[Mask] syncing from global, name = ", session.mask.name);
|
884 |
+
session.mask.modelConfig = { ...config.modelConfig };
|
885 |
+
}
|
886 |
+
});
|
887 |
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
888 |
+
}, []);
|
889 |
+
|
890 |
+
// check if should send message
|
891 |
+
const onInputKeyDown = (e: React.KeyboardEvent<HTMLTextAreaElement>) => {
|
892 |
+
// if ArrowUp and no userInput, fill with last input
|
893 |
+
if (
|
894 |
+
e.key === "ArrowUp" &&
|
895 |
+
userInput.length <= 0 &&
|
896 |
+
!(e.metaKey || e.altKey || e.ctrlKey)
|
897 |
+
) {
|
898 |
+
setUserInput(localStorage.getItem(LAST_INPUT_KEY) ?? "");
|
899 |
+
e.preventDefault();
|
900 |
+
return;
|
901 |
+
}
|
902 |
+
if (shouldSubmit(e) && promptHints.length === 0) {
|
903 |
+
doSubmit(userInput);
|
904 |
+
e.preventDefault();
|
905 |
+
}
|
906 |
+
};
|
907 |
+
const onRightClick = (e: any, message: ChatMessage) => {
|
908 |
+
// copy to clipboard
|
909 |
+
if (selectOrCopy(e.currentTarget, message.content)) {
|
910 |
+
if (userInput.length === 0) {
|
911 |
+
setUserInput(message.content);
|
912 |
+
}
|
913 |
+
|
914 |
+
e.preventDefault();
|
915 |
+
}
|
916 |
+
};
|
917 |
+
|
918 |
+
const deleteMessage = (msgId?: string) => {
|
919 |
+
chatStore.updateCurrentSession(
|
920 |
+
(session) =>
|
921 |
+
(session.messages = session.messages.filter((m) => m.id !== msgId)),
|
922 |
+
);
|
923 |
+
};
|
924 |
+
|
925 |
+
const onDelete = (msgId: string) => {
|
926 |
+
deleteMessage(msgId);
|
927 |
+
};
|
928 |
+
|
929 |
+
const onResend = (message: ChatMessage) => {
|
930 |
+
// when it is resending a message
|
931 |
+
// 1. for a user's message, find the next bot response
|
932 |
+
// 2. for a bot's message, find the last user's input
|
933 |
+
// 3. delete original user input and bot's message
|
934 |
+
// 4. resend the user's input
|
935 |
+
|
936 |
+
const resendingIndex = session.messages.findIndex(
|
937 |
+
(m) => m.id === message.id,
|
938 |
+
);
|
939 |
+
|
940 |
+
if (resendingIndex < 0 || resendingIndex >= session.messages.length) {
|
941 |
+
console.error("[Chat] failed to find resending message", message);
|
942 |
+
return;
|
943 |
+
}
|
944 |
+
|
945 |
+
let userMessage: ChatMessage | undefined;
|
946 |
+
let botMessage: ChatMessage | undefined;
|
947 |
+
|
948 |
+
if (message.role === "assistant") {
|
949 |
+
// if it is resending a bot's message, find the user input for it
|
950 |
+
botMessage = message;
|
951 |
+
for (let i = resendingIndex; i >= 0; i -= 1) {
|
952 |
+
if (session.messages[i].role === "user") {
|
953 |
+
userMessage = session.messages[i];
|
954 |
+
break;
|
955 |
+
}
|
956 |
+
}
|
957 |
+
} else if (message.role === "user") {
|
958 |
+
// if it is resending a user's input, find the bot's response
|
959 |
+
userMessage = message;
|
960 |
+
for (let i = resendingIndex; i < session.messages.length; i += 1) {
|
961 |
+
if (session.messages[i].role === "assistant") {
|
962 |
+
botMessage = session.messages[i];
|
963 |
+
break;
|
964 |
+
}
|
965 |
+
}
|
966 |
+
}
|
967 |
+
|
968 |
+
if (userMessage === undefined) {
|
969 |
+
console.error("[Chat] failed to resend", message);
|
970 |
+
return;
|
971 |
+
}
|
972 |
+
|
973 |
+
// delete the original messages
|
974 |
+
deleteMessage(userMessage.id);
|
975 |
+
deleteMessage(botMessage?.id);
|
976 |
+
|
977 |
+
// resend the message
|
978 |
+
setIsLoading(true);
|
979 |
+
chatStore.onUserInput(userMessage.content).then(() => setIsLoading(false));
|
980 |
+
inputRef.current?.focus();
|
981 |
+
};
|
982 |
+
|
983 |
+
const onPinMessage = (message: ChatMessage) => {
|
984 |
+
chatStore.updateCurrentSession((session) =>
|
985 |
+
session.mask.context.push(message),
|
986 |
+
);
|
987 |
+
|
988 |
+
showToast(Locale.Chat.Actions.PinToastContent, {
|
989 |
+
text: Locale.Chat.Actions.PinToastAction,
|
990 |
+
onClick: () => {
|
991 |
+
setShowPromptModal(true);
|
992 |
+
},
|
993 |
+
});
|
994 |
+
};
|
995 |
+
|
996 |
+
const accessStore = useAccessStore();
|
997 |
+
const isAuthorized = accessStore.isAuthorized();
|
998 |
+
const context: RenderMessage[] = useMemo(() => {
|
999 |
+
const contextMessages = session.mask.hideContext ? [] : session.mask.context.slice();
|
1000 |
+
|
1001 |
+
if (
|
1002 |
+
contextMessages.length === 0 &&
|
1003 |
+
session.messages.at(0)?.role !== "system"
|
1004 |
+
) {
|
1005 |
+
const copiedHello = Object.assign({}, BOT_HELLO);
|
1006 |
+
if (!isAuthorized) {
|
1007 |
+
copiedHello.role = "system";
|
1008 |
+
copiedHello.content = Locale.Error.Unauthorized;
|
1009 |
+
}
|
1010 |
+
contextMessages.push(copiedHello);
|
1011 |
+
}
|
1012 |
+
|
1013 |
+
return contextMessages;
|
1014 |
+
}, [session.mask.context, session.mask.hideContext, session.messages, isAuthorized]);
|
1015 |
+
|
1016 |
+
// preview messages
|
1017 |
+
const renderMessages = useMemo(() => {
|
1018 |
+
return context
|
1019 |
+
.concat(session.messages as RenderMessage[])
|
1020 |
+
.concat(
|
1021 |
+
isLoading
|
1022 |
+
? [
|
1023 |
+
{
|
1024 |
+
...createMessage({
|
1025 |
+
role: "assistant",
|
1026 |
+
content: "……",
|
1027 |
+
}),
|
1028 |
+
preview: true,
|
1029 |
+
},
|
1030 |
+
]
|
1031 |
+
: [],
|
1032 |
+
)
|
1033 |
+
.concat(
|
1034 |
+
userInput.length > 0 && config.sendPreviewBubble
|
1035 |
+
? [
|
1036 |
+
{
|
1037 |
+
...createMessage({
|
1038 |
+
role: "user",
|
1039 |
+
content: userInput,
|
1040 |
+
}),
|
1041 |
+
preview: true,
|
1042 |
+
},
|
1043 |
+
]
|
1044 |
+
: [],
|
1045 |
+
);
|
1046 |
+
}, [
|
1047 |
+
config.sendPreviewBubble,
|
1048 |
+
context,
|
1049 |
+
isLoading,
|
1050 |
+
session.messages,
|
1051 |
+
userInput,
|
1052 |
+
]);
|
1053 |
+
|
1054 |
+
const [msgRenderIndex, _setMsgRenderIndex] = useState(
|
1055 |
+
Math.max(0, renderMessages.length - CHAT_PAGE_SIZE),
|
1056 |
+
);
|
1057 |
+
function setMsgRenderIndex(newIndex: number) {
|
1058 |
+
newIndex = Math.min(renderMessages.length - CHAT_PAGE_SIZE, newIndex);
|
1059 |
+
newIndex = Math.max(0, newIndex);
|
1060 |
+
_setMsgRenderIndex(newIndex);
|
1061 |
+
}
|
1062 |
+
|
1063 |
+
const messages = useMemo(() => {
|
1064 |
+
const endRenderIndex = Math.min(
|
1065 |
+
msgRenderIndex + 3 * CHAT_PAGE_SIZE,
|
1066 |
+
renderMessages.length,
|
1067 |
+
);
|
1068 |
+
return renderMessages.slice(msgRenderIndex, endRenderIndex);
|
1069 |
+
}, [msgRenderIndex, renderMessages]);
|
1070 |
+
|
1071 |
+
const onChatBodyScroll = (e: HTMLElement) => {
|
1072 |
+
const bottomHeight = e.scrollTop + e.clientHeight;
|
1073 |
+
const edgeThreshold = e.clientHeight;
|
1074 |
+
|
1075 |
+
const isTouchTopEdge = e.scrollTop <= edgeThreshold;
|
1076 |
+
const isTouchBottomEdge = bottomHeight >= e.scrollHeight - edgeThreshold;
|
1077 |
+
const isHitBottom =
|
1078 |
+
bottomHeight >= e.scrollHeight - (isMobileScreen ? 4 : 10);
|
1079 |
+
|
1080 |
+
const prevPageMsgIndex = msgRenderIndex - CHAT_PAGE_SIZE;
|
1081 |
+
const nextPageMsgIndex = msgRenderIndex + CHAT_PAGE_SIZE;
|
1082 |
+
|
1083 |
+
if (isTouchTopEdge && !isTouchBottomEdge) {
|
1084 |
+
setMsgRenderIndex(prevPageMsgIndex);
|
1085 |
+
} else if (isTouchBottomEdge) {
|
1086 |
+
setMsgRenderIndex(nextPageMsgIndex);
|
1087 |
+
}
|
1088 |
+
|
1089 |
+
setHitBottom(isHitBottom);
|
1090 |
+
setAutoScroll(isHitBottom);
|
1091 |
+
};
|
1092 |
+
|
1093 |
+
function scrollToBottom() {
|
1094 |
+
setMsgRenderIndex(renderMessages.length - CHAT_PAGE_SIZE);
|
1095 |
+
scrollDomToBottom();
|
1096 |
+
}
|
1097 |
+
|
1098 |
+
// clear context index = context length + index in messages
|
1099 |
+
const clearContextIndex =
|
1100 |
+
(session.clearContextIndex ?? -1) >= 0
|
1101 |
+
? session.clearContextIndex! + context.length - msgRenderIndex
|
1102 |
+
: -1;
|
1103 |
+
|
1104 |
+
const [showPromptModal, setShowPromptModal] = useState(false);
|
1105 |
+
|
1106 |
+
const clientConfig = useMemo(() => getClientConfig(), []);
|
1107 |
+
|
1108 |
+
const autoFocus = !isMobileScreen; // wont auto focus on mobile screen
|
1109 |
+
const showMaxIcon = !isMobileScreen && !clientConfig?.isApp;
|
1110 |
+
|
1111 |
+
useCommand({
|
1112 |
+
fill: setUserInput,
|
1113 |
+
submit: (text) => {
|
1114 |
+
doSubmit(text);
|
1115 |
+
},
|
1116 |
+
code: (text) => {
|
1117 |
+
if (accessStore.disableFastLink) return;
|
1118 |
+
console.log("[Command] got code from url: ", text);
|
1119 |
+
showConfirm(Locale.URLCommand.Code + `code = ${text}`).then((res) => {
|
1120 |
+
if (res) {
|
1121 |
+
accessStore.update((access) => (access.accessCode = text));
|
1122 |
+
}
|
1123 |
+
});
|
1124 |
+
},
|
1125 |
+
settings: (text) => {
|
1126 |
+
if (accessStore.disableFastLink) return;
|
1127 |
+
|
1128 |
+
try {
|
1129 |
+
const payload = JSON.parse(text) as {
|
1130 |
+
key?: string;
|
1131 |
+
url?: string;
|
1132 |
+
};
|
1133 |
+
|
1134 |
+
console.log("[Command] got settings from url: ", payload);
|
1135 |
+
|
1136 |
+
if (payload.key || payload.url) {
|
1137 |
+
showConfirm(
|
1138 |
+
Locale.URLCommand.Settings +
|
1139 |
+
`\n${JSON.stringify(payload, null, 4)}`,
|
1140 |
+
).then((res) => {
|
1141 |
+
if (!res) return;
|
1142 |
+
if (payload.key) {
|
1143 |
+
accessStore.update((access) => (access.openaiApiKey = payload.key!));
|
1144 |
+
}
|
1145 |
+
if (payload.url) {
|
1146 |
+
accessStore.update((access) => (access.openaiUrl = payload.url!));
|
1147 |
+
}
|
1148 |
+
});
|
1149 |
+
}
|
1150 |
+
} catch {
|
1151 |
+
console.error("[Command] failed to get settings from url: ", text);
|
1152 |
+
}
|
1153 |
+
},
|
1154 |
+
});
|
1155 |
+
|
1156 |
+
// edit / insert message modal
|
1157 |
+
const [isEditingMessage, setIsEditingMessage] = useState(false);
|
1158 |
+
|
1159 |
+
// remember unfinished input
|
1160 |
+
useEffect(() => {
|
1161 |
+
// try to load from local storage
|
1162 |
+
const key = UNFINISHED_INPUT(session.id);
|
1163 |
+
const mayBeUnfinishedInput = localStorage.getItem(key);
|
1164 |
+
if (mayBeUnfinishedInput && userInput.length === 0) {
|
1165 |
+
setUserInput(mayBeUnfinishedInput);
|
1166 |
+
localStorage.removeItem(key);
|
1167 |
+
}
|
1168 |
+
|
1169 |
+
const dom = inputRef.current;
|
1170 |
+
return () => {
|
1171 |
+
localStorage.setItem(key, dom?.value ?? "");
|
1172 |
+
};
|
1173 |
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
1174 |
+
}, []);
|
1175 |
+
|
1176 |
+
return (
|
1177 |
+
<div className={styles.chat} key={session.id}>
|
1178 |
+
<div className="window-header" data-tauri-drag-region>
|
1179 |
+
{isMobileScreen && (
|
1180 |
+
<div className="window-actions">
|
1181 |
+
<div className={"window-action-button"}>
|
1182 |
+
<IconButton
|
1183 |
+
icon={<ReturnIcon />}
|
1184 |
+
bordered
|
1185 |
+
title={Locale.Chat.Actions.ChatList}
|
1186 |
+
onClick={() => navigate(Path.Home)}
|
1187 |
+
/>
|
1188 |
+
</div>
|
1189 |
+
</div>
|
1190 |
+
)}
|
1191 |
+
|
1192 |
+
<div className={`window-header-title ${styles["chat-body-title"]}`}>
|
1193 |
+
<div
|
1194 |
+
className={`window-header-main-title ${styles["chat-body-main-title"]}`}
|
1195 |
+
onClickCapture={() => setIsEditingMessage(true)}
|
1196 |
+
>
|
1197 |
+
{!session.topic ? DEFAULT_TOPIC : session.topic}
|
1198 |
+
</div>
|
1199 |
+
<div className="window-header-sub-title">
|
1200 |
+
{Locale.Chat.SubTitle(session.messages.length)}
|
1201 |
+
</div>
|
1202 |
+
</div>
|
1203 |
+
<div className="window-actions">
|
1204 |
+
{!isMobileScreen && (
|
1205 |
+
<div className="window-action-button">
|
1206 |
+
<IconButton
|
1207 |
+
icon={<RenameIcon />}
|
1208 |
+
bordered
|
1209 |
+
onClick={() => setIsEditingMessage(true)}
|
1210 |
+
/>
|
1211 |
+
</div>
|
1212 |
+
)}
|
1213 |
+
<div className="window-action-button">
|
1214 |
+
<IconButton
|
1215 |
+
icon={<ExportIcon />}
|
1216 |
+
bordered
|
1217 |
+
title={Locale.Chat.Actions.Export}
|
1218 |
+
onClick={() => {
|
1219 |
+
setShowExport(true);
|
1220 |
+
}}
|
1221 |
+
/>
|
1222 |
+
</div>
|
1223 |
+
{showMaxIcon && (
|
1224 |
+
<div className="window-action-button">
|
1225 |
+
<IconButton
|
1226 |
+
icon={config.tightBorder ? <MinIcon /> : <MaxIcon />}
|
1227 |
+
bordered
|
1228 |
+
onClick={() => {
|
1229 |
+
config.update(
|
1230 |
+
(config) => (config.tightBorder = !config.tightBorder),
|
1231 |
+
);
|
1232 |
+
}}
|
1233 |
+
/>
|
1234 |
+
</div>
|
1235 |
+
)}
|
1236 |
+
</div>
|
1237 |
+
|
1238 |
+
<PromptToast
|
1239 |
+
showToast={!hitBottom}
|
1240 |
+
showModal={showPromptModal}
|
1241 |
+
setShowModal={setShowPromptModal}
|
1242 |
+
/>
|
1243 |
+
</div>
|
1244 |
+
|
1245 |
+
<div
|
1246 |
+
className={styles["chat-body"]}
|
1247 |
+
ref={scrollRef}
|
1248 |
+
onScroll={(e) => onChatBodyScroll(e.currentTarget)}
|
1249 |
+
onMouseDown={() => inputRef.current?.blur()}
|
1250 |
+
onTouchStart={() => {
|
1251 |
+
inputRef.current?.blur();
|
1252 |
+
setAutoScroll(false);
|
1253 |
+
}}
|
1254 |
+
>
|
1255 |
+
{messages.map((message, i) => {
|
1256 |
+
const isUser = message.role === "user";
|
1257 |
+
const isContext = i < context.length;
|
1258 |
+
const showActions =
|
1259 |
+
i > 0 &&
|
1260 |
+
!(message.preview || message.content.length === 0) &&
|
1261 |
+
!isContext;
|
1262 |
+
const showTyping = message.preview || message.streaming;
|
1263 |
+
|
1264 |
+
const shouldShowClearContextDivider = i === clearContextIndex - 1;
|
1265 |
+
|
1266 |
+
return (
|
1267 |
+
<Fragment key={message.id}>
|
1268 |
+
<div
|
1269 |
+
className={
|
1270 |
+
isUser ? styles["chat-message-user"] : styles["chat-message"]
|
1271 |
+
}
|
1272 |
+
>
|
1273 |
+
<div className={styles["chat-message-container"]}>
|
1274 |
+
<div className={styles["chat-message-header"]}>
|
1275 |
+
<div className={styles["chat-message-avatar"]}>
|
1276 |
+
<div className={styles["chat-message-edit"]}>
|
1277 |
+
<IconButton
|
1278 |
+
icon={<EditIcon />}
|
1279 |
+
onClick={async () => {
|
1280 |
+
const newMessage = await showPrompt(
|
1281 |
+
Locale.Chat.Actions.Edit,
|
1282 |
+
message.content,
|
1283 |
+
10,
|
1284 |
+
);
|
1285 |
+
chatStore.updateCurrentSession((session) => {
|
1286 |
+
const m = session.mask.context
|
1287 |
+
.concat(session.messages)
|
1288 |
+
.find((m) => m.id === message.id);
|
1289 |
+
if (m) {
|
1290 |
+
m.content = newMessage;
|
1291 |
+
}
|
1292 |
+
});
|
1293 |
+
}}
|
1294 |
+
></IconButton>
|
1295 |
+
</div>
|
1296 |
+
{isUser ? (
|
1297 |
+
<Avatar avatar={config.avatar} />
|
1298 |
+
) : isContext ? (
|
1299 |
+
<Avatar avatar="1f4ab" /> // Add this line for system messages
|
1300 |
+
) : (
|
1301 |
+
<>
|
1302 |
+
{["system"].includes(message.role) ? (
|
1303 |
+
<Avatar avatar="2699-fe0f" />
|
1304 |
+
) : (
|
1305 |
+
<MaskAvatar mask={session.mask} />
|
1306 |
+
)}
|
1307 |
+
</>
|
1308 |
+
)}
|
1309 |
+
</div>
|
1310 |
+
|
1311 |
+
{showActions && (
|
1312 |
+
<div className={styles["chat-message-actions"]}>
|
1313 |
+
<div className={styles["chat-input-actions"]}>
|
1314 |
+
{message.streaming ? (
|
1315 |
+
<ChatAction
|
1316 |
+
text={Locale.Chat.Actions.Stop}
|
1317 |
+
icon={<StopIcon />}
|
1318 |
+
onClick={() => onUserStop(message.id ?? i)}
|
1319 |
+
/>
|
1320 |
+
) : (
|
1321 |
+
<>
|
1322 |
+
<ChatAction
|
1323 |
+
text={Locale.Chat.Actions.Retry}
|
1324 |
+
icon={<ResetIcon />}
|
1325 |
+
onClick={() => onResend(message)}
|
1326 |
+
/>
|
1327 |
+
|
1328 |
+
<ChatAction
|
1329 |
+
text={Locale.Chat.Actions.Delete}
|
1330 |
+
icon={<DeleteIcon />}
|
1331 |
+
onClick={() => onDelete(message.id ?? i)}
|
1332 |
+
/>
|
1333 |
+
|
1334 |
+
<ChatAction
|
1335 |
+
text={Locale.Chat.Actions.Pin}
|
1336 |
+
icon={<PinIcon />}
|
1337 |
+
onClick={() => onPinMessage(message)}
|
1338 |
+
/>
|
1339 |
+
<ChatAction
|
1340 |
+
text={Locale.Chat.Actions.Copy}
|
1341 |
+
icon={<CopyIcon />}
|
1342 |
+
onClick={() => copyToClipboard(message.content)}
|
1343 |
+
/>
|
1344 |
+
</>
|
1345 |
+
)}
|
1346 |
+
</div>
|
1347 |
+
</div>
|
1348 |
+
)}
|
1349 |
+
</div>
|
1350 |
+
{showTyping && (
|
1351 |
+
<div className={styles["chat-message-status"]}>
|
1352 |
+
{Locale.Chat.Typing}
|
1353 |
+
</div>
|
1354 |
+
)}
|
1355 |
+
<div className={styles["chat-message-item"]}>
|
1356 |
+
<Markdown
|
1357 |
+
content={message.content}
|
1358 |
+
loading={
|
1359 |
+
(message.preview || message.streaming) &&
|
1360 |
+
message.content.length === 0 &&
|
1361 |
+
!isUser
|
1362 |
+
}
|
1363 |
+
onContextMenu={(e) => onRightClick(e, message)}
|
1364 |
+
onDoubleClickCapture={() => {
|
1365 |
+
if (!isMobileScreen) return;
|
1366 |
+
setUserInput(message.content);
|
1367 |
+
}}
|
1368 |
+
fontSize={fontSize}
|
1369 |
+
parentRef={scrollRef}
|
1370 |
+
defaultShow={i >= messages.length - 6}
|
1371 |
+
/>
|
1372 |
+
</div>
|
1373 |
+
|
1374 |
+
<div className={styles["chat-message-action-date"]}>
|
1375 |
+
{isContext
|
1376 |
+
? Locale.Chat.IsContext
|
1377 |
+
: message.date.toLocaleString()}
|
1378 |
+
</div>
|
1379 |
+
</div>
|
1380 |
+
</div>
|
1381 |
+
{shouldShowClearContextDivider && <ClearContextDivider />}
|
1382 |
+
</Fragment>
|
1383 |
+
);
|
1384 |
+
})}
|
1385 |
+
</div>
|
1386 |
+
|
1387 |
+
<div className={styles["chat-input-panel"]}>
|
1388 |
+
<PromptHints prompts={promptHints} onPromptSelect={onPromptSelect} />
|
1389 |
+
|
1390 |
+
<ChatActions
|
1391 |
+
showPromptModal={() => setShowPromptModal(true)}
|
1392 |
+
scrollToBottom={scrollToBottom}
|
1393 |
+
hitBottom={hitBottom}
|
1394 |
+
showPromptHints={() => {
|
1395 |
+
// Click again to close
|
1396 |
+
if (promptHints.length > 0) {
|
1397 |
+
setPromptHints([]);
|
1398 |
+
return;
|
1399 |
+
}
|
1400 |
+
|
1401 |
+
inputRef.current?.focus();
|
1402 |
+
setUserInput("/");
|
1403 |
+
onSearch("");
|
1404 |
+
}}
|
1405 |
+
showContextPrompts={false}
|
1406 |
+
toggleContextPrompts={() => showToast(Locale.WIP)}
|
1407 |
+
/>
|
1408 |
+
<div className={styles["chat-input-panel-inner"]}>
|
1409 |
+
<textarea
|
1410 |
+
ref={inputRef}
|
1411 |
+
className={styles["chat-input"]}
|
1412 |
+
placeholder={Locale.Chat.Input(submitKey)}
|
1413 |
+
onInput={(e) => onInput(e.currentTarget.value)}
|
1414 |
+
value={userInput}
|
1415 |
+
onKeyDown={onInputKeyDown}
|
1416 |
+
onFocus={scrollToBottom}
|
1417 |
+
onClick={scrollToBottom}
|
1418 |
+
rows={inputRows}
|
1419 |
+
autoFocus={autoFocus}
|
1420 |
+
style={{
|
1421 |
+
fontSize: config.fontSize,
|
1422 |
+
}}
|
1423 |
+
/>
|
1424 |
+
<IconButton
|
1425 |
+
icon={<SendWhiteIcon />}
|
1426 |
+
text={Locale.Chat.Send}
|
1427 |
+
className={styles["chat-input-send"]}
|
1428 |
+
type="primary"
|
1429 |
+
onClick={() => doSubmit(userInput)}
|
1430 |
+
/>
|
1431 |
+
</div>
|
1432 |
+
</div>
|
1433 |
+
|
1434 |
+
{showExport && (
|
1435 |
+
<ExportMessageModal onClose={() => setShowExport(false)} />
|
1436 |
+
)}
|
1437 |
+
|
1438 |
+
{isEditingMessage && (
|
1439 |
+
<EditMessageModal
|
1440 |
+
onClose={() => {
|
1441 |
+
setIsEditingMessage(false);
|
1442 |
+
}}
|
1443 |
+
/>
|
1444 |
+
)}
|
1445 |
+
</div>
|
1446 |
+
);
|
1447 |
+
}
|
1448 |
+
|
1449 |
+
export function Chat() {
|
1450 |
+
const chatStore = useChatStore();
|
1451 |
+
const sessionIndex = chatStore.currentSessionIndex;
|
1452 |
+
return <_Chat key={sessionIndex}></_Chat>;
|
1453 |
+
}
|
NeuroGPT/app/components/emoji.tsx
ADDED
@@ -0,0 +1,79 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import EmojiPicker, {
|
2 |
+
Emoji,
|
3 |
+
EmojiStyle,
|
4 |
+
Theme as EmojiTheme,
|
5 |
+
} from "emoji-picker-react";
|
6 |
+
|
7 |
+
import { ModelType } from "../store";
|
8 |
+
|
9 |
+
import BotIcon from "../icons/bot.svg";
|
10 |
+
import BlackBotIcon from "../icons/black-bot.svg";
|
11 |
+
import { isIOS, isMacOS } from "../utils"; // Import the isIOS & isMacOS functions from the utils file
|
12 |
+
|
13 |
+
export function getEmojiUrl(unified: string, style: EmojiStyle) {
|
14 |
+
const isAppleDevice = isMacOS() || isIOS();
|
15 |
+
const emojiDataSource =
|
16 |
+
(isAppleDevice && style === "apple") ||
|
17 |
+
(!isAppleDevice && style === "google")
|
18 |
+
? "emoji-datasource-apple"
|
19 |
+
: "emoji-datasource-google";
|
20 |
+
|
21 |
+
const emojiStyle = style === "apple" && isAppleDevice ? "apple" : "google";
|
22 |
+
|
23 |
+
return `https://cdn.staticfile.org/${emojiDataSource}/14.0.0/img/${emojiStyle}/64/${unified}.png`;
|
24 |
+
}
|
25 |
+
|
26 |
+
export function debounce(func: Function, delay: number) {
|
27 |
+
let timeoutId: NodeJS.Timeout;
|
28 |
+
return function (...args: any[]) {
|
29 |
+
clearTimeout(timeoutId);
|
30 |
+
timeoutId = setTimeout(() => {
|
31 |
+
func.apply(null, args);
|
32 |
+
}, delay);
|
33 |
+
};
|
34 |
+
}
|
35 |
+
|
36 |
+
export function AvatarPicker(props: {
|
37 |
+
onEmojiClick: (emojiId: string) => void;
|
38 |
+
}) {
|
39 |
+
return (
|
40 |
+
<EmojiPicker
|
41 |
+
lazyLoadEmojis
|
42 |
+
theme={EmojiTheme.AUTO}
|
43 |
+
getEmojiUrl={getEmojiUrl}
|
44 |
+
onEmojiClick={(e) => {
|
45 |
+
props.onEmojiClick(e.unified);
|
46 |
+
}}
|
47 |
+
/>
|
48 |
+
);
|
49 |
+
}
|
50 |
+
|
51 |
+
export function Avatar(props: { model?: ModelType; avatar?: string }) {
|
52 |
+
if (props.model) {
|
53 |
+
return (
|
54 |
+
<div className="no-dark">
|
55 |
+
{props.model?.startsWith("gpt-4") ? (
|
56 |
+
<BlackBotIcon className="user-avatar" />
|
57 |
+
) : (
|
58 |
+
<BlackBotIcon className="user-avatar" />
|
59 |
+
)}
|
60 |
+
</div>
|
61 |
+
);
|
62 |
+
}
|
63 |
+
|
64 |
+
return (
|
65 |
+
<div className="user-avatar">
|
66 |
+
{props.avatar && <EmojiAvatar avatar={props.avatar} />}
|
67 |
+
</div>
|
68 |
+
);
|
69 |
+
}
|
70 |
+
|
71 |
+
export function EmojiAvatar(props: { avatar: string; size?: number }) {
|
72 |
+
return (
|
73 |
+
<Emoji
|
74 |
+
unified={props.avatar}
|
75 |
+
size={props.size ?? 18}
|
76 |
+
getEmojiUrl={getEmojiUrl}
|
77 |
+
/>
|
78 |
+
);
|
79 |
+
}
|
NeuroGPT/app/components/error.tsx
ADDED
@@ -0,0 +1,72 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import React from "react";
|
2 |
+
import { IconButton } from "./button";
|
3 |
+
import GithubIcon from "../icons/github.svg";
|
4 |
+
import ResetIcon from "../icons/reload.svg";
|
5 |
+
import { ISSUE_URL } from "../constant";
|
6 |
+
import Locale from "../locales";
|
7 |
+
import { showConfirm } from "./ui-lib";
|
8 |
+
import { useSyncStore } from "../store/sync";
|
9 |
+
|
10 |
+
interface IErrorBoundaryState {
|
11 |
+
hasError: boolean;
|
12 |
+
error: Error | null;
|
13 |
+
info: React.ErrorInfo | null;
|
14 |
+
}
|
15 |
+
|
16 |
+
export class ErrorBoundary extends React.Component<any, IErrorBoundaryState> {
|
17 |
+
constructor(props: any) {
|
18 |
+
super(props);
|
19 |
+
this.state = { hasError: false, error: null, info: null };
|
20 |
+
}
|
21 |
+
|
22 |
+
componentDidCatch(error: Error, info: React.ErrorInfo) {
|
23 |
+
// Update state with error details
|
24 |
+
this.setState({ hasError: true, error, info });
|
25 |
+
}
|
26 |
+
|
27 |
+
clearAndSaveData() {
|
28 |
+
try {
|
29 |
+
useSyncStore.getState().export();
|
30 |
+
} finally {
|
31 |
+
localStorage.clear();
|
32 |
+
location.reload();
|
33 |
+
}
|
34 |
+
}
|
35 |
+
|
36 |
+
render() {
|
37 |
+
if (this.state.hasError) {
|
38 |
+
// Render error message
|
39 |
+
return (
|
40 |
+
<div className="error">
|
41 |
+
<h2>Oops, something went wrong!</h2>
|
42 |
+
<pre>
|
43 |
+
<code>{this.state.error?.toString()}</code>
|
44 |
+
<code>{this.state.info?.componentStack}</code>
|
45 |
+
</pre>
|
46 |
+
|
47 |
+
<div style={{ display: "flex", justifyContent: "space-between" }}>
|
48 |
+
<a href={ISSUE_URL} className="report">
|
49 |
+
<IconButton
|
50 |
+
text="Report This Error"
|
51 |
+
icon={<GithubIcon />}
|
52 |
+
bordered
|
53 |
+
/>
|
54 |
+
</a>
|
55 |
+
<IconButton
|
56 |
+
icon={<ResetIcon />}
|
57 |
+
text="Clear All Data"
|
58 |
+
onClick={async () => {
|
59 |
+
if (await showConfirm(Locale.Settings.Danger.Reset.Confirm)) {
|
60 |
+
this.clearAndSaveData();
|
61 |
+
}
|
62 |
+
}}
|
63 |
+
bordered
|
64 |
+
/>
|
65 |
+
</div>
|
66 |
+
</div>
|
67 |
+
);
|
68 |
+
}
|
69 |
+
// if no error occurred, render children
|
70 |
+
return this.props.children;
|
71 |
+
}
|
72 |
+
}
|
NeuroGPT/app/components/exporter.module.scss
ADDED
@@ -0,0 +1,223 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
.message-exporter {
|
2 |
+
&-body {
|
3 |
+
margin-top: 20px;
|
4 |
+
}
|
5 |
+
}
|
6 |
+
|
7 |
+
.export-content {
|
8 |
+
white-space: break-spaces;
|
9 |
+
padding: 10px !important;
|
10 |
+
}
|
11 |
+
|
12 |
+
.steps {
|
13 |
+
background-color: var(--gray);
|
14 |
+
border-radius: 10px;
|
15 |
+
overflow: hidden;
|
16 |
+
padding: 5px;
|
17 |
+
position: relative;
|
18 |
+
box-shadow: var(--card-shadow) inset;
|
19 |
+
|
20 |
+
.steps-progress {
|
21 |
+
$padding: 5px;
|
22 |
+
height: calc(100% - 2 * $padding);
|
23 |
+
width: calc(100% - 2 * $padding);
|
24 |
+
position: absolute;
|
25 |
+
top: $padding;
|
26 |
+
left: $padding;
|
27 |
+
|
28 |
+
&-inner {
|
29 |
+
box-sizing: border-box;
|
30 |
+
box-shadow: var(--card-shadow);
|
31 |
+
border: var(--border-in-light);
|
32 |
+
content: "";
|
33 |
+
display: inline-block;
|
34 |
+
width: 0%;
|
35 |
+
height: 100%;
|
36 |
+
background-color: var(--white);
|
37 |
+
transition: all ease 0.3s;
|
38 |
+
border-radius: 8px;
|
39 |
+
}
|
40 |
+
}
|
41 |
+
|
42 |
+
.steps-inner {
|
43 |
+
display: flex;
|
44 |
+
transform: scale(1);
|
45 |
+
|
46 |
+
.step {
|
47 |
+
flex-grow: 1;
|
48 |
+
padding: 5px 10px;
|
49 |
+
font-size: 14px;
|
50 |
+
color: var(--black);
|
51 |
+
opacity: 0.5;
|
52 |
+
transition: all ease 0.3s;
|
53 |
+
|
54 |
+
display: flex;
|
55 |
+
align-items: center;
|
56 |
+
justify-content: center;
|
57 |
+
|
58 |
+
$radius: 8px;
|
59 |
+
|
60 |
+
&-finished {
|
61 |
+
opacity: 0.9;
|
62 |
+
}
|
63 |
+
|
64 |
+
&:hover {
|
65 |
+
opacity: 0.8;
|
66 |
+
}
|
67 |
+
|
68 |
+
&-current {
|
69 |
+
color: var(--primary);
|
70 |
+
}
|
71 |
+
|
72 |
+
.step-index {
|
73 |
+
background-color: var(--gray);
|
74 |
+
border: var(--border-in-light);
|
75 |
+
border-radius: 6px;
|
76 |
+
display: inline-block;
|
77 |
+
padding: 0px 5px;
|
78 |
+
font-size: 12px;
|
79 |
+
margin-right: 8px;
|
80 |
+
opacity: 0.8;
|
81 |
+
}
|
82 |
+
|
83 |
+
.step-name {
|
84 |
+
font-size: 12px;
|
85 |
+
}
|
86 |
+
}
|
87 |
+
}
|
88 |
+
}
|
89 |
+
|
90 |
+
.preview-actions {
|
91 |
+
margin-bottom: 20px;
|
92 |
+
display: flex;
|
93 |
+
justify-content: space-between;
|
94 |
+
|
95 |
+
button {
|
96 |
+
flex-grow: 1;
|
97 |
+
&:not(:last-child) {
|
98 |
+
margin-right: 10px;
|
99 |
+
}
|
100 |
+
}
|
101 |
+
}
|
102 |
+
|
103 |
+
.image-previewer {
|
104 |
+
.preview-body {
|
105 |
+
border-radius: 10px;
|
106 |
+
padding: 20px;
|
107 |
+
box-shadow: var(--card-shadow) inset;
|
108 |
+
background-color: var(--gray);
|
109 |
+
|
110 |
+
.chat-info {
|
111 |
+
background-color: var(--second);
|
112 |
+
padding: 20px;
|
113 |
+
border-radius: 10px;
|
114 |
+
margin-bottom: 20px;
|
115 |
+
display: flex;
|
116 |
+
justify-content: space-between;
|
117 |
+
align-items: flex-end;
|
118 |
+
position: relative;
|
119 |
+
overflow: hidden;
|
120 |
+
|
121 |
+
@media screen and (max-width: 600px) {
|
122 |
+
flex-direction: column;
|
123 |
+
align-items: flex-start;
|
124 |
+
|
125 |
+
.icons {
|
126 |
+
margin-bottom: 20px;
|
127 |
+
}
|
128 |
+
}
|
129 |
+
|
130 |
+
.logo {
|
131 |
+
position: absolute;
|
132 |
+
top: 0px;
|
133 |
+
left: 0px;
|
134 |
+
height: 50%;
|
135 |
+
transform: scale(1.5);
|
136 |
+
}
|
137 |
+
|
138 |
+
.main-title {
|
139 |
+
font-size: 20px;
|
140 |
+
font-weight: bolder;
|
141 |
+
}
|
142 |
+
|
143 |
+
.sub-title {
|
144 |
+
font-size: 12px;
|
145 |
+
}
|
146 |
+
|
147 |
+
.icons {
|
148 |
+
margin-top: 10px;
|
149 |
+
display: flex;
|
150 |
+
align-items: center;
|
151 |
+
|
152 |
+
.icon-space {
|
153 |
+
font-size: 12px;
|
154 |
+
margin: 0 10px;
|
155 |
+
font-weight: bolder;
|
156 |
+
color: var(--primary);
|
157 |
+
}
|
158 |
+
}
|
159 |
+
|
160 |
+
.chat-info-item {
|
161 |
+
font-size: 12px;
|
162 |
+
color: var(--primary);
|
163 |
+
padding: 2px 15px;
|
164 |
+
border-radius: 10px;
|
165 |
+
background-color: var(--white);
|
166 |
+
box-shadow: var(--card-shadow);
|
167 |
+
|
168 |
+
&:not(:last-child) {
|
169 |
+
margin-bottom: 5px;
|
170 |
+
}
|
171 |
+
}
|
172 |
+
}
|
173 |
+
|
174 |
+
.message {
|
175 |
+
margin-bottom: 20px;
|
176 |
+
display: flex;
|
177 |
+
|
178 |
+
.avatar {
|
179 |
+
margin-right: 10px;
|
180 |
+
}
|
181 |
+
|
182 |
+
.body {
|
183 |
+
border-radius: 10px;
|
184 |
+
padding: 8px 10px;
|
185 |
+
max-width: calc(100% - 104px);
|
186 |
+
box-shadow: var(--card-shadow);
|
187 |
+
border: var(--border-in-light);
|
188 |
+
|
189 |
+
*:not(li) {
|
190 |
+
overflow: hidden;
|
191 |
+
}
|
192 |
+
}
|
193 |
+
|
194 |
+
&-assistant {
|
195 |
+
.body {
|
196 |
+
background-color: var(--white);
|
197 |
+
}
|
198 |
+
}
|
199 |
+
|
200 |
+
&-system {
|
201 |
+
.body {
|
202 |
+
background-color: var(--white);
|
203 |
+
}
|
204 |
+
}
|
205 |
+
|
206 |
+
&-user {
|
207 |
+
flex-direction: row-reverse;
|
208 |
+
|
209 |
+
.avatar {
|
210 |
+
margin-right: 0;
|
211 |
+
}
|
212 |
+
|
213 |
+
.body {
|
214 |
+
background-color: var(--second);
|
215 |
+
margin-right: 10px;
|
216 |
+
}
|
217 |
+
}
|
218 |
+
}
|
219 |
+
}
|
220 |
+
|
221 |
+
.default-theme {
|
222 |
+
}
|
223 |
+
}
|
NeuroGPT/app/components/exporter.tsx
ADDED
@@ -0,0 +1,704 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
/* eslint-disable @next/next/no-img-element */
|
2 |
+
import { ChatMessage, useAppConfig, useChatStore } from "../store";
|
3 |
+
import Locale from "../locales";
|
4 |
+
import styles from "./exporter.module.scss";
|
5 |
+
import {
|
6 |
+
List,
|
7 |
+
ListItem,
|
8 |
+
Modal,
|
9 |
+
Select,
|
10 |
+
showImageModal,
|
11 |
+
showModal,
|
12 |
+
showToast,
|
13 |
+
} from "./ui-lib";
|
14 |
+
import { IconButton } from "./button";
|
15 |
+
import { copyToClipboard, downloadAs, useMobileScreen } from "../utils";
|
16 |
+
|
17 |
+
import CopyIcon from "../icons/copy.svg";
|
18 |
+
import LoadingIcon from "../icons/three-dots.svg";
|
19 |
+
import ChatGptIcon from "../icons/chatgpt.png";
|
20 |
+
import ShareIcon from "../icons/share.svg";
|
21 |
+
import BotIcon from "../icons/bot.png";
|
22 |
+
|
23 |
+
import DownloadIcon from "../icons/download.svg";
|
24 |
+
import { useEffect, useMemo, useRef, useState } from "react";
|
25 |
+
import { MessageSelector, useMessageSelector } from "./message-selector";
|
26 |
+
import { Avatar } from "./emoji";
|
27 |
+
import dynamic from "next/dynamic";
|
28 |
+
import NextImage from "next/image";
|
29 |
+
|
30 |
+
import { toBlob, toJpeg, toPng } from "html-to-image";
|
31 |
+
import { DEFAULT_MASK_AVATAR } from "../store/mask";
|
32 |
+
import { api } from "../client/api";
|
33 |
+
import { prettyObject } from "../utils/format";
|
34 |
+
import { EXPORT_MESSAGE_CLASS_NAME, REPO_URL } from "../constant";
|
35 |
+
import { getClientConfig } from "../config/client";
|
36 |
+
|
37 |
+
const Markdown = dynamic(async () => (await import("./markdown")).Markdown, {
|
38 |
+
loading: () => <LoadingIcon />,
|
39 |
+
});
|
40 |
+
|
41 |
+
export function ExportMessageModal(props: { onClose: () => void }) {
|
42 |
+
return (
|
43 |
+
<div className="modal-mask">
|
44 |
+
<Modal title={Locale.Export.Title} onClose={props.onClose}>
|
45 |
+
<div style={{ minHeight: "40vh" }}>
|
46 |
+
<MessageExporter />
|
47 |
+
</div>
|
48 |
+
</Modal>
|
49 |
+
</div>
|
50 |
+
);
|
51 |
+
}
|
52 |
+
|
53 |
+
function useSteps(
|
54 |
+
steps: Array<{
|
55 |
+
name: string;
|
56 |
+
value: string;
|
57 |
+
}>,
|
58 |
+
) {
|
59 |
+
const stepCount = steps.length;
|
60 |
+
const [currentStepIndex, setCurrentStepIndex] = useState(0);
|
61 |
+
const nextStep = () =>
|
62 |
+
setCurrentStepIndex((currentStepIndex + 1) % stepCount);
|
63 |
+
const prevStep = () =>
|
64 |
+
setCurrentStepIndex((currentStepIndex - 1 + stepCount) % stepCount);
|
65 |
+
|
66 |
+
return {
|
67 |
+
currentStepIndex,
|
68 |
+
setCurrentStepIndex,
|
69 |
+
nextStep,
|
70 |
+
prevStep,
|
71 |
+
currentStep: steps[currentStepIndex],
|
72 |
+
};
|
73 |
+
}
|
74 |
+
|
75 |
+
function Steps<
|
76 |
+
T extends {
|
77 |
+
name: string;
|
78 |
+
value: string;
|
79 |
+
}[],
|
80 |
+
>(props: { steps: T; onStepChange?: (index: number) => void; index: number }) {
|
81 |
+
const steps = props.steps;
|
82 |
+
const stepCount = steps.length;
|
83 |
+
|
84 |
+
return (
|
85 |
+
<div className={styles["steps"]}>
|
86 |
+
<div className={styles["steps-progress"]}>
|
87 |
+
<div
|
88 |
+
className={styles["steps-progress-inner"]}
|
89 |
+
style={{
|
90 |
+
width: `${((props.index + 1) / stepCount) * 100}%`,
|
91 |
+
}}
|
92 |
+
></div>
|
93 |
+
</div>
|
94 |
+
<div className={styles["steps-inner"]}>
|
95 |
+
{steps.map((step, i) => {
|
96 |
+
return (
|
97 |
+
<div
|
98 |
+
key={i}
|
99 |
+
className={`${styles["step"]} ${
|
100 |
+
styles[i <= props.index ? "step-finished" : ""]
|
101 |
+
} ${i === props.index && styles["step-current"]} clickable`}
|
102 |
+
onClick={() => {
|
103 |
+
props.onStepChange?.(i);
|
104 |
+
}}
|
105 |
+
role="button"
|
106 |
+
>
|
107 |
+
<span className={styles["step-index"]}>{i + 1}</span>
|
108 |
+
<span className={styles["step-name"]}>{step.name}</span>
|
109 |
+
</div>
|
110 |
+
);
|
111 |
+
})}
|
112 |
+
</div>
|
113 |
+
</div>
|
114 |
+
);
|
115 |
+
}
|
116 |
+
|
117 |
+
export function MessageExporter() {
|
118 |
+
const steps = [
|
119 |
+
{
|
120 |
+
name: Locale.Export.Steps.Select,
|
121 |
+
value: "select",
|
122 |
+
},
|
123 |
+
{
|
124 |
+
name: Locale.Export.Steps.Preview,
|
125 |
+
value: "preview",
|
126 |
+
},
|
127 |
+
];
|
128 |
+
const { currentStep, setCurrentStepIndex, currentStepIndex } =
|
129 |
+
useSteps(steps);
|
130 |
+
const formats = ["text", "image", "json"] as const;
|
131 |
+
type ExportFormat = (typeof formats)[number];
|
132 |
+
|
133 |
+
const [exportConfig, setExportConfig] = useState({
|
134 |
+
format: "image" as ExportFormat,
|
135 |
+
includeContext: true,
|
136 |
+
});
|
137 |
+
|
138 |
+
function updateExportConfig(updater: (config: typeof exportConfig) => void) {
|
139 |
+
const config = { ...exportConfig };
|
140 |
+
updater(config);
|
141 |
+
setExportConfig(config);
|
142 |
+
}
|
143 |
+
|
144 |
+
const chatStore = useChatStore();
|
145 |
+
const session = chatStore.currentSession();
|
146 |
+
const { selection, updateSelection } = useMessageSelector();
|
147 |
+
const selectedMessages = useMemo(() => {
|
148 |
+
const ret: ChatMessage[] = [];
|
149 |
+
if (exportConfig.includeContext) {
|
150 |
+
ret.push(...session.mask.context);
|
151 |
+
}
|
152 |
+
ret.push(...session.messages.filter((m, i) => selection.has(m.id)));
|
153 |
+
return ret;
|
154 |
+
}, [
|
155 |
+
exportConfig.includeContext,
|
156 |
+
session.messages,
|
157 |
+
session.mask.context,
|
158 |
+
selection,
|
159 |
+
]);
|
160 |
+
function preview() {
|
161 |
+
if (exportConfig.format === "text") {
|
162 |
+
return (
|
163 |
+
<MarkdownPreviewer messages={selectedMessages} topic={session.topic} />
|
164 |
+
);
|
165 |
+
} else if (exportConfig.format === "json") {
|
166 |
+
return (
|
167 |
+
<JsonPreviewer messages={selectedMessages} topic={session.topic} />
|
168 |
+
);
|
169 |
+
} else {
|
170 |
+
return (
|
171 |
+
<ImagePreviewer messages={selectedMessages} topic={session.topic} />
|
172 |
+
);
|
173 |
+
}
|
174 |
+
}
|
175 |
+
|
176 |
+
return (
|
177 |
+
<>
|
178 |
+
<Steps
|
179 |
+
steps={steps}
|
180 |
+
index={currentStepIndex}
|
181 |
+
onStepChange={setCurrentStepIndex}
|
182 |
+
/>
|
183 |
+
<div
|
184 |
+
className={styles["message-exporter-body"]}
|
185 |
+
style={currentStep.value !== "select" ? { display: "none" } : {}}
|
186 |
+
>
|
187 |
+
<List>
|
188 |
+
<ListItem
|
189 |
+
title={Locale.Export.Format.Title}
|
190 |
+
subTitle={Locale.Export.Format.SubTitle}
|
191 |
+
>
|
192 |
+
<Select
|
193 |
+
value={exportConfig.format}
|
194 |
+
onChange={(e) =>
|
195 |
+
updateExportConfig(
|
196 |
+
(config) =>
|
197 |
+
(config.format = e.currentTarget.value as ExportFormat),
|
198 |
+
)
|
199 |
+
}
|
200 |
+
>
|
201 |
+
{formats.map((f) => (
|
202 |
+
<option key={f} value={f}>
|
203 |
+
{f}
|
204 |
+
</option>
|
205 |
+
))}
|
206 |
+
</Select>
|
207 |
+
</ListItem>
|
208 |
+
<ListItem
|
209 |
+
title={Locale.Export.IncludeContext.Title}
|
210 |
+
subTitle={Locale.Export.IncludeContext.SubTitle}
|
211 |
+
>
|
212 |
+
<input
|
213 |
+
type="checkbox"
|
214 |
+
checked={exportConfig.includeContext}
|
215 |
+
onChange={(e) => {
|
216 |
+
updateExportConfig(
|
217 |
+
(config) => (config.includeContext = e.currentTarget.checked),
|
218 |
+
);
|
219 |
+
}}
|
220 |
+
></input>
|
221 |
+
</ListItem>
|
222 |
+
</List>
|
223 |
+
<MessageSelector
|
224 |
+
selection={selection}
|
225 |
+
updateSelection={updateSelection}
|
226 |
+
defaultSelectAll
|
227 |
+
/>
|
228 |
+
</div>
|
229 |
+
{currentStep.value === "preview" && (
|
230 |
+
<div className={styles["message-exporter-body"]}>{preview()}</div>
|
231 |
+
)}
|
232 |
+
</>
|
233 |
+
);
|
234 |
+
}
|
235 |
+
|
236 |
+
export function RenderExport(props: {
|
237 |
+
messages: ChatMessage[];
|
238 |
+
onRender: (messages: ChatMessage[]) => void;
|
239 |
+
}) {
|
240 |
+
const domRef = useRef<HTMLDivElement>(null);
|
241 |
+
|
242 |
+
useEffect(() => {
|
243 |
+
if (!domRef.current) return;
|
244 |
+
const dom = domRef.current;
|
245 |
+
const messages = Array.from(
|
246 |
+
dom.getElementsByClassName(EXPORT_MESSAGE_CLASS_NAME),
|
247 |
+
);
|
248 |
+
|
249 |
+
if (messages.length !== props.messages.length) {
|
250 |
+
return;
|
251 |
+
}
|
252 |
+
|
253 |
+
const renderMsgs = messages.map((v, i) => {
|
254 |
+
const [role, _] = v.id.split(":");
|
255 |
+
return {
|
256 |
+
id: i.toString(),
|
257 |
+
role: role as any,
|
258 |
+
content: role === "user" ? v.textContent ?? "" : v.innerHTML,
|
259 |
+
date: "",
|
260 |
+
};
|
261 |
+
});
|
262 |
+
|
263 |
+
props.onRender(renderMsgs);
|
264 |
+
});
|
265 |
+
|
266 |
+
return (
|
267 |
+
<div ref={domRef}>
|
268 |
+
{props.messages.map((m, i) => (
|
269 |
+
<div
|
270 |
+
key={i}
|
271 |
+
id={`${m.role}:${i}`}
|
272 |
+
className={EXPORT_MESSAGE_CLASS_NAME}
|
273 |
+
>
|
274 |
+
<Markdown content={m.content} defaultShow />
|
275 |
+
</div>
|
276 |
+
))}
|
277 |
+
</div>
|
278 |
+
);
|
279 |
+
}
|
280 |
+
|
281 |
+
export function PreviewActions(props: {
|
282 |
+
download: () => void;
|
283 |
+
copy: () => void;
|
284 |
+
showCopy?: boolean;
|
285 |
+
messages?: ChatMessage[];
|
286 |
+
}) {
|
287 |
+
const [loading, setLoading] = useState(false);
|
288 |
+
const [shouldExport, setShouldExport] = useState(false);
|
289 |
+
|
290 |
+
const onRenderMsgs = (msgs: ChatMessage[]) => {
|
291 |
+
setShouldExport(false);
|
292 |
+
|
293 |
+
api
|
294 |
+
.share(msgs)
|
295 |
+
.then((res) => {
|
296 |
+
if (!res) return;
|
297 |
+
showModal({
|
298 |
+
title: Locale.Export.Share,
|
299 |
+
children: [
|
300 |
+
<input
|
301 |
+
type="text"
|
302 |
+
value={res}
|
303 |
+
key="input"
|
304 |
+
style={{
|
305 |
+
width: "100%",
|
306 |
+
maxWidth: "unset",
|
307 |
+
}}
|
308 |
+
readOnly
|
309 |
+
onClick={(e) => e.currentTarget.select()}
|
310 |
+
></input>,
|
311 |
+
],
|
312 |
+
actions: [
|
313 |
+
<IconButton
|
314 |
+
icon={<CopyIcon />}
|
315 |
+
text={Locale.Chat.Actions.Copy}
|
316 |
+
key="copy"
|
317 |
+
onClick={() => copyToClipboard(res)}
|
318 |
+
/>,
|
319 |
+
],
|
320 |
+
});
|
321 |
+
setTimeout(() => {
|
322 |
+
window.open(res, "_blank");
|
323 |
+
}, 800);
|
324 |
+
})
|
325 |
+
.catch((e) => {
|
326 |
+
console.error("[Share]", e);
|
327 |
+
showToast(prettyObject(e));
|
328 |
+
})
|
329 |
+
.finally(() => setLoading(false));
|
330 |
+
};
|
331 |
+
|
332 |
+
const share = async () => {
|
333 |
+
if (props.messages?.length) {
|
334 |
+
setLoading(true);
|
335 |
+
setShouldExport(true);
|
336 |
+
}
|
337 |
+
};
|
338 |
+
|
339 |
+
return (
|
340 |
+
<>
|
341 |
+
<div className={styles["preview-actions"]}>
|
342 |
+
{props.showCopy && (
|
343 |
+
<IconButton
|
344 |
+
text={Locale.Export.Copy}
|
345 |
+
bordered
|
346 |
+
shadow
|
347 |
+
icon={<CopyIcon />}
|
348 |
+
onClick={props.copy}
|
349 |
+
></IconButton>
|
350 |
+
)}
|
351 |
+
<IconButton
|
352 |
+
text={Locale.Export.Download}
|
353 |
+
bordered
|
354 |
+
shadow
|
355 |
+
icon={<DownloadIcon />}
|
356 |
+
onClick={props.download}
|
357 |
+
></IconButton>
|
358 |
+
<IconButton
|
359 |
+
text={Locale.Export.Share}
|
360 |
+
bordered
|
361 |
+
shadow
|
362 |
+
icon={loading ? <LoadingIcon /> : <ShareIcon />}
|
363 |
+
onClick={share}
|
364 |
+
></IconButton>
|
365 |
+
</div>
|
366 |
+
<div
|
367 |
+
style={{
|
368 |
+
position: "fixed",
|
369 |
+
right: "200vw",
|
370 |
+
pointerEvents: "none",
|
371 |
+
}}
|
372 |
+
>
|
373 |
+
{shouldExport && (
|
374 |
+
<RenderExport
|
375 |
+
messages={props.messages ?? []}
|
376 |
+
onRender={onRenderMsgs}
|
377 |
+
/>
|
378 |
+
)}
|
379 |
+
</div>
|
380 |
+
</>
|
381 |
+
);
|
382 |
+
}
|
383 |
+
|
384 |
+
function ExportAvatar(props: { avatar: string }) {
|
385 |
+
if (props.avatar === DEFAULT_MASK_AVATAR) {
|
386 |
+
return (
|
387 |
+
<img
|
388 |
+
src={BotIcon.src}
|
389 |
+
width={30}
|
390 |
+
height={30}
|
391 |
+
alt="bot"
|
392 |
+
className="user-avatar"
|
393 |
+
/>
|
394 |
+
);
|
395 |
+
}
|
396 |
+
|
397 |
+
return <Avatar avatar={props.avatar} />;
|
398 |
+
}
|
399 |
+
|
400 |
+
export function ImagePreviewer(props: {
|
401 |
+
messages: ChatMessage[];
|
402 |
+
topic: string;
|
403 |
+
}) {
|
404 |
+
const chatStore = useChatStore();
|
405 |
+
const session = chatStore.currentSession();
|
406 |
+
const mask = session.mask;
|
407 |
+
const config = useAppConfig();
|
408 |
+
|
409 |
+
const previewRef = useRef<HTMLDivElement>(null);
|
410 |
+
|
411 |
+
const copy = () => {
|
412 |
+
showToast(Locale.Export.Image.Toast);
|
413 |
+
const dom = previewRef.current;
|
414 |
+
if (!dom) return;
|
415 |
+
toBlob(dom).then((blob) => {
|
416 |
+
if (!blob) return;
|
417 |
+
try {
|
418 |
+
navigator.clipboard
|
419 |
+
.write([
|
420 |
+
new ClipboardItem({
|
421 |
+
"image/png": blob,
|
422 |
+
}),
|
423 |
+
])
|
424 |
+
.then(() => {
|
425 |
+
showToast(Locale.Copy.Success);
|
426 |
+
refreshPreview();
|
427 |
+
});
|
428 |
+
} catch (e) {
|
429 |
+
console.error("[Copy Image] ", e);
|
430 |
+
showToast(Locale.Copy.Failed);
|
431 |
+
}
|
432 |
+
});
|
433 |
+
};
|
434 |
+
|
435 |
+
const isMobile = useMobileScreen();
|
436 |
+
|
437 |
+
const download = async () => {
|
438 |
+
showToast(Locale.Export.Image.Toast);
|
439 |
+
const dom = previewRef.current;
|
440 |
+
if (!dom) return;
|
441 |
+
|
442 |
+
const isApp = getClientConfig()?.isApp;
|
443 |
+
|
444 |
+
try {
|
445 |
+
const blob = await toPng(dom);
|
446 |
+
if (!blob) return;
|
447 |
+
|
448 |
+
if (isMobile || (isApp && window.__TAURI__)) {
|
449 |
+
if (isApp && window.__TAURI__) {
|
450 |
+
/**
|
451 |
+
* Fixed Tauri client app
|
452 |
+
* Resolved the issue where files couldn't be saved when there was a `:` in the dialog.
|
453 |
+
*/
|
454 |
+
const fileName = props.topic.replace(/:/g, '');
|
455 |
+
const result = await window.__TAURI__.dialog.save({
|
456 |
+
defaultPath: `${fileName}.png`,
|
457 |
+
filters: [
|
458 |
+
{
|
459 |
+
name: "PNG Files",
|
460 |
+
extensions: ["png"],
|
461 |
+
},
|
462 |
+
{
|
463 |
+
name: "All Files",
|
464 |
+
extensions: ["*"],
|
465 |
+
},
|
466 |
+
],
|
467 |
+
});
|
468 |
+
|
469 |
+
if (result !== null) {
|
470 |
+
const response = await fetch(blob);
|
471 |
+
const buffer = await response.arrayBuffer();
|
472 |
+
const uint8Array = new Uint8Array(buffer);
|
473 |
+
await window.__TAURI__.fs.writeBinaryFile(result, uint8Array);
|
474 |
+
showToast(Locale.Download.Success);
|
475 |
+
} else {
|
476 |
+
showToast(Locale.Download.Failed);
|
477 |
+
}
|
478 |
+
} else {
|
479 |
+
showImageModal(blob);
|
480 |
+
}
|
481 |
+
} else {
|
482 |
+
const link = document.createElement("a");
|
483 |
+
link.download = `${props.topic}.png`;
|
484 |
+
link.href = blob;
|
485 |
+
link.click();
|
486 |
+
refreshPreview();
|
487 |
+
}
|
488 |
+
} catch (error) {
|
489 |
+
showToast(Locale.Download.Failed);
|
490 |
+
}
|
491 |
+
};
|
492 |
+
|
493 |
+
const refreshPreview = () => {
|
494 |
+
const dom = previewRef.current;
|
495 |
+
if (dom) {
|
496 |
+
dom.innerHTML = dom.innerHTML; // Refresh the content of the preview by resetting its HTML for fix a bug glitching
|
497 |
+
}
|
498 |
+
};
|
499 |
+
|
500 |
+
return (
|
501 |
+
<div className={styles["image-previewer"]}>
|
502 |
+
<PreviewActions
|
503 |
+
copy={copy}
|
504 |
+
download={download}
|
505 |
+
showCopy={!isMobile}
|
506 |
+
messages={props.messages}
|
507 |
+
/>
|
508 |
+
<div
|
509 |
+
className={`${styles["preview-body"]} ${styles["default-theme"]}`}
|
510 |
+
ref={previewRef}
|
511 |
+
>
|
512 |
+
<div className={styles["chat-info"]}>
|
513 |
+
<div className={styles["logo"] + " no-dark"}>
|
514 |
+
<NextImage
|
515 |
+
src={ChatGptIcon.src}
|
516 |
+
alt="logo"
|
517 |
+
width={50}
|
518 |
+
height={50}
|
519 |
+
/>
|
520 |
+
</div>
|
521 |
+
|
522 |
+
<div>
|
523 |
+
<div className={styles["main-title"]}>NeuroGPT</div>
|
524 |
+
<div className={styles["sub-title"]}>
|
525 |
+
Author: https://t.me/neurogen_news
|
526 |
+
</div>
|
527 |
+
<div className={styles["icons"]}>
|
528 |
+
<ExportAvatar avatar={config.avatar} />
|
529 |
+
<span className={styles["icon-space"]}>&</span>
|
530 |
+
<ExportAvatar avatar={mask.avatar} />
|
531 |
+
</div>
|
532 |
+
</div>
|
533 |
+
<div>
|
534 |
+
<div className={styles["chat-info-item"]}>
|
535 |
+
{"🔗"} {REPO_URL}
|
536 |
+
</div>
|
537 |
+
<div className={styles["chat-info-item"]}>
|
538 |
+
{"🤖"} {Locale.Exporter.Model}: {mask.modelConfig.model}
|
539 |
+
</div>
|
540 |
+
<div className={styles["chat-info-item"]}>
|
541 |
+
{"💭"} {Locale.Exporter.Messages}: {props.messages.length}
|
542 |
+
</div>
|
543 |
+
<div className={styles["chat-info-item"]}>
|
544 |
+
{"💫"} {Locale.Exporter.Topic}: {session.topic}
|
545 |
+
</div>
|
546 |
+
<div className={styles["chat-info-item"]}>
|
547 |
+
{"🗓️"} {Locale.Exporter.Time}:{" "}
|
548 |
+
{new Date(
|
549 |
+
props.messages.at(-1)?.date ?? Date.now(),
|
550 |
+
).toLocaleString()}
|
551 |
+
</div>
|
552 |
+
</div>
|
553 |
+
</div>
|
554 |
+
{props.messages.map((m, i) => {
|
555 |
+
const isUserMessage = m.role === "user";
|
556 |
+
const isSystemMessage = m.role === "system";
|
557 |
+
const avatar =
|
558 |
+
isUserMessage && config.avatar
|
559 |
+
? config.avatar
|
560 |
+
: isSystemMessage
|
561 |
+
? "1f4ab"
|
562 |
+
: mask.avatar;
|
563 |
+
const messageClass = `${styles["message"]} ${
|
564 |
+
styles["message-" + m.role]
|
565 |
+
}`;
|
566 |
+
|
567 |
+
return (
|
568 |
+
<div className={messageClass} key={i}>
|
569 |
+
<div className={styles["avatar"]}>
|
570 |
+
<ExportAvatar avatar={avatar} />
|
571 |
+
</div>
|
572 |
+
|
573 |
+
<div className={styles["body"]}>
|
574 |
+
<Markdown
|
575 |
+
content={m.content}
|
576 |
+
fontSize={config.fontSize}
|
577 |
+
defaultShow
|
578 |
+
/>
|
579 |
+
</div>
|
580 |
+
</div>
|
581 |
+
);
|
582 |
+
})}
|
583 |
+
</div>
|
584 |
+
</div>
|
585 |
+
);
|
586 |
+
}
|
587 |
+
|
588 |
+
export function MarkdownPreviewer(props: {
|
589 |
+
messages: ChatMessage[];
|
590 |
+
topic: string;
|
591 |
+
}) {
|
592 |
+
const mdText =
|
593 |
+
`# ${props.topic}\n\n` +
|
594 |
+
props.messages
|
595 |
+
.map((m) => {
|
596 |
+
return m.role === "user"
|
597 |
+
? `## ${Locale.Export.MessageFromYou}:\n${m.content}`
|
598 |
+
: `## ${Locale.Export.MessageFromChatGPT}:\n${m.content.trim()}`;
|
599 |
+
})
|
600 |
+
.join("\n\n");
|
601 |
+
|
602 |
+
const copy = () => {
|
603 |
+
copyToClipboard(mdText);
|
604 |
+
};
|
605 |
+
const download = async () => {
|
606 |
+
const isApp = getClientConfig()?.isApp;
|
607 |
+
const blob = new Blob([mdText], { type: "text/markdown" });
|
608 |
+
const url = URL.createObjectURL(blob);
|
609 |
+
const link = document.createElement("a");
|
610 |
+
link.href = url;
|
611 |
+
link.download = `${props.topic}.md`;
|
612 |
+
|
613 |
+
if (isApp && window.__TAURI__) {
|
614 |
+
try {
|
615 |
+
const fileName = props.topic.replace(/:/g, '');
|
616 |
+
const result = await window.__TAURI__.dialog.save({
|
617 |
+
/**
|
618 |
+
* Fixed Tauri client app
|
619 |
+
* Resolved the issue where files couldn't be saved when there was a `:` in the dialog.
|
620 |
+
*/
|
621 |
+
defaultPath: `${fileName}.md`,
|
622 |
+
filters: [
|
623 |
+
{
|
624 |
+
name: "MD Files",
|
625 |
+
extensions: ["md"],
|
626 |
+
},
|
627 |
+
{
|
628 |
+
name: "All Files",
|
629 |
+
extensions: ["*"],
|
630 |
+
},
|
631 |
+
],
|
632 |
+
});
|
633 |
+
|
634 |
+
if (result !== null) {
|
635 |
+
const response = await fetch(url);
|
636 |
+
const buffer = await response.arrayBuffer();
|
637 |
+
const uint8Array = new Uint8Array(buffer);
|
638 |
+
await window.__TAURI__.fs.writeBinaryFile(result, uint8Array);
|
639 |
+
showToast(Locale.Download.Success);
|
640 |
+
} else {
|
641 |
+
showToast(Locale.Download.Failed);
|
642 |
+
}
|
643 |
+
} catch (error) {
|
644 |
+
showToast(Locale.Download.Failed);
|
645 |
+
}
|
646 |
+
} else {
|
647 |
+
link.click();
|
648 |
+
}
|
649 |
+
|
650 |
+
URL.revokeObjectURL(url);
|
651 |
+
};
|
652 |
+
return (
|
653 |
+
<>
|
654 |
+
<PreviewActions
|
655 |
+
copy={copy}
|
656 |
+
download={download}
|
657 |
+
showCopy={true}
|
658 |
+
messages={props.messages}
|
659 |
+
/>
|
660 |
+
<div className="markdown-body">
|
661 |
+
<pre className={styles["export-content"]}>{mdText}</pre>
|
662 |
+
</div>
|
663 |
+
</>
|
664 |
+
);
|
665 |
+
}
|
666 |
+
|
667 |
+
// modified by BackTrackZ now it's looks better
|
668 |
+
|
669 |
+
export function JsonPreviewer(props: {
|
670 |
+
messages: ChatMessage[];
|
671 |
+
topic: string;
|
672 |
+
}) {
|
673 |
+
const msgs = {
|
674 |
+
messages: [
|
675 |
+
...props.messages.map((m) => ({
|
676 |
+
role: m.role,
|
677 |
+
content: m.content,
|
678 |
+
})),
|
679 |
+
],
|
680 |
+
};
|
681 |
+
const mdText = "```json\n" + JSON.stringify(msgs, null, 2) + "\n```";
|
682 |
+
const minifiedJson = JSON.stringify(msgs);
|
683 |
+
|
684 |
+
const copy = () => {
|
685 |
+
copyToClipboard(minifiedJson);
|
686 |
+
};
|
687 |
+
const download = () => {
|
688 |
+
downloadAs((msgs), `${props.topic}.json`);
|
689 |
+
};
|
690 |
+
|
691 |
+
return (
|
692 |
+
<>
|
693 |
+
<PreviewActions
|
694 |
+
copy={copy}
|
695 |
+
download={download}
|
696 |
+
showCopy={false}
|
697 |
+
messages={props.messages}
|
698 |
+
/>
|
699 |
+
<div className="markdown-body" onClick={copy}>
|
700 |
+
<Markdown content={mdText} />
|
701 |
+
</div>
|
702 |
+
</>
|
703 |
+
);
|
704 |
+
}
|
NeuroGPT/app/components/home.module.scss
ADDED
@@ -0,0 +1,353 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
@mixin container {
|
2 |
+
background-color: var(--white);
|
3 |
+
border: var(--border-in-light);
|
4 |
+
border-radius: 20px;
|
5 |
+
box-shadow: var(--shadow);
|
6 |
+
color: var(--black);
|
7 |
+
background-color: var(--white);
|
8 |
+
min-width: 600px;
|
9 |
+
min-height: 370px;
|
10 |
+
max-width: 1200px;
|
11 |
+
|
12 |
+
display: flex;
|
13 |
+
overflow: hidden;
|
14 |
+
box-sizing: border-box;
|
15 |
+
|
16 |
+
width: var(--window-width);
|
17 |
+
height: var(--window-height);
|
18 |
+
}
|
19 |
+
|
20 |
+
.container {
|
21 |
+
@include container();
|
22 |
+
}
|
23 |
+
|
24 |
+
@media only screen and (min-width: 600px) {
|
25 |
+
.tight-container {
|
26 |
+
--window-width: 100vw;
|
27 |
+
--window-height: var(--full-height);
|
28 |
+
--window-content-width: calc(100% - var(--sidebar-width));
|
29 |
+
|
30 |
+
@include container();
|
31 |
+
|
32 |
+
max-width: 100vw;
|
33 |
+
max-height: var(--full-height);
|
34 |
+
|
35 |
+
border-radius: 0;
|
36 |
+
border: 0;
|
37 |
+
}
|
38 |
+
}
|
39 |
+
|
40 |
+
.sidebar {
|
41 |
+
top: 0;
|
42 |
+
width: var(--sidebar-width);
|
43 |
+
box-sizing: border-box;
|
44 |
+
padding: 20px;
|
45 |
+
background-color: var(--second);
|
46 |
+
display: flex;
|
47 |
+
flex-direction: column;
|
48 |
+
box-shadow: inset -2px 0px 2px 0px rgb(0, 0, 0, 0.05);
|
49 |
+
position: relative;
|
50 |
+
transition: width ease 0.05s;
|
51 |
+
|
52 |
+
.sidebar-header-bar {
|
53 |
+
display: flex;
|
54 |
+
margin-bottom: 20px;
|
55 |
+
|
56 |
+
.sidebar-bar-button {
|
57 |
+
flex-grow: 1;
|
58 |
+
|
59 |
+
&:not(:last-child) {
|
60 |
+
margin-right: 10px;
|
61 |
+
}
|
62 |
+
}
|
63 |
+
}
|
64 |
+
|
65 |
+
&:hover,
|
66 |
+
&:active {
|
67 |
+
.sidebar-drag {
|
68 |
+
background-color: rgba($color: #000000, $alpha: 0.01);
|
69 |
+
|
70 |
+
svg {
|
71 |
+
opacity: 0.2;
|
72 |
+
}
|
73 |
+
}
|
74 |
+
}
|
75 |
+
}
|
76 |
+
|
77 |
+
.sidebar-drag {
|
78 |
+
$width: 14px;
|
79 |
+
|
80 |
+
position: absolute;
|
81 |
+
top: 0;
|
82 |
+
right: 0;
|
83 |
+
height: 100%;
|
84 |
+
width: $width;
|
85 |
+
background-color: rgba($color: #000000, $alpha: 0);
|
86 |
+
cursor: ew-resize;
|
87 |
+
transition: all ease 0.3s;
|
88 |
+
display: flex;
|
89 |
+
align-items: center;
|
90 |
+
|
91 |
+
svg {
|
92 |
+
opacity: 0;
|
93 |
+
margin-left: -2px;
|
94 |
+
}
|
95 |
+
}
|
96 |
+
|
97 |
+
.rotate {
|
98 |
+
animation: rotate 5s infinite linear;
|
99 |
+
}
|
100 |
+
|
101 |
+
@keyframes rotate {
|
102 |
+
from {
|
103 |
+
transform: rotate(0deg);
|
104 |
+
}
|
105 |
+
to {
|
106 |
+
transform: rotate(360deg);
|
107 |
+
}
|
108 |
+
}
|
109 |
+
|
110 |
+
.window-content {
|
111 |
+
width: var(--window-content-width);
|
112 |
+
height: 100%;
|
113 |
+
display: flex;
|
114 |
+
flex-direction: column;
|
115 |
+
}
|
116 |
+
|
117 |
+
.mobile {
|
118 |
+
display: none;
|
119 |
+
}
|
120 |
+
|
121 |
+
@media only screen and (max-width: 600px) {
|
122 |
+
.container {
|
123 |
+
min-height: unset;
|
124 |
+
min-width: unset;
|
125 |
+
max-height: unset;
|
126 |
+
min-width: unset;
|
127 |
+
border: 0;
|
128 |
+
border-radius: 0;
|
129 |
+
}
|
130 |
+
|
131 |
+
.sidebar {
|
132 |
+
position: absolute;
|
133 |
+
left: -100%;
|
134 |
+
z-index: 1000;
|
135 |
+
height: var(--full-height);
|
136 |
+
transition: all ease 0.3s;
|
137 |
+
box-shadow: none;
|
138 |
+
}
|
139 |
+
|
140 |
+
.sidebar-show {
|
141 |
+
left: 0;
|
142 |
+
}
|
143 |
+
|
144 |
+
.mobile {
|
145 |
+
display: block;
|
146 |
+
}
|
147 |
+
}
|
148 |
+
|
149 |
+
.sidebar-header {
|
150 |
+
position: relative;
|
151 |
+
padding-top: 20px;
|
152 |
+
padding-bottom: 20px;
|
153 |
+
}
|
154 |
+
|
155 |
+
.sidebar-logo {
|
156 |
+
position: absolute;
|
157 |
+
right: 0;
|
158 |
+
bottom: 18px;
|
159 |
+
}
|
160 |
+
|
161 |
+
.sidebar-title {
|
162 |
+
font-size: 20px;
|
163 |
+
font-weight: bold;
|
164 |
+
animation: slide-in ease 0.3s;
|
165 |
+
}
|
166 |
+
|
167 |
+
.sidebar-sub-title {
|
168 |
+
font-size: 12px;
|
169 |
+
font-weight: 400;
|
170 |
+
animation: slide-in ease 0.3s;
|
171 |
+
}
|
172 |
+
|
173 |
+
.sidebar-body {
|
174 |
+
flex: 1;
|
175 |
+
overflow: auto;
|
176 |
+
overflow-x: hidden;
|
177 |
+
}
|
178 |
+
|
179 |
+
.chat-item {
|
180 |
+
padding: 10px 14px;
|
181 |
+
background-color: var(--white);
|
182 |
+
border-radius: 10px;
|
183 |
+
margin-bottom: 10px;
|
184 |
+
box-shadow: var(--card-shadow);
|
185 |
+
transition: background-color 0.3s ease;
|
186 |
+
cursor: pointer;
|
187 |
+
user-select: none;
|
188 |
+
border: 2px solid transparent;
|
189 |
+
position: relative;
|
190 |
+
content-visibility: auto;
|
191 |
+
}
|
192 |
+
|
193 |
+
.chat-item:hover {
|
194 |
+
background-color: var(--hover-color);
|
195 |
+
}
|
196 |
+
|
197 |
+
.chat-item-selected {
|
198 |
+
border-color: var(--primary);
|
199 |
+
}
|
200 |
+
|
201 |
+
.chat-item-title {
|
202 |
+
font-size: 14px;
|
203 |
+
font-weight: bolder;
|
204 |
+
display: block;
|
205 |
+
width: calc(100% - 15px);
|
206 |
+
overflow: hidden;
|
207 |
+
text-overflow: ellipsis;
|
208 |
+
white-space: nowrap;
|
209 |
+
animation: slide-in ease 0.3s;
|
210 |
+
}
|
211 |
+
|
212 |
+
.chat-item-delete {
|
213 |
+
position: absolute;
|
214 |
+
top: 0;
|
215 |
+
right: 0;
|
216 |
+
transition: all ease 0.3s;
|
217 |
+
opacity: 0;
|
218 |
+
cursor: pointer;
|
219 |
+
}
|
220 |
+
|
221 |
+
.chat-item:hover > .chat-item-delete {
|
222 |
+
opacity: 0.5;
|
223 |
+
transform: translateX(-4px);
|
224 |
+
}
|
225 |
+
|
226 |
+
.chat-item:hover > .chat-item-delete:hover {
|
227 |
+
opacity: 1;
|
228 |
+
}
|
229 |
+
|
230 |
+
.chat-item-info {
|
231 |
+
display: flex;
|
232 |
+
justify-content: space-between;
|
233 |
+
color: rgb(166, 166, 166);
|
234 |
+
font-size: 12px;
|
235 |
+
margin-top: 8px;
|
236 |
+
animation: slide-in ease 0.3s;
|
237 |
+
}
|
238 |
+
|
239 |
+
.chat-item-count,
|
240 |
+
.chat-item-date {
|
241 |
+
overflow: hidden;
|
242 |
+
text-overflow: ellipsis;
|
243 |
+
white-space: nowrap;
|
244 |
+
}
|
245 |
+
|
246 |
+
.narrow-sidebar {
|
247 |
+
.sidebar-title,
|
248 |
+
.sidebar-sub-title {
|
249 |
+
display: none;
|
250 |
+
}
|
251 |
+
.sidebar-logo {
|
252 |
+
position: relative;
|
253 |
+
display: flex;
|
254 |
+
justify-content: center;
|
255 |
+
}
|
256 |
+
|
257 |
+
.sidebar-header-bar {
|
258 |
+
flex-direction: column;
|
259 |
+
|
260 |
+
.sidebar-bar-button {
|
261 |
+
&:not(:last-child) {
|
262 |
+
margin-right: 0;
|
263 |
+
margin-bottom: 10px;
|
264 |
+
}
|
265 |
+
}
|
266 |
+
}
|
267 |
+
|
268 |
+
.chat-item {
|
269 |
+
padding: 0;
|
270 |
+
min-height: 50px;
|
271 |
+
display: flex;
|
272 |
+
justify-content: center;
|
273 |
+
align-items: center;
|
274 |
+
transition: all ease 0.3s;
|
275 |
+
overflow: hidden;
|
276 |
+
|
277 |
+
&:hover {
|
278 |
+
.chat-item-narrow {
|
279 |
+
transform: scale(0.7) translateX(-50%);
|
280 |
+
}
|
281 |
+
}
|
282 |
+
}
|
283 |
+
|
284 |
+
.chat-item-narrow {
|
285 |
+
line-height: 0;
|
286 |
+
font-weight: lighter;
|
287 |
+
color: var(--black);
|
288 |
+
transform: translateX(0);
|
289 |
+
transition: all ease 0.3s;
|
290 |
+
padding: 4px;
|
291 |
+
display: flex;
|
292 |
+
flex-direction: column;
|
293 |
+
justify-content: center;
|
294 |
+
|
295 |
+
.chat-item-avatar {
|
296 |
+
display: flex;
|
297 |
+
justify-content: center;
|
298 |
+
opacity: 0.2;
|
299 |
+
position: absolute;
|
300 |
+
transform: scale(4);
|
301 |
+
}
|
302 |
+
|
303 |
+
.chat-item-narrow-count {
|
304 |
+
font-size: 24px;
|
305 |
+
font-weight: bolder;
|
306 |
+
text-align: center;
|
307 |
+
color: var(--primary);
|
308 |
+
opacity: 0.6;
|
309 |
+
}
|
310 |
+
}
|
311 |
+
|
312 |
+
.sidebar-tail {
|
313 |
+
flex-direction: column-reverse;
|
314 |
+
align-items: center;
|
315 |
+
|
316 |
+
.sidebar-actions {
|
317 |
+
flex-direction: column-reverse;
|
318 |
+
align-items: center;
|
319 |
+
|
320 |
+
.sidebar-action {
|
321 |
+
margin-right: 0;
|
322 |
+
margin-top: 15px;
|
323 |
+
}
|
324 |
+
}
|
325 |
+
}
|
326 |
+
}
|
327 |
+
|
328 |
+
.sidebar-tail {
|
329 |
+
display: flex;
|
330 |
+
justify-content: space-between;
|
331 |
+
padding-top: 20px;
|
332 |
+
}
|
333 |
+
|
334 |
+
.sidebar-actions {
|
335 |
+
display: inline-flex;
|
336 |
+
}
|
337 |
+
|
338 |
+
.sidebar-action:not(:last-child) {
|
339 |
+
margin-right: 15px;
|
340 |
+
}
|
341 |
+
|
342 |
+
.loading-content {
|
343 |
+
display: flex;
|
344 |
+
flex-direction: column;
|
345 |
+
justify-content: center;
|
346 |
+
align-items: center;
|
347 |
+
height: 100%;
|
348 |
+
width: 100%;
|
349 |
+
}
|
350 |
+
|
351 |
+
.rtl-screen {
|
352 |
+
direction: rtl;
|
353 |
+
}
|
NeuroGPT/app/components/home.tsx
ADDED
@@ -0,0 +1,210 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
"use client";
|
2 |
+
|
3 |
+
require("../polyfill");
|
4 |
+
|
5 |
+
import { useState, useEffect } from "react";
|
6 |
+
|
7 |
+
import styles from "./home.module.scss";
|
8 |
+
|
9 |
+
import BotIcon from "../icons/bot.svg";
|
10 |
+
import LoadingIcon from "../icons/three-dots.svg";
|
11 |
+
|
12 |
+
import { getCSSVar, useMobileScreen } from "../utils";
|
13 |
+
|
14 |
+
import dynamic from "next/dynamic";
|
15 |
+
import { Path, SlotID, DEFAULT_CORS_HOST } from "../constant";
|
16 |
+
import { ErrorBoundary } from "./error";
|
17 |
+
|
18 |
+
import { getISOLang, getLang } from "../locales";
|
19 |
+
|
20 |
+
import {
|
21 |
+
HashRouter as Router,
|
22 |
+
Routes,
|
23 |
+
Route,
|
24 |
+
useLocation,
|
25 |
+
} from "react-router-dom";
|
26 |
+
import { SideBar } from "./sidebar";
|
27 |
+
import { useAppConfig } from "../store/config";
|
28 |
+
import { AuthPage } from "./auth";
|
29 |
+
import { getClientConfig } from "../config/client";
|
30 |
+
import { api } from "../client/api";
|
31 |
+
import { useAccessStore } from "../store";
|
32 |
+
|
33 |
+
export function Loading(props: { noLogo?: boolean }) {
|
34 |
+
return (
|
35 |
+
<div className={styles["loading-content"] + " no-dark"}>
|
36 |
+
{!props.noLogo && <BotIcon />}
|
37 |
+
<LoadingIcon />
|
38 |
+
</div>
|
39 |
+
);
|
40 |
+
}
|
41 |
+
|
42 |
+
const Settings = dynamic(async () => (await import("./settings")).Settings, {
|
43 |
+
loading: () => <Loading noLogo />,
|
44 |
+
});
|
45 |
+
|
46 |
+
const Chat = dynamic(async () => (await import("./chat")).Chat, {
|
47 |
+
loading: () => <Loading noLogo />,
|
48 |
+
});
|
49 |
+
|
50 |
+
const NewChat = dynamic(async () => (await import("./new-chat")).NewChat, {
|
51 |
+
loading: () => <Loading noLogo />,
|
52 |
+
});
|
53 |
+
|
54 |
+
const MaskPage = dynamic(async () => (await import("./mask")).MaskPage, {
|
55 |
+
loading: () => <Loading noLogo />,
|
56 |
+
});
|
57 |
+
|
58 |
+
const PrivacyPage = dynamic(async () => (await import("./privacy")).PrivacyPage, {
|
59 |
+
loading: () => <Loading noLogo />,
|
60 |
+
});
|
61 |
+
|
62 |
+
const ChangeLog = dynamic(async () => (await import("./changelog")).ChangeLog, {
|
63 |
+
loading: () => <Loading noLogo />,
|
64 |
+
});
|
65 |
+
|
66 |
+
export function useSwitchTheme() {
|
67 |
+
const config = useAppConfig();
|
68 |
+
|
69 |
+
useEffect(() => {
|
70 |
+
document.body.classList.remove("light");
|
71 |
+
document.body.classList.remove("dark");
|
72 |
+
|
73 |
+
if (config.theme === "dark") {
|
74 |
+
document.body.classList.add("dark");
|
75 |
+
} else if (config.theme === "light") {
|
76 |
+
document.body.classList.add("light");
|
77 |
+
}
|
78 |
+
|
79 |
+
const metaDescriptionDark = document.querySelector(
|
80 |
+
'meta[name="theme-color"][media*="dark"]',
|
81 |
+
);
|
82 |
+
const metaDescriptionLight = document.querySelector(
|
83 |
+
'meta[name="theme-color"][media*="light"]',
|
84 |
+
);
|
85 |
+
|
86 |
+
if (config.theme === "auto") {
|
87 |
+
metaDescriptionDark?.setAttribute("content", "#151515");
|
88 |
+
metaDescriptionLight?.setAttribute("content", "#fafafa");
|
89 |
+
} else {
|
90 |
+
const themeColor = getCSSVar("--theme-color");
|
91 |
+
metaDescriptionDark?.setAttribute("content", themeColor);
|
92 |
+
metaDescriptionLight?.setAttribute("content", themeColor);
|
93 |
+
}
|
94 |
+
}, [config.theme]);
|
95 |
+
}
|
96 |
+
|
97 |
+
function useHtmlLang() {
|
98 |
+
useEffect(() => {
|
99 |
+
const lang = getISOLang();
|
100 |
+
const htmlLang = document.documentElement.lang;
|
101 |
+
|
102 |
+
if (lang !== htmlLang) {
|
103 |
+
document.documentElement.lang = lang;
|
104 |
+
}
|
105 |
+
}, []);
|
106 |
+
}
|
107 |
+
|
108 |
+
const useHasHydrated = () => {
|
109 |
+
const [hasHydrated, setHasHydrated] = useState<boolean>(false);
|
110 |
+
|
111 |
+
useEffect(() => {
|
112 |
+
setHasHydrated(true);
|
113 |
+
}, []);
|
114 |
+
|
115 |
+
return hasHydrated;
|
116 |
+
};
|
117 |
+
|
118 |
+
const loadAsyncGoogleFont = () => {
|
119 |
+
const linkEl = document.createElement("link");
|
120 |
+
const proxyFontUrl = "/google-fonts";
|
121 |
+
const remoteFontUrl = `${DEFAULT_CORS_HOST}/google-fonts`; // we use proxy in client app
|
122 |
+
const googleFontUrl =
|
123 |
+
getClientConfig()?.buildMode === "export" ? remoteFontUrl : proxyFontUrl;
|
124 |
+
linkEl.rel = "stylesheet";
|
125 |
+
const fontFamilies = encodeURIComponent("Noto Sans:wght@300;400;700;900");
|
126 |
+
const fontUrl = `${googleFontUrl}/css2?family=${fontFamilies}&display=swap`;
|
127 |
+
linkEl.href = fontUrl;
|
128 |
+
document.head.appendChild(linkEl);
|
129 |
+
};
|
130 |
+
|
131 |
+
function Screen() {
|
132 |
+
const config = useAppConfig();
|
133 |
+
const location = useLocation();
|
134 |
+
const isHome = location.pathname === Path.Home;
|
135 |
+
const isAuth = location.pathname === Path.Auth;
|
136 |
+
const isMobileScreen = useMobileScreen();
|
137 |
+
const shouldTightBorder = getClientConfig()?.isApp || (config.tightBorder && !isMobileScreen);
|
138 |
+
|
139 |
+
useEffect(() => {
|
140 |
+
loadAsyncGoogleFont();
|
141 |
+
}, []);
|
142 |
+
|
143 |
+
return (
|
144 |
+
<div
|
145 |
+
className={
|
146 |
+
styles.container +
|
147 |
+
` ${shouldTightBorder ? styles["tight-container"] : styles.container} ${
|
148 |
+
getLang() === "ar" ? styles["rtl-screen"] : ""
|
149 |
+
}`
|
150 |
+
}
|
151 |
+
>
|
152 |
+
{isAuth ? (
|
153 |
+
<>
|
154 |
+
<AuthPage />
|
155 |
+
</>
|
156 |
+
) : (
|
157 |
+
<>
|
158 |
+
<SideBar className={isHome ? styles["sidebar-show"] : ""} />
|
159 |
+
|
160 |
+
<div className={styles["window-content"]} id={SlotID.AppBody}>
|
161 |
+
<Routes>
|
162 |
+
<Route path={Path.Home} element={<Chat />} />
|
163 |
+
<Route path={Path.NewChat} element={<NewChat />} />
|
164 |
+
<Route path={Path.Masks} element={<MaskPage />} />
|
165 |
+
<Route path={Path.Chat} element={<Chat />} />
|
166 |
+
<Route path={Path.PrivacyPage} element={<PrivacyPage />} />
|
167 |
+
<Route path={Path.ChangeLog} element={<ChangeLog />} />
|
168 |
+
<Route path={Path.Settings} element={<Settings />} />
|
169 |
+
</Routes>
|
170 |
+
</div>
|
171 |
+
</>
|
172 |
+
)}
|
173 |
+
</div>
|
174 |
+
);
|
175 |
+
}
|
176 |
+
|
177 |
+
export function useLoadData() {
|
178 |
+
const config = useAppConfig();
|
179 |
+
|
180 |
+
useEffect(() => {
|
181 |
+
(async () => {
|
182 |
+
const models = await api.llm.models();
|
183 |
+
config.mergeModels(models);
|
184 |
+
})();
|
185 |
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
186 |
+
}, []);
|
187 |
+
}
|
188 |
+
|
189 |
+
export function Home() {
|
190 |
+
useSwitchTheme();
|
191 |
+
useLoadData();
|
192 |
+
useHtmlLang();
|
193 |
+
|
194 |
+
useEffect(() => {
|
195 |
+
console.log("[Config] got config from build time", getClientConfig());
|
196 |
+
useAccessStore.getState().fetch();
|
197 |
+
}, []);
|
198 |
+
|
199 |
+
if (!useHasHydrated()) {
|
200 |
+
return <Loading />;
|
201 |
+
}
|
202 |
+
|
203 |
+
return (
|
204 |
+
<ErrorBoundary>
|
205 |
+
<Router>
|
206 |
+
<Screen />
|
207 |
+
</Router>
|
208 |
+
</ErrorBoundary>
|
209 |
+
);
|
210 |
+
}
|
NeuroGPT/app/components/input-range.module.scss
ADDED
@@ -0,0 +1,13 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
.input-range {
|
2 |
+
border: var(--border-in-light);
|
3 |
+
border-radius: 10px;
|
4 |
+
padding: 5px 10px 5px 10px;
|
5 |
+
font-size: 12px;
|
6 |
+
display: flex;
|
7 |
+
justify-content: space-between;
|
8 |
+
max-width: 40%;
|
9 |
+
|
10 |
+
input[type="range"] {
|
11 |
+
max-width: calc(100% - 34px);
|
12 |
+
}
|
13 |
+
}
|
NeuroGPT/app/components/input-range.tsx
ADDED
@@ -0,0 +1,37 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import * as React from "react";
|
2 |
+
import styles from "./input-range.module.scss";
|
3 |
+
|
4 |
+
interface InputRangeProps {
|
5 |
+
onChange: React.ChangeEventHandler<HTMLInputElement>;
|
6 |
+
title?: string;
|
7 |
+
value: number | string;
|
8 |
+
className?: string;
|
9 |
+
min: string;
|
10 |
+
max: string;
|
11 |
+
step: string;
|
12 |
+
}
|
13 |
+
|
14 |
+
export function InputRange({
|
15 |
+
onChange,
|
16 |
+
title,
|
17 |
+
value,
|
18 |
+
className,
|
19 |
+
min,
|
20 |
+
max,
|
21 |
+
step,
|
22 |
+
}: InputRangeProps) {
|
23 |
+
return (
|
24 |
+
<div className={styles["input-range"] + ` ${className ?? ""}`}>
|
25 |
+
{title || value}
|
26 |
+
<input
|
27 |
+
type="range"
|
28 |
+
title={title}
|
29 |
+
value={value}
|
30 |
+
min={min}
|
31 |
+
max={max}
|
32 |
+
step={step}
|
33 |
+
onChange={onChange}
|
34 |
+
></input>
|
35 |
+
</div>
|
36 |
+
);
|
37 |
+
}
|
NeuroGPT/app/components/markdown.tsx
ADDED
@@ -0,0 +1,212 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import ReactMarkdown from "react-markdown";
|
2 |
+
import "katex/dist/katex.min.css";
|
3 |
+
import RemarkMath from "remark-math";
|
4 |
+
import RemarkBreaks from "remark-breaks";
|
5 |
+
import RehypeKatex from "rehype-katex";
|
6 |
+
import RemarkGfm from "remark-gfm";
|
7 |
+
import RehypeHighlight from "rehype-highlight";
|
8 |
+
import { useRef, useState, RefObject, useEffect, useMemo } from "react";
|
9 |
+
import { copyToClipboard } from "../utils";
|
10 |
+
import mermaid from "mermaid";
|
11 |
+
|
12 |
+
import LoadingIcon from "../icons/three-dots.svg";
|
13 |
+
import React from "react";
|
14 |
+
import { useDebouncedCallback, useThrottledCallback } from "use-debounce";
|
15 |
+
import { showImageModal } from "./ui-lib";
|
16 |
+
import { isIOS, isMacOS } from "../utils"; // Import the isIOS & isMacOS functions from the utils file
|
17 |
+
|
18 |
+
export function Mermaid(props: { code: string }) {
|
19 |
+
const ref = useRef<HTMLDivElement>(null);
|
20 |
+
const [hasError, setHasError] = useState(false);
|
21 |
+
|
22 |
+
useEffect(() => {
|
23 |
+
if (props.code && ref.current) {
|
24 |
+
mermaid
|
25 |
+
.run({
|
26 |
+
nodes: [ref.current],
|
27 |
+
suppressErrors: true,
|
28 |
+
})
|
29 |
+
.catch((e) => {
|
30 |
+
setHasError(true);
|
31 |
+
console.error("[Mermaid] ", e.message);
|
32 |
+
});
|
33 |
+
}
|
34 |
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
35 |
+
}, [props.code]);
|
36 |
+
|
37 |
+
function viewSvgInNewWindow() {
|
38 |
+
const svg = ref.current?.querySelector("svg");
|
39 |
+
if (!svg) return;
|
40 |
+
const text = new XMLSerializer().serializeToString(svg);
|
41 |
+
const blob = new Blob([text], { type: "image/svg+xml" });
|
42 |
+
showImageModal(URL.createObjectURL(blob));
|
43 |
+
}
|
44 |
+
|
45 |
+
if (hasError) {
|
46 |
+
return null;
|
47 |
+
}
|
48 |
+
|
49 |
+
return (
|
50 |
+
<div
|
51 |
+
className="no-dark mermaid"
|
52 |
+
style={{
|
53 |
+
cursor: "pointer",
|
54 |
+
overflow: "auto",
|
55 |
+
}}
|
56 |
+
ref={ref}
|
57 |
+
onClick={() => viewSvgInNewWindow()}
|
58 |
+
>
|
59 |
+
{props.code}
|
60 |
+
</div>
|
61 |
+
);
|
62 |
+
}
|
63 |
+
|
64 |
+
export function PreCode(props: { children: any }) {
|
65 |
+
const ref = useRef<HTMLPreElement>(null);
|
66 |
+
const refText = ref.current?.innerText;
|
67 |
+
const [mermaidCode, setMermaidCode] = useState("");
|
68 |
+
|
69 |
+
const renderMermaid = useDebouncedCallback(() => {
|
70 |
+
if (!ref.current) return;
|
71 |
+
const mermaidDom = ref.current.querySelector("code.language-mermaid");
|
72 |
+
if (mermaidDom) {
|
73 |
+
setMermaidCode((mermaidDom as HTMLElement).innerText);
|
74 |
+
}
|
75 |
+
}, 600);
|
76 |
+
|
77 |
+
useEffect(() => {
|
78 |
+
setTimeout(renderMermaid, 1);
|
79 |
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
80 |
+
}, [refText]);
|
81 |
+
|
82 |
+
return (
|
83 |
+
<>
|
84 |
+
{mermaidCode.length > 0 && (
|
85 |
+
<Mermaid code={mermaidCode} key={mermaidCode} />
|
86 |
+
)}
|
87 |
+
<pre ref={ref}>
|
88 |
+
<span
|
89 |
+
className="copy-code-button"
|
90 |
+
onClick={() => {
|
91 |
+
if (ref.current) {
|
92 |
+
const code = ref.current.innerText;
|
93 |
+
copyToClipboard(code);
|
94 |
+
}
|
95 |
+
}}
|
96 |
+
></span>
|
97 |
+
{props.children}
|
98 |
+
</pre>
|
99 |
+
</>
|
100 |
+
);
|
101 |
+
}
|
102 |
+
|
103 |
+
function escapeMarkdownContent(content: string): string {
|
104 |
+
const userAgent = navigator.userAgent.toLowerCase();
|
105 |
+
const isAppleIosDevice = isIOS() || isMacOS(); // Load isAppleDevice from isIOS functions
|
106 |
+
// According to this post: https://www.drupal.org/project/next_webform/issues/3358901
|
107 |
+
// iOS 16.4 is the first version to support lookbehind
|
108 |
+
const iosVersionSupportsLookBehind = 16.4;
|
109 |
+
let doesIosSupportLookBehind = false;
|
110 |
+
|
111 |
+
if (isAppleIosDevice) {
|
112 |
+
const match = /os (\d+([_.]\d+)+)/.exec(userAgent);
|
113 |
+
if (match && match[1]) {
|
114 |
+
const iosVersion = parseFloat(match[1].replace("_", "."));
|
115 |
+
doesIosSupportLookBehind = iosVersion >= iosVersionSupportsLookBehind;
|
116 |
+
}
|
117 |
+
}
|
118 |
+
|
119 |
+
if (isAppleIosDevice && !doesIosSupportLookBehind) {
|
120 |
+
return content.replace(
|
121 |
+
// Exclude code blocks & math block from replacement
|
122 |
+
// custom-regex for unsupported Apple devices
|
123 |
+
/(`{3}[\s\S]*?`{3}|`[^`]*`)|(\$(?!\$))/g,
|
124 |
+
(match, codeBlock) => {
|
125 |
+
if (codeBlock) {
|
126 |
+
return match; // Return the code block as it is
|
127 |
+
} else {
|
128 |
+
return "$"; // Escape dollar signs outside of code blocks
|
129 |
+
}
|
130 |
+
}
|
131 |
+
);
|
132 |
+
} else {
|
133 |
+
return content.replace(
|
134 |
+
// Exclude code blocks & math block from replacement
|
135 |
+
/(`{3}[\s\S]*?`{3}|`[^`]*`)|(?<!\$)(\$(?!\$))/g,
|
136 |
+
(match, codeBlock) => {
|
137 |
+
if (codeBlock) {
|
138 |
+
return match; // Return the code block as it is
|
139 |
+
} else {
|
140 |
+
return "$"; // Escape dollar signs outside of code blocks
|
141 |
+
}
|
142 |
+
}
|
143 |
+
);
|
144 |
+
}
|
145 |
+
}
|
146 |
+
|
147 |
+
function _MarkDownContent(props: { content: string }) {
|
148 |
+
const escapedContent = useMemo(() => escapeMarkdownContent(props.content), [
|
149 |
+
props.content,
|
150 |
+
]);
|
151 |
+
|
152 |
+
return (
|
153 |
+
<ReactMarkdown
|
154 |
+
remarkPlugins={[RemarkMath, RemarkGfm, RemarkBreaks]}
|
155 |
+
rehypePlugins={[
|
156 |
+
RehypeKatex,
|
157 |
+
[
|
158 |
+
RehypeHighlight,
|
159 |
+
{
|
160 |
+
detect: false,
|
161 |
+
ignoreMissing: true,
|
162 |
+
},
|
163 |
+
],
|
164 |
+
]}
|
165 |
+
components={{
|
166 |
+
pre: PreCode,
|
167 |
+
p: (pProps) => <p {...pProps} dir="auto" />,
|
168 |
+
a: (aProps) => {
|
169 |
+
const href = aProps.href || "";
|
170 |
+
const isInternal = /^\/#/i.test(href);
|
171 |
+
const target = isInternal ? "_self" : aProps.target ?? "_blank";
|
172 |
+
return <a {...aProps} target={target} />;
|
173 |
+
},
|
174 |
+
}}
|
175 |
+
>
|
176 |
+
{escapedContent}
|
177 |
+
</ReactMarkdown>
|
178 |
+
);
|
179 |
+
}
|
180 |
+
|
181 |
+
export const MarkdownContent = React.memo(_MarkDownContent);
|
182 |
+
|
183 |
+
export function Markdown(
|
184 |
+
props: {
|
185 |
+
content: string;
|
186 |
+
loading?: boolean;
|
187 |
+
fontSize?: number;
|
188 |
+
parentRef?: RefObject<HTMLDivElement>;
|
189 |
+
defaultShow?: boolean;
|
190 |
+
} & React.DOMAttributes<HTMLDivElement>,
|
191 |
+
) {
|
192 |
+
const mdRef = useRef<HTMLDivElement>(null);
|
193 |
+
|
194 |
+
return (
|
195 |
+
<div
|
196 |
+
className="markdown-body"
|
197 |
+
style={{
|
198 |
+
fontSize: `${props.fontSize ?? 14}px`,
|
199 |
+
}}
|
200 |
+
ref={mdRef}
|
201 |
+
onContextMenu={props.onContextMenu}
|
202 |
+
onDoubleClickCapture={props.onDoubleClickCapture}
|
203 |
+
dir="auto"
|
204 |
+
>
|
205 |
+
{props.loading ? (
|
206 |
+
<LoadingIcon />
|
207 |
+
) : (
|
208 |
+
<MarkdownContent content={props.content} />
|
209 |
+
)}
|
210 |
+
</div>
|
211 |
+
);
|
212 |
+
}
|
NeuroGPT/app/components/mask.module.scss
ADDED
@@ -0,0 +1,108 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
@import "../styles/animation.scss";
|
2 |
+
.mask-page {
|
3 |
+
height: 100%;
|
4 |
+
display: flex;
|
5 |
+
flex-direction: column;
|
6 |
+
|
7 |
+
.mask-page-body {
|
8 |
+
padding: 20px;
|
9 |
+
overflow-y: auto;
|
10 |
+
|
11 |
+
.mask-filter {
|
12 |
+
width: 100%;
|
13 |
+
max-width: 100%;
|
14 |
+
margin-bottom: 20px;
|
15 |
+
animation: slide-in ease 0.3s;
|
16 |
+
height: 40px;
|
17 |
+
|
18 |
+
display: flex;
|
19 |
+
|
20 |
+
.search-bar {
|
21 |
+
flex-grow: 1;
|
22 |
+
max-width: 100%;
|
23 |
+
min-width: 0;
|
24 |
+
}
|
25 |
+
|
26 |
+
.mask-filter-lang {
|
27 |
+
height: 100%;
|
28 |
+
margin-left: 10px;
|
29 |
+
}
|
30 |
+
|
31 |
+
.mask-create {
|
32 |
+
height: 100%;
|
33 |
+
margin-left: 10px;
|
34 |
+
box-sizing: border-box;
|
35 |
+
min-width: 80px;
|
36 |
+
}
|
37 |
+
}
|
38 |
+
|
39 |
+
.mask-item {
|
40 |
+
display: flex;
|
41 |
+
justify-content: space-between;
|
42 |
+
padding: 20px;
|
43 |
+
border: var(--border-in-light);
|
44 |
+
animation: slide-in ease 0.3s;
|
45 |
+
|
46 |
+
&:not(:last-child) {
|
47 |
+
border-bottom: 0;
|
48 |
+
}
|
49 |
+
|
50 |
+
&:first-child {
|
51 |
+
border-top-left-radius: 10px;
|
52 |
+
border-top-right-radius: 10px;
|
53 |
+
}
|
54 |
+
|
55 |
+
&:last-child {
|
56 |
+
border-bottom-left-radius: 10px;
|
57 |
+
border-bottom-right-radius: 10px;
|
58 |
+
}
|
59 |
+
|
60 |
+
.mask-header {
|
61 |
+
display: flex;
|
62 |
+
align-items: center;
|
63 |
+
|
64 |
+
.mask-icon {
|
65 |
+
display: flex;
|
66 |
+
align-items: center;
|
67 |
+
justify-content: center;
|
68 |
+
margin-right: 10px;
|
69 |
+
}
|
70 |
+
|
71 |
+
.mask-title {
|
72 |
+
.mask-name {
|
73 |
+
font-size: 14px;
|
74 |
+
font-weight: bold;
|
75 |
+
}
|
76 |
+
.mask-info {
|
77 |
+
font-size: 12px;
|
78 |
+
}
|
79 |
+
}
|
80 |
+
}
|
81 |
+
|
82 |
+
.mask-actions {
|
83 |
+
display: flex;
|
84 |
+
flex-wrap: nowrap;
|
85 |
+
transition: all ease 0.3s;
|
86 |
+
}
|
87 |
+
|
88 |
+
@media screen and (max-width: 600px) {
|
89 |
+
display: flex;
|
90 |
+
flex-direction: column;
|
91 |
+
padding-bottom: 10px;
|
92 |
+
border-radius: 10px;
|
93 |
+
margin-bottom: 20px;
|
94 |
+
box-shadow: var(--card-shadow);
|
95 |
+
|
96 |
+
&:not(:last-child) {
|
97 |
+
border-bottom: var(--border-in-light);
|
98 |
+
}
|
99 |
+
|
100 |
+
.mask-actions {
|
101 |
+
width: 100%;
|
102 |
+
justify-content: space-between;
|
103 |
+
padding-top: 10px;
|
104 |
+
}
|
105 |
+
}
|
106 |
+
}
|
107 |
+
}
|
108 |
+
}
|
NeuroGPT/app/components/mask.tsx
ADDED
@@ -0,0 +1,618 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import { IconButton } from "./button";
|
2 |
+
import { ErrorBoundary } from "./error";
|
3 |
+
|
4 |
+
import styles from "./mask.module.scss";
|
5 |
+
|
6 |
+
import DownloadIcon from "../icons/download.svg";
|
7 |
+
import UploadIcon from "../icons/upload.svg";
|
8 |
+
import EditIcon from "../icons/edit.svg";
|
9 |
+
import AddIcon from "../icons/add.svg";
|
10 |
+
import CloseIcon from "../icons/close.svg";
|
11 |
+
import DeleteIcon from "../icons/delete.svg";
|
12 |
+
import EyeIcon from "../icons/eye.svg";
|
13 |
+
import CopyIcon from "../icons/copy.svg";
|
14 |
+
import DragIcon from "../icons/drag.svg";
|
15 |
+
|
16 |
+
import { DEFAULT_MASK_AVATAR, Mask, useMaskStore } from "../store/mask";
|
17 |
+
import {
|
18 |
+
ChatMessage,
|
19 |
+
createMessage,
|
20 |
+
ModelConfig,
|
21 |
+
useAppConfig,
|
22 |
+
useChatStore,
|
23 |
+
} from "../store";
|
24 |
+
import { ROLES } from "../client/api";
|
25 |
+
import {
|
26 |
+
Input,
|
27 |
+
List,
|
28 |
+
ListItem,
|
29 |
+
Modal,
|
30 |
+
Popover,
|
31 |
+
Select,
|
32 |
+
showConfirm,
|
33 |
+
} from "./ui-lib";
|
34 |
+
import { Avatar, AvatarPicker } from "./emoji";
|
35 |
+
import Locale, { AllLangs, ALL_LANG_OPTIONS, Lang } from "../locales";
|
36 |
+
import { useNavigate } from "react-router-dom";
|
37 |
+
|
38 |
+
import chatStyle from "./chat.module.scss";
|
39 |
+
import { useEffect, useState } from "react";
|
40 |
+
import { copyToClipboard, downloadAs, readFromFile } from "../utils";
|
41 |
+
import { Updater } from "../typing";
|
42 |
+
import { ModelConfigList } from "./model-config";
|
43 |
+
import { FileName, Path } from "../constant";
|
44 |
+
import { BUILTIN_MASK_STORE } from "../masks";
|
45 |
+
import { nanoid } from "nanoid";
|
46 |
+
import {
|
47 |
+
DragDropContext,
|
48 |
+
Droppable,
|
49 |
+
Draggable,
|
50 |
+
OnDragEndResponder,
|
51 |
+
} from "@hello-pangea/dnd";
|
52 |
+
|
53 |
+
// drag and drop helper function
|
54 |
+
function reorder<T>(list: T[], startIndex: number, endIndex: number): T[] {
|
55 |
+
const result = [...list];
|
56 |
+
const [removed] = result.splice(startIndex, 1);
|
57 |
+
result.splice(endIndex, 0, removed);
|
58 |
+
return result;
|
59 |
+
}
|
60 |
+
|
61 |
+
export function MaskAvatar(props: { mask: Mask }) {
|
62 |
+
return props.mask.avatar !== DEFAULT_MASK_AVATAR ? (
|
63 |
+
<Avatar avatar={props.mask.avatar} />
|
64 |
+
) : (
|
65 |
+
<Avatar model={props.mask.modelConfig.model} />
|
66 |
+
);
|
67 |
+
}
|
68 |
+
|
69 |
+
export function MaskConfig(props: {
|
70 |
+
mask: Mask;
|
71 |
+
updateMask: Updater<Mask>;
|
72 |
+
extraListItems?: JSX.Element;
|
73 |
+
readonly?: boolean;
|
74 |
+
shouldSyncFromGlobal?: boolean;
|
75 |
+
}) {
|
76 |
+
const [showPicker, setShowPicker] = useState(false);
|
77 |
+
|
78 |
+
const updateConfig = (updater: (config: ModelConfig) => void) => {
|
79 |
+
if (props.readonly) return;
|
80 |
+
|
81 |
+
const config = { ...props.mask.modelConfig };
|
82 |
+
updater(config);
|
83 |
+
props.updateMask((mask) => {
|
84 |
+
mask.modelConfig = config;
|
85 |
+
// if user changed current session mask, it will disable auto sync
|
86 |
+
mask.syncGlobalConfig = false;
|
87 |
+
});
|
88 |
+
};
|
89 |
+
|
90 |
+
const copyMaskLink = () => {
|
91 |
+
const maskLink = `${location.protocol}//${location.host}/#${Path.NewChat}?mask=${props.mask.id}`;
|
92 |
+
copyToClipboard(maskLink);
|
93 |
+
};
|
94 |
+
|
95 |
+
const globalConfig = useAppConfig();
|
96 |
+
|
97 |
+
return (
|
98 |
+
<>
|
99 |
+
<ContextPrompts
|
100 |
+
context={props.mask.context}
|
101 |
+
updateContext={(updater) => {
|
102 |
+
const context = props.mask.context.slice();
|
103 |
+
updater(context);
|
104 |
+
props.updateMask((mask) => (mask.context = context));
|
105 |
+
}}
|
106 |
+
/>
|
107 |
+
|
108 |
+
<List>
|
109 |
+
<ListItem title={Locale.Mask.Config.Avatar}>
|
110 |
+
<Popover
|
111 |
+
content={
|
112 |
+
<AvatarPicker
|
113 |
+
onEmojiClick={(emoji) => {
|
114 |
+
props.updateMask((mask) => (mask.avatar = emoji));
|
115 |
+
setShowPicker(false);
|
116 |
+
}}
|
117 |
+
></AvatarPicker>
|
118 |
+
}
|
119 |
+
open={showPicker}
|
120 |
+
onClose={() => setShowPicker(false)}
|
121 |
+
>
|
122 |
+
<div
|
123 |
+
onClick={() => setShowPicker(true)}
|
124 |
+
style={{ cursor: "pointer" }}
|
125 |
+
>
|
126 |
+
<MaskAvatar mask={props.mask} />
|
127 |
+
</div>
|
128 |
+
</Popover>
|
129 |
+
</ListItem>
|
130 |
+
<ListItem title={Locale.Mask.Config.Name}>
|
131 |
+
<input
|
132 |
+
type="text"
|
133 |
+
value={props.mask.name}
|
134 |
+
onInput={(e) =>
|
135 |
+
props.updateMask((mask) => {
|
136 |
+
mask.name = e.currentTarget.value;
|
137 |
+
})
|
138 |
+
}
|
139 |
+
></input>
|
140 |
+
</ListItem>
|
141 |
+
<ListItem
|
142 |
+
title={Locale.Mask.Config.HideContext.Title}
|
143 |
+
subTitle={Locale.Mask.Config.HideContext.SubTitle}
|
144 |
+
>
|
145 |
+
<input
|
146 |
+
type="checkbox"
|
147 |
+
checked={props.mask.hideContext}
|
148 |
+
onChange={(e) => {
|
149 |
+
props.updateMask((mask) => {
|
150 |
+
mask.hideContext = e.currentTarget.checked;
|
151 |
+
});
|
152 |
+
}}
|
153 |
+
></input>
|
154 |
+
</ListItem>
|
155 |
+
|
156 |
+
{!props.shouldSyncFromGlobal ? (
|
157 |
+
<ListItem
|
158 |
+
title={Locale.Mask.Config.Share.Title}
|
159 |
+
subTitle={Locale.Mask.Config.Share.SubTitle}
|
160 |
+
>
|
161 |
+
<IconButton
|
162 |
+
icon={<CopyIcon />}
|
163 |
+
text={Locale.Mask.Config.Share.Action}
|
164 |
+
onClick={copyMaskLink}
|
165 |
+
/>
|
166 |
+
</ListItem>
|
167 |
+
) : null}
|
168 |
+
|
169 |
+
{props.shouldSyncFromGlobal ? (
|
170 |
+
<ListItem
|
171 |
+
title={Locale.Mask.Config.Sync.Title}
|
172 |
+
subTitle={Locale.Mask.Config.Sync.SubTitle}
|
173 |
+
>
|
174 |
+
<input
|
175 |
+
type="checkbox"
|
176 |
+
checked={props.mask.syncGlobalConfig}
|
177 |
+
onChange={async (e) => {
|
178 |
+
const checked = e.currentTarget.checked;
|
179 |
+
if (
|
180 |
+
checked &&
|
181 |
+
(await showConfirm(Locale.Mask.Config.Sync.Confirm))
|
182 |
+
) {
|
183 |
+
props.updateMask((mask) => {
|
184 |
+
mask.syncGlobalConfig = checked;
|
185 |
+
mask.modelConfig = { ...globalConfig.modelConfig };
|
186 |
+
});
|
187 |
+
} else if (!checked) {
|
188 |
+
props.updateMask((mask) => {
|
189 |
+
mask.syncGlobalConfig = checked;
|
190 |
+
});
|
191 |
+
}
|
192 |
+
}}
|
193 |
+
></input>
|
194 |
+
</ListItem>
|
195 |
+
) : null}
|
196 |
+
</List>
|
197 |
+
|
198 |
+
<List>
|
199 |
+
<ModelConfigList
|
200 |
+
modelConfig={{ ...props.mask.modelConfig }}
|
201 |
+
updateConfig={updateConfig}
|
202 |
+
/>
|
203 |
+
{props.extraListItems}
|
204 |
+
</List>
|
205 |
+
</>
|
206 |
+
);
|
207 |
+
}
|
208 |
+
|
209 |
+
function ContextPromptItem(props: {
|
210 |
+
index: number;
|
211 |
+
prompt: ChatMessage;
|
212 |
+
update: (prompt: ChatMessage) => void;
|
213 |
+
remove: () => void;
|
214 |
+
}) {
|
215 |
+
const [focusingInput, setFocusingInput] = useState(false);
|
216 |
+
|
217 |
+
return (
|
218 |
+
<div className={chatStyle["context-prompt-row"]}>
|
219 |
+
{!focusingInput && (
|
220 |
+
<>
|
221 |
+
<div className={chatStyle["context-drag"]}>
|
222 |
+
<DragIcon />
|
223 |
+
</div>
|
224 |
+
<Select
|
225 |
+
value={props.prompt.role}
|
226 |
+
className={chatStyle["context-role"]}
|
227 |
+
onChange={(e) =>
|
228 |
+
props.update({
|
229 |
+
...props.prompt,
|
230 |
+
role: e.target.value as any,
|
231 |
+
})
|
232 |
+
}
|
233 |
+
>
|
234 |
+
{ROLES.map((r) => (
|
235 |
+
<option key={r} value={r}>
|
236 |
+
{r}
|
237 |
+
</option>
|
238 |
+
))}
|
239 |
+
</Select>
|
240 |
+
</>
|
241 |
+
)}
|
242 |
+
<Input
|
243 |
+
value={props.prompt.content}
|
244 |
+
type="text"
|
245 |
+
className={chatStyle["context-content"]}
|
246 |
+
rows={focusingInput ? 5 : 1}
|
247 |
+
onFocus={() => setFocusingInput(true)}
|
248 |
+
onBlur={() => {
|
249 |
+
setFocusingInput(false);
|
250 |
+
// If the selection is not removed when the user loses focus, some
|
251 |
+
// extensions like "Translate" will always display a floating bar
|
252 |
+
window?.getSelection()?.removeAllRanges();
|
253 |
+
}}
|
254 |
+
onInput={(e) =>
|
255 |
+
props.update({
|
256 |
+
...props.prompt,
|
257 |
+
content: e.currentTarget.value as any,
|
258 |
+
})
|
259 |
+
}
|
260 |
+
/>
|
261 |
+
{!focusingInput && (
|
262 |
+
<IconButton
|
263 |
+
icon={<DeleteIcon />}
|
264 |
+
className={chatStyle["context-delete-button"]}
|
265 |
+
onClick={() => props.remove()}
|
266 |
+
bordered
|
267 |
+
/>
|
268 |
+
)}
|
269 |
+
</div>
|
270 |
+
);
|
271 |
+
}
|
272 |
+
|
273 |
+
export function ContextPrompts(props: {
|
274 |
+
context: ChatMessage[];
|
275 |
+
updateContext: (updater: (context: ChatMessage[]) => void) => void;
|
276 |
+
}) {
|
277 |
+
const context = props.context;
|
278 |
+
|
279 |
+
const addContextPrompt = (prompt: ChatMessage, i: number) => {
|
280 |
+
props.updateContext((context) => context.splice(i, 0, prompt));
|
281 |
+
};
|
282 |
+
|
283 |
+
const removeContextPrompt = (i: number) => {
|
284 |
+
props.updateContext((context) => context.splice(i, 1));
|
285 |
+
};
|
286 |
+
|
287 |
+
const updateContextPrompt = (i: number, prompt: ChatMessage) => {
|
288 |
+
props.updateContext((context) => (context[i] = prompt));
|
289 |
+
};
|
290 |
+
|
291 |
+
const onDragEnd: OnDragEndResponder = (result) => {
|
292 |
+
if (!result.destination) {
|
293 |
+
return;
|
294 |
+
}
|
295 |
+
const newContext = reorder(
|
296 |
+
context,
|
297 |
+
result.source.index,
|
298 |
+
result.destination.index,
|
299 |
+
);
|
300 |
+
props.updateContext((context) => {
|
301 |
+
context.splice(0, context.length, ...newContext);
|
302 |
+
});
|
303 |
+
};
|
304 |
+
|
305 |
+
return (
|
306 |
+
<>
|
307 |
+
<div className={chatStyle["context-prompt"]} style={{ marginBottom: 20 }}>
|
308 |
+
<DragDropContext onDragEnd={onDragEnd}>
|
309 |
+
<Droppable droppableId="context-prompt-list">
|
310 |
+
{(provided) => (
|
311 |
+
<div ref={provided.innerRef} {...provided.droppableProps}>
|
312 |
+
{context.map((c, i) => (
|
313 |
+
<Draggable
|
314 |
+
draggableId={c.id || i.toString()}
|
315 |
+
index={i}
|
316 |
+
key={c.id}
|
317 |
+
>
|
318 |
+
{(provided) => (
|
319 |
+
<div
|
320 |
+
ref={provided.innerRef}
|
321 |
+
{...provided.draggableProps}
|
322 |
+
{...provided.dragHandleProps}
|
323 |
+
>
|
324 |
+
<ContextPromptItem
|
325 |
+
index={i}
|
326 |
+
prompt={c}
|
327 |
+
update={(prompt) => updateContextPrompt(i, prompt)}
|
328 |
+
remove={() => removeContextPrompt(i)}
|
329 |
+
/>
|
330 |
+
<div
|
331 |
+
className={chatStyle["context-prompt-insert"]}
|
332 |
+
onClick={() => {
|
333 |
+
addContextPrompt(
|
334 |
+
createMessage({
|
335 |
+
role: "user",
|
336 |
+
content: "",
|
337 |
+
date: new Date().toLocaleString(),
|
338 |
+
}),
|
339 |
+
i + 1,
|
340 |
+
);
|
341 |
+
}}
|
342 |
+
>
|
343 |
+
<AddIcon />
|
344 |
+
</div>
|
345 |
+
</div>
|
346 |
+
)}
|
347 |
+
</Draggable>
|
348 |
+
))}
|
349 |
+
{provided.placeholder}
|
350 |
+
</div>
|
351 |
+
)}
|
352 |
+
</Droppable>
|
353 |
+
</DragDropContext>
|
354 |
+
|
355 |
+
{props.context.length === 0 && (
|
356 |
+
<div className={chatStyle["context-prompt-row"]}>
|
357 |
+
<IconButton
|
358 |
+
icon={<AddIcon />}
|
359 |
+
text={Locale.Context.Add}
|
360 |
+
bordered
|
361 |
+
className={chatStyle["context-prompt-button"]}
|
362 |
+
onClick={() =>
|
363 |
+
addContextPrompt(
|
364 |
+
createMessage({
|
365 |
+
role: "user",
|
366 |
+
content: "",
|
367 |
+
date: "",
|
368 |
+
}),
|
369 |
+
props.context.length,
|
370 |
+
)
|
371 |
+
}
|
372 |
+
/>
|
373 |
+
</div>
|
374 |
+
)}
|
375 |
+
</div>
|
376 |
+
</>
|
377 |
+
);
|
378 |
+
}
|
379 |
+
|
380 |
+
export function MaskPage() {
|
381 |
+
const navigate = useNavigate();
|
382 |
+
|
383 |
+
const maskStore = useMaskStore();
|
384 |
+
const chatStore = useChatStore();
|
385 |
+
|
386 |
+
const [filterLang, setFilterLang] = useState<Lang>();
|
387 |
+
|
388 |
+
const allMasks = maskStore
|
389 |
+
.getAll()
|
390 |
+
.filter((m) => !filterLang || m.lang === filterLang);
|
391 |
+
|
392 |
+
const [searchMasks, setSearchMasks] = useState<Mask[]>([]);
|
393 |
+
const [searchText, setSearchText] = useState("");
|
394 |
+
const masks = searchText.length > 0 ? searchMasks : allMasks;
|
395 |
+
|
396 |
+
// refactored already, now it accurate
|
397 |
+
const onSearch = (text: string) => {
|
398 |
+
setSearchText(text);
|
399 |
+
if (text.length > 0) {
|
400 |
+
const result = allMasks.filter((m) =>
|
401 |
+
m.name.toLowerCase().includes(text.toLowerCase())
|
402 |
+
);
|
403 |
+
setSearchMasks(result);
|
404 |
+
} else {
|
405 |
+
setSearchMasks(allMasks);
|
406 |
+
}
|
407 |
+
};
|
408 |
+
|
409 |
+
const [editingMaskId, setEditingMaskId] = useState<string | undefined>();
|
410 |
+
const editingMask =
|
411 |
+
maskStore.get(editingMaskId) ?? BUILTIN_MASK_STORE.get(editingMaskId);
|
412 |
+
const closeMaskModal = () => setEditingMaskId(undefined);
|
413 |
+
|
414 |
+
const downloadAll = () => {
|
415 |
+
downloadAs(
|
416 |
+
masks.filter((v) => !v.builtin),
|
417 |
+
FileName.Masks,
|
418 |
+
);
|
419 |
+
};
|
420 |
+
|
421 |
+
const importFromFile = () => {
|
422 |
+
readFromFile().then((content) => {
|
423 |
+
try {
|
424 |
+
const importMasks = JSON.parse(content);
|
425 |
+
if (Array.isArray(importMasks)) {
|
426 |
+
for (const mask of importMasks) {
|
427 |
+
if (mask.name) {
|
428 |
+
maskStore.create(mask);
|
429 |
+
}
|
430 |
+
}
|
431 |
+
return;
|
432 |
+
}
|
433 |
+
//if the content is a single mask.
|
434 |
+
if (importMasks.name) {
|
435 |
+
maskStore.create(importMasks);
|
436 |
+
}
|
437 |
+
} catch {}
|
438 |
+
});
|
439 |
+
};
|
440 |
+
|
441 |
+
return (
|
442 |
+
<ErrorBoundary>
|
443 |
+
<div className={styles["mask-page"]}>
|
444 |
+
<div className="window-header">
|
445 |
+
<div className="window-header-title">
|
446 |
+
<div className="window-header-main-title">
|
447 |
+
{Locale.Mask.Page.Title}
|
448 |
+
</div>
|
449 |
+
<div className="window-header-submai-title">
|
450 |
+
{Locale.Mask.Page.SubTitle(allMasks.length)}
|
451 |
+
</div>
|
452 |
+
</div>
|
453 |
+
|
454 |
+
<div className="window-actions">
|
455 |
+
<div className="window-action-button">
|
456 |
+
<IconButton
|
457 |
+
icon={<DownloadIcon />}
|
458 |
+
bordered
|
459 |
+
onClick={downloadAll}
|
460 |
+
text={Locale.UI.Export}
|
461 |
+
/>
|
462 |
+
</div>
|
463 |
+
<div className="window-action-button">
|
464 |
+
<IconButton
|
465 |
+
icon={<UploadIcon />}
|
466 |
+
text={Locale.UI.Import}
|
467 |
+
bordered
|
468 |
+
onClick={() => importFromFile()}
|
469 |
+
/>
|
470 |
+
</div>
|
471 |
+
<div className="window-action-button">
|
472 |
+
<IconButton
|
473 |
+
icon={<CloseIcon />}
|
474 |
+
bordered
|
475 |
+
onClick={() => navigate(-1)}
|
476 |
+
/>
|
477 |
+
</div>
|
478 |
+
</div>
|
479 |
+
</div>
|
480 |
+
|
481 |
+
<div className={styles["mask-page-body"]}>
|
482 |
+
<div className={styles["mask-filter"]}>
|
483 |
+
<input
|
484 |
+
type="text"
|
485 |
+
className={styles["search-bar"]}
|
486 |
+
placeholder={Locale.Mask.Page.Search}
|
487 |
+
autoFocus
|
488 |
+
onInput={(e) => onSearch(e.currentTarget.value)}
|
489 |
+
/>
|
490 |
+
<Select
|
491 |
+
className={styles["mask-filter-lang"]}
|
492 |
+
value={filterLang ?? Locale.Settings.Lang.All}
|
493 |
+
onChange={(e) => {
|
494 |
+
const value = e.currentTarget.value;
|
495 |
+
if (value === Locale.Settings.Lang.All) {
|
496 |
+
setFilterLang(undefined);
|
497 |
+
} else {
|
498 |
+
setFilterLang(value as Lang);
|
499 |
+
}
|
500 |
+
}}
|
501 |
+
>
|
502 |
+
<option key="all" value={Locale.Settings.Lang.All}>
|
503 |
+
{Locale.Settings.Lang.All}
|
504 |
+
</option>
|
505 |
+
{AllLangs.map((lang) => (
|
506 |
+
<option value={lang} key={lang}>
|
507 |
+
{ALL_LANG_OPTIONS[lang]}
|
508 |
+
</option>
|
509 |
+
))}
|
510 |
+
</Select>
|
511 |
+
|
512 |
+
<IconButton
|
513 |
+
className={styles["mask-create"]}
|
514 |
+
icon={<AddIcon />}
|
515 |
+
text={Locale.Mask.Page.Create}
|
516 |
+
bordered
|
517 |
+
onClick={() => {
|
518 |
+
const createdMask = maskStore.create();
|
519 |
+
setEditingMaskId(createdMask.id);
|
520 |
+
}}
|
521 |
+
/>
|
522 |
+
</div>
|
523 |
+
|
524 |
+
<div>
|
525 |
+
{masks.map((m) => (
|
526 |
+
<div className={styles["mask-item"]} key={m.id}>
|
527 |
+
<div className={styles["mask-header"]}>
|
528 |
+
<div className={styles["mask-icon"]}>
|
529 |
+
<MaskAvatar mask={m} />
|
530 |
+
</div>
|
531 |
+
<div className={styles["mask-title"]}>
|
532 |
+
<div className={styles["mask-name"]}>{m.name}</div>
|
533 |
+
<div className={styles["mask-info"] + " one-line"}>
|
534 |
+
{`${Locale.Mask.Item.Info(m.context.length)} / ${
|
535 |
+
ALL_LANG_OPTIONS[m.lang]
|
536 |
+
} / ${m.modelConfig.model}`}
|
537 |
+
</div>
|
538 |
+
</div>
|
539 |
+
</div>
|
540 |
+
<div className={styles["mask-actions"]}>
|
541 |
+
<IconButton
|
542 |
+
icon={<AddIcon />}
|
543 |
+
text={Locale.Mask.Item.Chat}
|
544 |
+
onClick={() => {
|
545 |
+
chatStore.newSession(m);
|
546 |
+
navigate(Path.Chat);
|
547 |
+
}}
|
548 |
+
/>
|
549 |
+
{m.builtin ? (
|
550 |
+
<IconButton
|
551 |
+
icon={<EyeIcon />}
|
552 |
+
text={Locale.Mask.Item.View}
|
553 |
+
onClick={() => setEditingMaskId(m.id)}
|
554 |
+
/>
|
555 |
+
) : (
|
556 |
+
<IconButton
|
557 |
+
icon={<EditIcon />}
|
558 |
+
text={Locale.Mask.Item.Edit}
|
559 |
+
onClick={() => setEditingMaskId(m.id)}
|
560 |
+
/>
|
561 |
+
)}
|
562 |
+
{!m.builtin && (
|
563 |
+
<IconButton
|
564 |
+
icon={<DeleteIcon />}
|
565 |
+
text={Locale.Mask.Item.Delete}
|
566 |
+
onClick={async () => {
|
567 |
+
if (await showConfirm(Locale.Mask.Item.DeleteConfirm)) {
|
568 |
+
maskStore.delete(m.id);
|
569 |
+
}
|
570 |
+
}}
|
571 |
+
/>
|
572 |
+
)}
|
573 |
+
</div>
|
574 |
+
</div>
|
575 |
+
))}
|
576 |
+
</div>
|
577 |
+
</div>
|
578 |
+
</div>
|
579 |
+
|
580 |
+
{editingMask && (
|
581 |
+
<div className="modal-mask">
|
582 |
+
<Modal
|
583 |
+
title={Locale.Mask.EditModal.Title(editingMask?.builtin)}
|
584 |
+
onClose={closeMaskModal}
|
585 |
+
actions={[
|
586 |
+
<IconButton
|
587 |
+
icon={<DownloadIcon />}
|
588 |
+
text={Locale.Mask.EditModal.Download}
|
589 |
+
key="export"
|
590 |
+
bordered
|
591 |
+
onClick={() => downloadAs(editingMask, `${editingMask.name}.json`)}
|
592 |
+
/>,
|
593 |
+
<IconButton
|
594 |
+
key="copy"
|
595 |
+
icon={<CopyIcon />}
|
596 |
+
bordered
|
597 |
+
text={Locale.Mask.EditModal.Clone}
|
598 |
+
onClick={() => {
|
599 |
+
navigate(Path.Masks);
|
600 |
+
maskStore.create(editingMask);
|
601 |
+
setEditingMaskId(undefined);
|
602 |
+
}}
|
603 |
+
/>,
|
604 |
+
]}
|
605 |
+
>
|
606 |
+
<MaskConfig
|
607 |
+
mask={editingMask}
|
608 |
+
updateMask={(updater) =>
|
609 |
+
maskStore.updateMask(editingMaskId!, updater)
|
610 |
+
}
|
611 |
+
readonly={editingMask.builtin}
|
612 |
+
/>
|
613 |
+
</Modal>
|
614 |
+
</div>
|
615 |
+
)}
|
616 |
+
</ErrorBoundary>
|
617 |
+
);
|
618 |
+
}
|
NeuroGPT/app/components/message-selector.module.scss
ADDED
@@ -0,0 +1,76 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
.message-selector {
|
2 |
+
.message-filter {
|
3 |
+
display: flex;
|
4 |
+
|
5 |
+
.search-bar {
|
6 |
+
max-width: unset;
|
7 |
+
flex-grow: 1;
|
8 |
+
margin-right: 10px;
|
9 |
+
}
|
10 |
+
|
11 |
+
.actions {
|
12 |
+
display: flex;
|
13 |
+
|
14 |
+
button:not(:last-child) {
|
15 |
+
margin-right: 10px;
|
16 |
+
}
|
17 |
+
}
|
18 |
+
|
19 |
+
@media screen and (max-width: 600px) {
|
20 |
+
flex-direction: column;
|
21 |
+
|
22 |
+
.search-bar {
|
23 |
+
margin-right: 0;
|
24 |
+
}
|
25 |
+
|
26 |
+
.actions {
|
27 |
+
margin-top: 20px;
|
28 |
+
|
29 |
+
button {
|
30 |
+
flex-grow: 1;
|
31 |
+
}
|
32 |
+
}
|
33 |
+
}
|
34 |
+
}
|
35 |
+
|
36 |
+
.messages {
|
37 |
+
margin-top: 20px;
|
38 |
+
border-radius: 10px;
|
39 |
+
border: var(--border-in-light);
|
40 |
+
overflow: hidden;
|
41 |
+
|
42 |
+
.message {
|
43 |
+
display: flex;
|
44 |
+
align-items: center;
|
45 |
+
padding: 8px 10px;
|
46 |
+
cursor: pointer;
|
47 |
+
|
48 |
+
&-selected {
|
49 |
+
background-color: var(--second);
|
50 |
+
}
|
51 |
+
|
52 |
+
&:not(:last-child) {
|
53 |
+
border-bottom: var(--border-in-light);
|
54 |
+
}
|
55 |
+
|
56 |
+
.avatar {
|
57 |
+
margin-right: 10px;
|
58 |
+
}
|
59 |
+
|
60 |
+
.body {
|
61 |
+
flex-grow: 1;
|
62 |
+
max-width: calc(100% - 40px);
|
63 |
+
|
64 |
+
.date {
|
65 |
+
font-size: 12px;
|
66 |
+
line-height: 1.2;
|
67 |
+
opacity: 0.5;
|
68 |
+
}
|
69 |
+
|
70 |
+
.content {
|
71 |
+
font-size: 12px;
|
72 |
+
}
|
73 |
+
}
|
74 |
+
}
|
75 |
+
}
|
76 |
+
}
|
NeuroGPT/app/components/message-selector.tsx
ADDED
@@ -0,0 +1,215 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import { useEffect, useState } from "react";
|
2 |
+
import { ChatMessage, useAppConfig, useChatStore } from "../store";
|
3 |
+
import { Updater } from "../typing";
|
4 |
+
import { IconButton } from "./button";
|
5 |
+
import { Avatar } from "./emoji";
|
6 |
+
import { MaskAvatar } from "./mask";
|
7 |
+
import Locale from "../locales";
|
8 |
+
|
9 |
+
import styles from "./message-selector.module.scss";
|
10 |
+
|
11 |
+
function useShiftRange() {
|
12 |
+
const [startIndex, setStartIndex] = useState<number>();
|
13 |
+
const [endIndex, setEndIndex] = useState<number>();
|
14 |
+
const [shiftDown, setShiftDown] = useState(false);
|
15 |
+
|
16 |
+
const onClickIndex = (index: number) => {
|
17 |
+
if (shiftDown && startIndex !== undefined) {
|
18 |
+
setEndIndex(index);
|
19 |
+
} else {
|
20 |
+
setStartIndex(index);
|
21 |
+
setEndIndex(undefined);
|
22 |
+
}
|
23 |
+
};
|
24 |
+
|
25 |
+
useEffect(() => {
|
26 |
+
const onKeyDown = (e: KeyboardEvent) => {
|
27 |
+
if (e.key !== "Shift") return;
|
28 |
+
setShiftDown(true);
|
29 |
+
};
|
30 |
+
const onKeyUp = (e: KeyboardEvent) => {
|
31 |
+
if (e.key !== "Shift") return;
|
32 |
+
setShiftDown(false);
|
33 |
+
setStartIndex(undefined);
|
34 |
+
setEndIndex(undefined);
|
35 |
+
};
|
36 |
+
|
37 |
+
window.addEventListener("keyup", onKeyUp);
|
38 |
+
window.addEventListener("keydown", onKeyDown);
|
39 |
+
|
40 |
+
return () => {
|
41 |
+
window.removeEventListener("keyup", onKeyUp);
|
42 |
+
window.removeEventListener("keydown", onKeyDown);
|
43 |
+
};
|
44 |
+
}, []);
|
45 |
+
|
46 |
+
return {
|
47 |
+
onClickIndex,
|
48 |
+
startIndex,
|
49 |
+
endIndex,
|
50 |
+
};
|
51 |
+
}
|
52 |
+
|
53 |
+
export function useMessageSelector() {
|
54 |
+
const [selection, setSelection] = useState(new Set<string>());
|
55 |
+
const updateSelection: Updater<Set<string>> = (updater) => {
|
56 |
+
const newSelection = new Set<string>(selection);
|
57 |
+
updater(newSelection);
|
58 |
+
setSelection(newSelection);
|
59 |
+
};
|
60 |
+
|
61 |
+
return {
|
62 |
+
selection,
|
63 |
+
updateSelection,
|
64 |
+
};
|
65 |
+
}
|
66 |
+
|
67 |
+
export function MessageSelector(props: {
|
68 |
+
selection: Set<string>;
|
69 |
+
updateSelection: Updater<Set<string>>;
|
70 |
+
defaultSelectAll?: boolean;
|
71 |
+
onSelected?: (messages: ChatMessage[]) => void;
|
72 |
+
}) {
|
73 |
+
const chatStore = useChatStore();
|
74 |
+
const session = chatStore.currentSession();
|
75 |
+
const isValid = (m: ChatMessage) => m.content && !m.isError && !m.streaming;
|
76 |
+
const messages = session.messages.filter(
|
77 |
+
(m, i) =>
|
78 |
+
m.id && // message must have id
|
79 |
+
isValid(m) &&
|
80 |
+
(i >= session.messages.length - 1 || isValid(session.messages[i + 1])),
|
81 |
+
);
|
82 |
+
const messageCount = messages.length;
|
83 |
+
const config = useAppConfig();
|
84 |
+
|
85 |
+
const [searchInput, setSearchInput] = useState("");
|
86 |
+
const [searchIds, setSearchIds] = useState(new Set<string>());
|
87 |
+
const isInSearchResult = (id: string) => {
|
88 |
+
return searchInput.length === 0 || searchIds.has(id);
|
89 |
+
};
|
90 |
+
const doSearch = (text: string) => {
|
91 |
+
const searchResults = new Set<string>();
|
92 |
+
if (text.length > 0) {
|
93 |
+
messages.forEach((m) =>
|
94 |
+
m.content.includes(text) ? searchResults.add(m.id!) : null,
|
95 |
+
);
|
96 |
+
}
|
97 |
+
setSearchIds(searchResults);
|
98 |
+
};
|
99 |
+
|
100 |
+
// for range selection
|
101 |
+
const { startIndex, endIndex, onClickIndex } = useShiftRange();
|
102 |
+
|
103 |
+
const selectAll = () => {
|
104 |
+
props.updateSelection((selection) =>
|
105 |
+
messages.forEach((m) => selection.add(m.id!)),
|
106 |
+
);
|
107 |
+
};
|
108 |
+
|
109 |
+
useEffect(() => {
|
110 |
+
if (props.defaultSelectAll) {
|
111 |
+
selectAll();
|
112 |
+
}
|
113 |
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
114 |
+
}, []);
|
115 |
+
|
116 |
+
useEffect(() => {
|
117 |
+
if (startIndex === undefined || endIndex === undefined) {
|
118 |
+
return;
|
119 |
+
}
|
120 |
+
const [start, end] = [startIndex, endIndex].sort((a, b) => a - b);
|
121 |
+
props.updateSelection((selection) => {
|
122 |
+
for (let i = start; i <= end; i += 1) {
|
123 |
+
selection.add(messages[i].id ?? i);
|
124 |
+
}
|
125 |
+
});
|
126 |
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
127 |
+
}, [startIndex, endIndex]);
|
128 |
+
|
129 |
+
const LATEST_COUNT = 4;
|
130 |
+
|
131 |
+
return (
|
132 |
+
<div className={styles["message-selector"]}>
|
133 |
+
<div className={styles["message-filter"]}>
|
134 |
+
<input
|
135 |
+
type="text"
|
136 |
+
placeholder={Locale.Select.Search}
|
137 |
+
className={styles["filter-item"] + " " + styles["search-bar"]}
|
138 |
+
value={searchInput}
|
139 |
+
onInput={(e) => {
|
140 |
+
setSearchInput(e.currentTarget.value);
|
141 |
+
doSearch(e.currentTarget.value);
|
142 |
+
}}
|
143 |
+
></input>
|
144 |
+
|
145 |
+
<div className={styles["actions"]}>
|
146 |
+
<IconButton
|
147 |
+
text={Locale.Select.All}
|
148 |
+
bordered
|
149 |
+
className={styles["filter-item"]}
|
150 |
+
onClick={selectAll}
|
151 |
+
/>
|
152 |
+
<IconButton
|
153 |
+
text={Locale.Select.Latest}
|
154 |
+
bordered
|
155 |
+
className={styles["filter-item"]}
|
156 |
+
onClick={() =>
|
157 |
+
props.updateSelection((selection) => {
|
158 |
+
selection.clear();
|
159 |
+
messages
|
160 |
+
.slice(messageCount - LATEST_COUNT)
|
161 |
+
.forEach((m) => selection.add(m.id!));
|
162 |
+
})
|
163 |
+
}
|
164 |
+
/>
|
165 |
+
<IconButton
|
166 |
+
text={Locale.Select.Clear}
|
167 |
+
bordered
|
168 |
+
className={styles["filter-item"]}
|
169 |
+
onClick={() =>
|
170 |
+
props.updateSelection((selection) => selection.clear())
|
171 |
+
}
|
172 |
+
/>
|
173 |
+
</div>
|
174 |
+
</div>
|
175 |
+
|
176 |
+
<div className={styles["messages"]}>
|
177 |
+
{messages.map((m, i) => {
|
178 |
+
if (!isInSearchResult(m.id!)) return null;
|
179 |
+
|
180 |
+
return (
|
181 |
+
<div
|
182 |
+
className={`${styles["message"]} ${
|
183 |
+
props.selection.has(m.id!) && styles["message-selected"]
|
184 |
+
}`}
|
185 |
+
key={i}
|
186 |
+
onClick={() => {
|
187 |
+
props.updateSelection((selection) => {
|
188 |
+
const id = m.id ?? i;
|
189 |
+
selection.has(id) ? selection.delete(id) : selection.add(id);
|
190 |
+
});
|
191 |
+
onClickIndex(i);
|
192 |
+
}}
|
193 |
+
>
|
194 |
+
<div className={styles["avatar"]}>
|
195 |
+
{m.role === "user" ? (
|
196 |
+
<Avatar avatar={config.avatar}></Avatar>
|
197 |
+
) : (
|
198 |
+
<MaskAvatar mask={session.mask} />
|
199 |
+
)}
|
200 |
+
</div>
|
201 |
+
<div className={styles["body"]}>
|
202 |
+
<div className={styles["date"]}>
|
203 |
+
{new Date(m.date).toLocaleString()}
|
204 |
+
</div>
|
205 |
+
<div className={`${styles["content"]} one-line`}>
|
206 |
+
{m.content}
|
207 |
+
</div>
|
208 |
+
</div>
|
209 |
+
</div>
|
210 |
+
);
|
211 |
+
})}
|
212 |
+
</div>
|
213 |
+
</div>
|
214 |
+
);
|
215 |
+
}
|