CostalCry commited on
Commit
72e12da
1 Parent(s): 658155c

Upload folder using huggingface_hub

Browse files
This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. .github/README.md +28 -35
  2. NeuroGPT/.babelrc +14 -0
  3. NeuroGPT/.env.template +39 -0
  4. NeuroGPT/.eslintignore +1 -0
  5. NeuroGPT/.eslintrc.json +4 -0
  6. NeuroGPT/.github/FUNDING.yml +13 -0
  7. NeuroGPT/.github/ISSUE_TEMPLATE/bug_report.md +38 -0
  8. NeuroGPT/.github/ISSUE_TEMPLATE/feature_request.md +20 -0
  9. NeuroGPT/.github/README.md +276 -0
  10. NeuroGPT/.github/README_RU.md +85 -0
  11. NeuroGPT/.github/workflows/build_chat.yml +104 -0
  12. NeuroGPT/.gitignore +46 -0
  13. NeuroGPT/.gitpod.yml +11 -0
  14. NeuroGPT/.lintstagedrc.json +6 -0
  15. NeuroGPT/.prettierrc.js +10 -0
  16. NeuroGPT/CODE_OF_CONDUCT.md +128 -0
  17. NeuroGPT/LICENSE +21 -0
  18. NeuroGPT/app/api/auth.ts +63 -0
  19. NeuroGPT/app/api/common.ts +102 -0
  20. NeuroGPT/app/api/config/route.ts +29 -0
  21. NeuroGPT/app/api/cors/[...path]/route.ts +70 -0
  22. NeuroGPT/app/api/model-config/route.ts +14 -0
  23. NeuroGPT/app/api/openai/[...path]/route.ts +104 -0
  24. NeuroGPT/app/azure.ts +9 -0
  25. NeuroGPT/app/client/api.ts +156 -0
  26. NeuroGPT/app/client/controller.ts +37 -0
  27. NeuroGPT/app/client/platforms/openai.ts +661 -0
  28. NeuroGPT/app/command.ts +90 -0
  29. NeuroGPT/app/components/auth.module.scss +36 -0
  30. NeuroGPT/app/components/auth.tsx +116 -0
  31. NeuroGPT/app/components/button.module.scss +83 -0
  32. NeuroGPT/app/components/button.tsx +51 -0
  33. NeuroGPT/app/components/changelog.module.scss +108 -0
  34. NeuroGPT/app/components/changelog.tsx +93 -0
  35. NeuroGPT/app/components/chat-list.tsx +167 -0
  36. NeuroGPT/app/components/chat.module.scss +518 -0
  37. NeuroGPT/app/components/chat.tsx +1453 -0
  38. NeuroGPT/app/components/emoji.tsx +79 -0
  39. NeuroGPT/app/components/error.tsx +72 -0
  40. NeuroGPT/app/components/exporter.module.scss +223 -0
  41. NeuroGPT/app/components/exporter.tsx +704 -0
  42. NeuroGPT/app/components/home.module.scss +353 -0
  43. NeuroGPT/app/components/home.tsx +210 -0
  44. NeuroGPT/app/components/input-range.module.scss +13 -0
  45. NeuroGPT/app/components/input-range.tsx +37 -0
  46. NeuroGPT/app/components/markdown.tsx +212 -0
  47. NeuroGPT/app/components/mask.module.scss +108 -0
  48. NeuroGPT/app/components/mask.tsx +618 -0
  49. NeuroGPT/app/components/message-selector.module.scss +76 -0
  50. 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=Inconsolata&weight=700&size=30&duration=4000&pause=1000&color=1BED29&center=true&width=435&lines=NeuroGPT+by+Neurogen...;Opening..." alt="NeuroGPT" />
4
  </a>
5
 
6
- <strong> Русский | <a href="https://github.com/Em1tSan/NeuroGPT/blob/main/.github/README_EN.md">English </a></strong>
7
 
8
- <p> NeuroGPT позволяет простому пользователю получить бесплатный доступ к gpt-3.5, gpt-4 и другим языковым моделям без VPN и регистрации аккаунта. В стабильном режиме на данный момент функционирует только gpt-3.5-turbo 4097. Постоянный доступ к остальным моделям не гарантируется.
9
-
10
- Проект основан на модифицированных версиях <a href="https://github.com/xtekky/gpt4free">gpt4free</a> и <a href="https://github.com/GaiZhenbiao/ChuanhuChatGPT">ChuanhuChatGPT</a></p>
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-indigo.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.4.6-indigo.svg" alt="latest"/>
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/%D0%A3%D1%81%D1%82%D0%B0%D0%BD%D0%BE%D0%B2%D0%BA%D0%B0-%D0%B8-%D0%B7%D0%B0%D0%BF%D1%83%D1%81%D0%BA#windows">
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/%D0%A3%D1%81%D1%82%D0%B0%D0%BD%D0%BE%D0%B2%D0%BA%D0%B0-%D0%B8-%D0%B7%D0%B0%D0%BF%D1%83%D1%81%D0%BA#linux">
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/%D0%A3%D1%81%D1%82%D0%B0%D0%BD%D0%BE%D0%B2%D0%BA%D0%B0-%D0%B8-%D0%B7%D0%B0%D0%BF%D1%83%D1%81%D0%BA#macos">
30
  <img src="https://img.shields.io/badge/-MacOS-C0BFC0?logo=apple" alt="macos"/>
31
- </a> </p>
32
- <a href="https://github.com/Em1tSan/NeuroGPT/wiki/%D0%A3%D1%81%D1%82%D0%B0%D0%BD%D0%BE%D0%B2%D0%BA%D0%B0-%D0%B8-%D0%B7%D0%B0%D0%BF%D1%83%D1%81%D0%BA#%D0%BF%D0%BE%D1%80%D1%82%D0%B0%D1%82%D0%B8%D0%B2%D0%BD%D0%B0%D1%8F-%D0%B2%D0%B5%D1%80%D1%81%D0%B8%D1%8F">
33
- <img src="https://img.shields.io/badge/-Портативная версия-008000?logo=portable" alt="portable"/>
34
  </a>
35
 
36
- <br> Новости и обратная связь: <br/>
37
 
38
  <a href="https://t.me/neurogen_news">
39
- <img src="https://img.shields.io/badge/-Telegram канал-0088CC?logo=telegram" alt="telegram"/>
40
  </a>
41
  <a href="https://t.me/neurogen_chat">
42
- <img src="https://img.shields.io/badge/-Telegram чат-0088CC?logo=telegram" alt="telegram_chat"/>
43
  </a>
44
 
45
- <br> Поддержать проект: <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
- - [Список моделей](https://status.neuroapi.host/v1/status)
61
- - Встроенные шаблоны промптов под разные задачи
62
- - Встроенные джейлбрейки для снятия цензуры
63
- - Контекст беседы
64
- - Режим endpoint для работы с API
65
- - Изменение параметров генерации для gpt-моделей
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&center=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&center=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&center=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 "&#36;"; // 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 "&#36;"; // 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
+ }