Compare commits

...

162 Commits

Author SHA1 Message Date
kangfenmao
f7ef895ce6 chore(version): 0.5.0 2024-08-07 21:55:51 +08:00
kangfenmao
beb40f5baf feat: fix add assistant search keywords format 2024-08-07 20:57:31 +08:00
kangfenmao
07613e65f5 feat: add max token limit #18 2024-08-07 20:49:21 +08:00
kangfenmao
6185068353 feat: use ubuntu font as default 2024-08-07 14:28:29 +08:00
kangfenmao
61934cd65c feat add agent popup #14 2024-08-07 13:23:29 +08:00
kangfenmao
41f65b66ba chore(version): 0.4.9 2024-08-06 20:41:34 +08:00
kangfenmao
5edb53ef7d feat: add ollama settings 2024-08-06 20:38:01 +08:00
kangfenmao
167988927b feat: add custom agent #14 2024-08-06 19:18:17 +08:00
kangfenmao
a39beb3841 fix(AboutSettings.tsx): handle errors in update check by setting loading state 2024-08-05 16:15:58 +08:00
kangfenmao
8719d5c330 chore(version): 0.4.8 2024-08-05 13:20:55 +08:00
kangfenmao
a7427d6cb6 feat(i18n): new topic 2024-08-05 13:14:57 +08:00
kangfenmao
8759a50727 fix: estimate history token count 2024-08-05 13:09:13 +08:00
kangfenmao
7ffa42caa0 feat: input status use tag 2024-08-05 13:00:18 +08:00
kangfenmao
b0a3d705ff feat: @model regenerate message 2024-08-05 12:39:37 +08:00
kangfenmao
de41199f7e feat: quick regenerate with new model 2024-08-04 14:04:11 +08:00
kangfenmao
cbd9f60cfc fix: markdown link color 2024-08-04 13:30:15 +08:00
kangfenmao
8a0e2890dd fix: math code format 2024-08-04 13:23:35 +08:00
kangfenmao
a8f3e2be6b chore(release.yml): add CSC_LINK and CSC_KEY_PASSWORD environment variables for windows build to enable code signing 2024-08-02 15:17:21 +08:00
kangfenmao
297539bab7 chore(version): 0.4.7 2024-08-02 11:32:29 +08:00
kangfenmao
911c2d0202 fix: footnote style 2024-08-02 11:30:06 +08:00
kangfenmao
2969a05f10 feat: enhance markdown style 2024-08-02 10:39:13 +08:00
kangfenmao
5d90489a04 style: change import order 2024-08-02 10:11:18 +08:00
kangfenmao
18fa1c92a4 feat(provider): sillicon api key use referrer link 2024-08-02 09:24:31 +08:00
kangfenmao
937e62bf9d feat(provider): add gpt-4o-mini model 2024-08-02 09:24:00 +08:00
kangfenmao
6291a463d8 perf(messages): usememo & usecallback message component 2024-08-01 23:55:51 +08:00
kangfenmao
681c93f5eb chore(version): 0.4.6 2024-08-01 15:36:07 +08:00
kangfenmao
23687f119d fix(SendMessageButton.tsx): remove unnecessary placement prop from SendMessageButton to prevent potential UI alignment issues 2024-08-01 15:23:12 +08:00
kangfenmao
0bcdffc159 fix(SettingsTab.tsx): correct the temperature label 2024-08-01 15:19:45 +08:00
kangfenmao
b04b0cc8a6 feat: add markdown footnote 2024-08-01 15:18:09 +08:00
kangfenmao
c9a964d8f8 feat: add markdown plugins remark-gfm remark-math rehype-katex 2024-08-01 14:51:20 +08:00
kangfenmao
86fc4676ba feat: add link component 2024-08-01 14:28:18 +08:00
kangfenmao
527afa1357 chore(version): 0.4.5 2024-08-01 00:05:16 +08:00
kangfenmao
384178c617 style(Message.tsx): increase padding in MessageContainer 2024-08-01 00:04:47 +08:00
kangfenmao
c53e35db76 feat: use poppins fonts 2024-07-31 23:20:28 +08:00
kangfenmao
c36075f0b5 fix: optimize interface display style 2024-07-31 21:04:09 +08:00
kangfenmao
5c95373a37 feat: new window style 2024-07-31 17:30:17 +08:00
kangfenmao
29d6d607da chore(version): 0.4.4 2024-07-31 13:54:04 +08:00
kangfenmao
e64375a74c feat(Inputbar.tsx): change height to min-height for Inputbar 2024-07-31 13:41:02 +08:00
kangfenmao
4689bb53e9 chore(package.json): add publish script to automate the release and patch version push process 2024-07-31 13:11:31 +08:00
kangfenmao
e00c66e54a chore(version): 0.4.3 2024-07-31 13:08:19 +08:00
kangfenmao
62b0908dfa feat: add send message button 2024-07-31 13:07:02 +08:00
kangfenmao
cb0b9de1e9 feat: default enable new added provider 2024-07-31 12:21:46 +08:00
kangfenmao
d8d4afbc0d feat: add message suggestions 2024-07-31 12:13:03 +08:00
kangfenmao
c50ff4585a chore(version): 0.4.2 2024-07-30 17:53:45 +08:00
kangfenmao
a5ee8548f3 feat(AboutSettings): implement functionality to open license page from about settings 2024-07-30 16:33:58 +08:00
kangfenmao
15b286a095 doc: update LICENSE 2024-07-30 16:13:32 +08:00
kangfenmao
d47d4a158d docs: change offical website url 2024-07-30 15:31:17 +08:00
kangfenmao
cd85dcddf8 remove: website 2024-07-30 15:30:35 +08:00
kangfenmao
925a9fb8ec fix: delete provider crash 2024-07-30 15:30:09 +08:00
kangfenmao
17c3437e02 chore(version): 0.4.1 2024-07-29 18:18:03 +08:00
kangfenmao
69293846fc fix: model list text color 2024-07-29 18:17:50 +08:00
kangfenmao
20a7fbfc48 fix(ProviderSDK.ts): translation message 2024-07-29 17:45:08 +08:00
kangfenmao
64d4b8450a style(website): adjust border-radius of images to 20% 2024-07-29 17:36:27 +08:00
kangfenmao
f080fc5048 chore(version): 0.4.0 2024-07-29 17:33:09 +08:00
kangfenmao
50f08124d7 feat: add dark and light theme 2024-07-29 17:14:49 +08:00
kangfenmao
b91081ef99 docs(index.html): update website URLs from easys.run to cherry-ai.com 2024-07-29 09:55:24 +08:00
kangfenmao
d869ec9a9b chore(version): 0.3.9 2024-07-29 09:16:46 +08:00
kangfenmao
70c4354d6c feat: add model logo on select model dropdown 2024-07-28 15:10:36 +08:00
kangfenmao
527c4e77dc fix(Message.tsx): add optional chaining to assistant.name to prevent potential undefined errors 2024-07-28 11:16:16 +08:00
kangfenmao
2483ce3bb4 chore(version): 0.3.8 2024-07-28 02:28:48 +08:00
kangfenmao
db3f8b8bee refactor(TranslatePage.tsx): simplify OutputText styling for cleaner code structure 2024-07-28 02:28:48 +08:00
kangfenmao
45bf3d4e86 fix(index.html): update Content-Security-Policy to allow fonts 2024-07-28 01:37:43 +08:00
kangfenmao
59b39dc41a feat(TranslatePage.tsx): add markdown style to handle whitespace properly in translation output 2024-07-28 01:32:03 +08:00
kangfenmao
a267a8d4c3 feat: add translation module 2024-07-28 01:07:15 +08:00
kangfenmao
5b123f2c33 fix(markdown.scss): replace :first-of-type with :first-child for consistent styling of first elements 2024-07-26 18:02:50 +08:00
kangfenmao
fe34fb3c25 fix(api.ts): modify provider apiKey check to exclude 'ollama' provider 2024-07-26 18:02:32 +08:00
kangfenmao
e6359d2048 feat(markdown.scss): add white-space: pre-wrap to code elements 2024-07-26 17:22:48 +08:00
kangfenmao
aa3b2d6290 chore(version): 0.3.7 2024-07-26 11:04:02 +08:00
kangfenmao
c0e51c3992 feat(ProviderSetting.tsx): add remove icon for models to allow deletion 2024-07-26 10:40:53 +08:00
kangfenmao
8c80cc00b3 feat(provider.ts): add API endpoint configuration for each provider with editable status 2024-07-26 10:34:55 +08:00
kangfenmao
f961accd86 fix(SettingsTab.tsx): reposition reset button to align with model settings title for better visibility 2024-07-26 10:10:34 +08:00
kangfenmao
7de91d236d feat(NavigationCenter.tsx): add CodeSandboxOutlined icon to model selection 2024-07-26 10:04:59 +08:00
kangfenmao
2fdf0acec6 feat: add global _activeAssistant and_activeTopic variable to persist state across re-renders 2024-07-26 09:57:49 +08:00
kangfenmao
40e76f3e53 feat: save file to disk 2024-07-26 09:53:07 +08:00
kangfenmao
d7b8721848 refactor: remove conditional devTools enabling 2024-07-25 18:04:12 +08:00
kangfenmao
b91b0dd8e4 fix(api.ts): add null return if provider apiKey is missing to prevent unauthorized requests 2024-07-25 18:00:32 +08:00
kangfenmao
bb9b053924 docs(assistants.json): simplify prompts for clarity and consistency #6 2024-07-25 17:50:26 +08:00
kangfenmao
5743046200 refactor: use —narbar-background 2024-07-25 15:55:23 +08:00
kangfenmao
a507776c1e fix: default assistant name is empty 2024-07-25 14:03:54 +08:00
kangfenmao
e74c828379 feat: set provider as default setting entry 2024-07-25 13:45:43 +08:00
kangfenmao
4b264c6a6b 0.3.6 2024-07-24 19:19:35 +08:00
kangfenmao
d21a4dce92 feat(ui): optimize messages ui styles 2024-07-24 19:17:58 +08:00
kangfenmao
74df29604b chore(version): v0.3.5 2024-07-24 18:37:48 +08:00
kangfenmao
8807783aa6 feat: switch topic tab on change assistant 2024-07-24 18:28:23 +08:00
kangfenmao
f81b38a362 perf(mermaid): lazy load mermaid 2024-07-24 18:19:43 +08:00
kangfenmao
d0280186bc feat: add setting panel 2024-07-24 18:08:05 +08:00
kangfenmao
9d96b826e2 feat(settings): add input status show switch 2024-07-24 13:08:30 +08:00
kangfenmao
ec20750e64 fix: sidebar mac style 2024-07-24 12:28:56 +08:00
kangfenmao
51f4653cde feat(settings): add messageFont setting 2024-07-24 12:25:36 +08:00
kangfenmao
3625eefec4 fix: prevent navigate to new url 2024-07-23 19:08:36 +08:00
亢奋猫
1e1414d659 Update README.md 2024-07-23 18:39:06 +08:00
亢奋猫
b7162663f2 Update README.md 2024-07-23 18:24:12 +08:00
kangfenmao
1dd1bb5804 0.3.4 2024-07-23 18:10:33 +08:00
kangfenmao
4dd6c46035 fix: message style 2024-07-23 18:10:25 +08:00
kangfenmao
4036c36753 feat: add Mermaid render 2024-07-23 18:05:14 +08:00
kangfenmao
764aadd234 feat: change message font 2024-07-23 17:42:52 +08:00
kangfenmao
3d801f1552 feat: optimize message style 2024-07-23 17:32:06 +08:00
kangfenmao
bd865f0270 fix: windows title style 2024-07-23 16:55:32 +08:00
kangfenmao
93505a4bc6 feat: hide window title 2024-07-23 16:40:06 +08:00
kangfenmao
c43be11d20 feat: add username and message divider line settings 2024-07-23 15:16:34 +08:00
kangfenmao
8535edbdd1 feat: messages styles optimization 2024-07-23 14:59:09 +08:00
kangfenmao
731fb7860b 0.3.3 2024-07-23 12:37:40 +08:00
kangfenmao
4a32976483 fix: proxy check 2024-07-23 12:37:12 +08:00
kangfenmao
dedabe320e feat: new navbar style 2024-07-23 12:29:20 +08:00
kangfenmao
235b481645 feat: change icons 2024-07-23 10:42:58 +08:00
kangfenmao
58c5ace678 fix: inputbar setShowRightSidebar 2024-07-23 10:20:57 +08:00
kangfenmao
973d24271b feat(settings): add proxy setting 2024-07-23 00:28:41 +08:00
kangfenmao
f434fe1231 feat: add show or hide assistant sidebar 2024-07-22 21:57:39 +08:00
kangfenmao
a0c147ae3f feat(website): fetch github release info 2024-07-22 15:40:30 +08:00
kangfenmao
8d7cde1231 0.3.2 2024-07-22 14:52:54 +08:00
kangfenmao
87c04408de feat: add contextCount to inputbar 2024-07-22 14:50:40 +08:00
kangfenmao
2592448c74 feat: add email to about titles 2024-07-22 14:26:35 +08:00
kangfenmao
6f054874e8 chore: remove change log component 2024-07-22 14:25:15 +08:00
kangfenmao
40d687104e feat: new about page 2024-07-22 14:24:14 +08:00
kangfenmao
ac3cfe2878 fix: disable switch while assistant generating message 2024-07-22 11:28:26 +08:00
kangfenmao
e9a7735fce feat: add updateAssistantSettings to useAssistant hook 2024-07-22 11:15:10 +08:00
kangfenmao
c1a8198575 fix(ProviderSDK): clarify instruction for session summary to avoid punctuation marks and special characters 2024-07-22 10:49:10 +08:00
kangfenmao
8b45548b79 refactor: topic component code 2024-07-22 10:38:00 +08:00
kangfenmao
3f3b930819 fix: disabled switch topic while generating message 2024-07-22 10:22:47 +08:00
kangfenmao
a5d6e2c5c5 0.3.1 2024-07-21 23:44:09 +08:00
kangfenmao
2993ab8dc1 fix: topic missing bug and delete assistant crash 2024-07-21 23:43:17 +08:00
kangfenmao
117069e450 chore(version): 0.3.0 2024-07-21 22:03:49 +08:00
kangfenmao
c5965dc696 fix: assistant settings bugs 2024-07-21 21:57:08 +08:00
kangfenmao
4169a2ef35 feat: add asistant model temperature maxTokens contextCount 2024-07-21 17:50:50 +08:00
kangfenmao
75c37632d4 feat: change default assistant name
# Conflicts:
#	src/renderer/src/i18n/index.ts
2024-07-21 10:51:33 +08:00
亢奋猫
3f5c151a11 Update README.md 2024-07-20 15:10:49 +08:00
kangfenmao
d049e36c46 0.2.9 2024-07-20 12:47:29 +08:00
kangfenmao
d05fc1c9be chore(version): v0.2.9 2024-07-20 12:47:19 +08:00
kangfenmao
f33317a3fb fix: send message setting position 2024-07-20 11:34:52 +08:00
kangfenmao
f2b5ed09c0 feat(provider): add AiHubMix provider 2024-07-20 11:29:24 +08:00
kangfenmao
81e66dde0e 0.2.8 2024-07-20 00:57:02 +08:00
kangfenmao
f76388d979 chore(version): v0.2.8 2024-07-20 00:56:52 +08:00
kangfenmao
9e542f813c feat: add custom llm provider 2024-07-20 00:50:46 +08:00
kangfenmao
5ede95cf2e 0.2.7 2024-07-19 15:57:16 +08:00
kangfenmao
fd8b15ebbe chore(version): 0.2.7 2024-07-19 15:52:52 +08:00
kangfenmao
5a636e7614 refactor: ProviderSDK 2024-07-19 15:49:08 +08:00
kangfenmao
13c73a3de1 fix: use activeAssistant's id for fetching assistant and model data 2024-07-19 15:39:49 +08:00
kangfenmao
31284a6e23 feat: add anthropic provider 2024-07-19 15:34:34 +08:00
kangfenmao
c4394b925d feat(settings/components): introduce password input for API key to enhance security 2024-07-19 13:38:44 +08:00
kangfenmao
93a5739d87 feat(assistants.json): Introduce new assistants for translation and summarization 2024-07-19 13:34:21 +08:00
kangfenmao
f23c4a0afa feat: add DashScope provider 2024-07-19 12:28:00 +08:00
kangfenmao
8723c251b1 Update Website 2024-07-19 09:38:29 +08:00
亢奋猫
a9634fd684 Update README.md 2024-07-18 16:46:33 +08:00
亢奋猫
53757626f2 Update README.md 2024-07-18 16:41:09 +08:00
kangfenmao
83af70e460 feat(website): cherry studio -> cherry ai 2024-07-18 13:02:46 +08:00
亢奋猫
3377aae0ff Update README.md 2024-07-17 23:08:13 +08:00
kangfenmao
2c4d18843b 0.2.6 2024-07-17 17:50:07 +08:00
kangfenmao
244cce0b65 chore(version): 0.2.6 2024-07-17 17:49:47 +08:00
kangfenmao
af41cebe18 feat: new agent center 2024-07-17 17:45:29 +08:00
kangfenmao
00cf2d6b24 0.2.5 2024-07-17 16:22:01 +08:00
kangfenmao
2507dd1bcf chore(version): 0.2.5 2024-07-17 16:21:48 +08:00
kangfenmao
a6ff6e3a4d refactor: assistants 2024-07-17 15:48:08 +08:00
kangfenmao
474beca088 fix: delete all topic confirm button text 2024-07-17 12:47:26 +08:00
kangfenmao
810c44f7fc feat(provider): add baichuan provider 2024-07-17 12:44:01 +08:00
kangfenmao
8bff4df722 feat: update website 2024-07-16 21:04:18 +08:00
kangfenmao
a19c69340a ci: log notarized app name 2024-07-16 20:49:41 +08:00
kangfenmao
c250124043 0.2.4 2024-07-16 20:42:21 +08:00
kangfenmao
7aa6d6ebeb fix: process is not defined 2024-07-16 20:40:25 +08:00
kangfenmao
e962351b13 feat: check update 2024-07-16 20:06:25 +08:00
kangfenmao
80e34688b1 fix(about): changelog overflow 2024-07-16 17:27:16 +08:00
kangfenmao
8c23d6ec55 feat: add website 2024-07-16 17:08:10 +08:00
kangfenmao
2cc09a52f4 feat: add sentry integration 2024-07-16 17:08:04 +08:00
134 changed files with 7883 additions and 1809 deletions

View File

@@ -1,5 +1,5 @@
module.exports = {
plugins: ['unused-imports'],
plugins: ['unused-imports', 'simple-import-sort'],
extends: [
'eslint:recommended',
'plugin:react/recommended',
@@ -14,12 +14,7 @@ module.exports = {
'unused-imports/no-unused-imports': 'error',
'@typescript-eslint/no-non-null-asserted-optional-chain': 'off',
'react/prop-types': 'off',
'sort-imports': [
'error',
{
ignoreCase: true,
ignoreDeclarationSort: true
}
]
'simple-import-sort/imports': 'error',
'simple-import-sort/exports': 'error'
}
}

View File

@@ -52,6 +52,8 @@ jobs:
if: matrix.os == 'windows-latest'
run: yarn build:win
env:
CSC_LINK: ${{ secrets.CSC_LINK }}
CSC_KEY_PASSWORD: ${{ secrets.CSC_KEY_PASSWORD }}
GH_TOKEN: ${{ secrets.GH_TOKEN }}
- name: Replace spaces in filenames
@@ -60,7 +62,7 @@ jobs:
- name: Release
uses: softprops/action-gh-release@v2
with:
draft: false
draft: true
files: |
dist/*.exe
dist/*.zip

3
.gitignore vendored
View File

@@ -34,6 +34,9 @@ npm/*/*
!.yarn/sdks
!.yarn/versions
# Windows
Thumbs.db
# Project
node_modules
dist

114
LICENSE
View File

@@ -1,21 +1,101 @@
MIT License
### Cherry Studio 商业许可协议
Copyright (c) 2024 亢奋猫
---
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
#### 中文版
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
**Cherry Studio 商业许可协议**
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
本协议(以下简称“协议”)由以下双方签订:
- 许可方王谦kangfenmao@qq.com
- 被许可方:[被许可方名称]
**1. 定义**
- “软件”指 Cherry Studio 软件,网址为 https://cherry-ai.com。
- “商业用途”指任何以盈利为目的的使用。
**2. 许可**
- 未经许可方明确书面许可,被许可方不得将软件用于商业用途。
- 未经许可方事先书面同意,被许可方不得将软件全部或部分用于商业用途分发。
- 未经许可方明确授权,被许可方不得再许可、租赁、销售、出租或以其他方式将软件转让给任何第三方用于商业用途。
**3. 责任限制**
开发者不对因使用本软件而产生的任何直接或间接损失承担责任。用户应自行承担使用本软件的风险。
**4. 许可协议生效日期**
本许可协议自用户首次下载或使用本软件之日起生效。
**5. 许可终止**
如发现用户违反上述条款,开发者有权随时终止本许可,并要求用户停止使用本软件及删除所有相关副本。
**6. 其他**
本协议的解释、效力及争议的解决,均适用中华人民共和国法律。
**7. 联系信息**
- 许可方联系方式:
- 手机号18539907620
- 邮箱kangfenmao@qq.com
**许可方(签字):**
**日期:**
**被许可方(签字):**
**日期:**
---
#### English Version
**Cherry Studio Commercial License Agreement**
This Agreement ("Agreement") is entered into by and between:
- Licensor: Wang Qian (kangfenmao)
- Licensee: [Licensee Name]
**1. Definitions**
- "Software" refers to the Cherry Studio software, available at https://cherry-ai.com.
- "Commercial Use" refers to any use for profit.
**2. License**
- The Licensee may not use the Software for Commercial Use without the Licensor's explicit written permission.
- The Licensee may not distribute the Software in whole or in part for Commercial Use without the Licensor's prior written consent.
- The Licensee may not sublicense, lease, sell, rent, or otherwise transfer the Software to any third party for Commercial Use without the Licensor's explicit authorization.
**3. Termination of License**
The developer reserves the right to terminate this license at any time if the terms are violated, and may require the user to cease using the software and delete all related copies.
**4. Effective Date of License Agreement**
This license agreement becomes effective from the date the user first downloads or uses the software.
**5. Termination of License**
The developer reserves the right to terminate this license at any time if the terms are violated, and may require the user to cease using the software and delete all related copies.
**6. Miscellaneous**
This Agreement shall be governed by and construed in accordance with the laws of the People's Republic of China.
**7. Contact Information**
- Licensor's Contact Details:
- Phone: 18539907620
- Email: kangfenmao@qq.com
**Licensor (Signature):**
**Date:**
**Licensee (Signature):**
**Date:**

View File

@@ -1,12 +1,14 @@
# Cherry Studio
Cherry Studio is a desktop client for multiple cutting-edge LLM models, available on Windows, Mac and Linux.
🍒 Cherry Studio is a desktop client that supports multiple Large Language Model (LLM) providers, available on Windows, Mac and Linux.
# Screenshot
![image.png](https://s2.loli.net/2024/07/16/IAVSOorsfFQyGhM.png)
![](https://github.com/user-attachments/assets/e32b244f-3a84-473a-89ef-0b12ef4127b2)
![image.png](https://s2.loli.net/2024/07/16/IQPz12OajfNoBTV.png)
![](https://github.com/user-attachments/assets/18c10eed-4711-4975-bf9c-b274c61924f3)
![](https://github.com/user-attachments/assets/7395ebf2-64f8-46fa-aa48-63293516c320)
# Feature
@@ -16,6 +18,7 @@ Cherry Studio is a desktop client for multiple cutting-edge LLM models, availabl
4. Allows using multiple models to answer questions in the same conversation.
5. Supports drag-and-drop sorting.
6. Code highlighting.
7. Mermaid chart
# Develop
## Recommended IDE Setup

View File

@@ -1,3 +1,6 @@
provider: generic
url: http://127.0.0.1:8080
updaterCacheDirName: cherry-studio-updater
# provider: generic
# url: http://127.0.0.1:8080
# updaterCacheDirName: cherry-studio-updater
provider: github
repo: cherry-studio
owner: kangfenmao

View File

@@ -56,7 +56,5 @@ electronDownload:
afterSign: scripts/notarize.js
releaseInfo:
releaseNotes: |
- 修复多语言提示错误
- 修复智谱AI默认模型错误问题
- 修复 OpenRouter API 检测出错问题
- 修复模型提供商多语言翻译错误问题
支持保存自定义智能体
修复话题重命名的问题

View File

@@ -1,5 +1,5 @@
import { defineConfig, externalizeDepsPlugin } from 'electron-vite'
import react from '@vitejs/plugin-react'
import { defineConfig, externalizeDepsPlugin } from 'electron-vite'
import { resolve } from 'path'
export default defineConfig({
@@ -16,9 +16,6 @@ export default defineConfig({
}
},
plugins: [react()],
assetsInclude: ['**/*.md'],
server: {
host: '0.0.0.0'
}
assetsInclude: ['**/*.md']
}
})

View File

@@ -1,6 +1,6 @@
{
"name": "cherry-studio",
"version": "0.2.3",
"version": "0.5.0",
"description": "A powerful AI assistant for producer.",
"main": "./out/main/index.js",
"author": "kangfenmao@qq.com",
@@ -18,20 +18,25 @@
"build:unpack": "dotenv npm run build && electron-builder --dir",
"build:win": "dotenv npm run build && electron-builder --win --publish never",
"build:mac": "dotenv electron-vite build && electron-builder --mac --publish never",
"build:linux": "dotenv electron-vite build && electron-builder --linux --publish never"
"build:linux": "dotenv electron-vite build && electron-builder --linux --publish never",
"release": "node scripts/version.js",
"publish": "yarn release patch push"
},
"dependencies": {
"@electron-toolkit/preload": "^3.0.0",
"@electron-toolkit/utils": "^3.0.0",
"@sentry/electron": "^5.2.0",
"electron-log": "^5.1.5",
"electron-store": "^8.2.0",
"electron-updater": "^6.1.7",
"electron-window-state": "^5.0.3"
"electron-window-state": "^5.0.3",
"eslint-plugin-simple-import-sort": "^12.1.1"
},
"devDependencies": {
"@anthropic-ai/sdk": "^0.24.3",
"@electron-toolkit/eslint-config-prettier": "^2.0.0",
"@electron-toolkit/eslint-config-ts": "^1.0.1",
"@electron-toolkit/tsconfig": "^1.0.1",
"@fontsource/inter": "^5.0.18",
"@hello-pangea/dnd": "^16.6.0",
"@kangfenmao/keyv-storage": "^0.1.0",
"@reduxjs/toolkit": "^2.2.5",
@@ -50,10 +55,12 @@
"electron-devtools-installer": "^3.2.0",
"electron-vite": "^2.0.0",
"emittery": "^1.0.3",
"emoji-picker-element": "^1.22.1",
"eslint": "^8.56.0",
"eslint-plugin-react": "^7.34.3",
"eslint-plugin-react-hooks": "^4.6.2",
"eslint-plugin-unused-imports": "^4.0.0",
"gpt-tokens": "^1.3.6",
"i18next": "^23.11.5",
"localforage": "^1.10.0",
"lodash": "^4.17.21",
@@ -66,8 +73,12 @@
"react-redux": "^9.1.2",
"react-router": "6",
"react-router-dom": "6",
"react-spinners": "^0.14.1",
"react-syntax-highlighter": "^15.5.0",
"redux-persist": "^6.0.0",
"rehype-katex": "^7.0.0",
"remark-gfm": "^4.0.0",
"remark-math": "^6.0.0",
"sass": "^1.77.2",
"styled-components": "^6.1.11",
"typescript": "^5.3.3",

View File

@@ -6,17 +6,21 @@ exports.default = async function notarizing(context) {
return
}
const appName = context.packager.appInfo.productFilename
if (!process.env.APPLE_ID || !process.env.APPLE_APP_SPECIFIC_PASSWORD || !process.env.APPLE_TEAM_ID) {
console.log('Skipping notarization')
return
}
const notarized = await notarize({
appPath: `${context.appOutDir}/${appName}.app`,
const appName = context.packager.appInfo.productFilename
const appPath = `${context.appOutDir}/${appName}.app`
await notarize({
appPath,
appBundleId: 'com.kangfenmao.CherryStudio',
appleId: process.env.APPLE_ID,
appleIdPassword: process.env.APPLE_APP_SPECIFIC_PASSWORD,
teamId: process.env.APPLE_TEAM_ID
})
console.log('Notarized:', notarized)
return notarized
console.log('Notarized app:', appPath)
}

40
scripts/version.js Normal file
View File

@@ -0,0 +1,40 @@
const { execSync } = require('child_process')
const fs = require('fs')
// 执行命令并返回输出
function exec(command) {
return execSync(command, { encoding: 'utf8' }).trim()
}
// 获取命令行参数
const args = process.argv.slice(2)
const versionType = args[0] || 'patch'
const shouldPush = args.includes('push')
// 验证版本类型
if (!['patch', 'minor', 'major'].includes(versionType)) {
console.error('Invalid version type. Use patch, minor, or major.')
process.exit(1)
}
// 更新版本
exec(`yarn version ${versionType} --immediate`)
// 读取更新后的 package.json 获取新版本号
const updatedPackageJson = JSON.parse(fs.readFileSync('package.json', 'utf8'))
const newVersion = updatedPackageJson.version
// Git 操作
exec('git add .')
exec(`git commit -m "chore(version): ${newVersion}"`)
exec(`git tag -a v${newVersion} -m "Version ${newVersion}"`)
console.log(`Version bumped to ${newVersion}`)
if (shouldPush) {
console.log('Pushing to remote...')
exec('git push && git push --tags')
console.log('Pushed to remote.')
} else {
console.log('Changes are committed locally. Use "git push && git push --tags" to push to remote.')
}

15
src/main/config.ts Normal file
View File

@@ -0,0 +1,15 @@
import Store from 'electron-store'
export const appConfig = new Store()
export const titleBarOverlayDark = {
height: 41,
color: '#00000000',
symbolColor: '#ffffff'
}
export const titleBarOverlayLight = {
height: 41,
color: '#00000000',
symbolColor: '#000000'
}

24
src/main/event.ts Normal file
View File

@@ -0,0 +1,24 @@
import { dialog, SaveDialogOptions, SaveDialogReturnValue } from 'electron'
import logger from 'electron-log'
import { writeFile } from 'fs'
export async function saveFile(_: Electron.IpcMainInvokeEvent, fileName: string, content: string): Promise<void> {
try {
const options: SaveDialogOptions = {
title: '保存文件',
defaultPath: fileName
}
const result: SaveDialogReturnValue = await dialog.showSaveDialog(options)
if (!result.canceled && result.filePath) {
writeFile(result.filePath, content, { encoding: 'utf-8' }, (err) => {
if (err) {
logger.error('[IPC - Error]', 'An error occurred saving the file:', err)
}
})
}
} catch (err) {
logger.error('[IPC - Error]', 'An error occurred saving the file:', err)
}
}

View File

@@ -1,18 +1,24 @@
import { electronApp, is, optimizer } from '@electron-toolkit/utils'
import { app, BrowserWindow, ipcMain, Menu, MenuItem, shell } from 'electron'
import * as Sentry from '@sentry/electron/main'
import { app, BrowserWindow, ipcMain, Menu, MenuItem, session, shell } from 'electron'
import installExtension, { REDUX_DEVTOOLS } from 'electron-devtools-installer'
import windowStateKeeper from 'electron-window-state'
import { join } from 'path'
import icon from '../../resources/icon.png?asset'
import installExtension, { REDUX_DEVTOOLS } from 'electron-devtools-installer'
import { appConfig, titleBarOverlayDark, titleBarOverlayLight } from './config'
import { saveFile } from './event'
import AppUpdater from './updater'
function createWindow(): void {
function createWindow() {
// Load the previous state with fallback to defaults
const mainWindowState = windowStateKeeper({
defaultWidth: 1080,
defaultHeight: 670
})
const theme = appConfig.get('theme') || 'light'
// Create the browser window.
const mainWindow = new BrowserWindow({
x: mainWindowState.x,
@@ -20,16 +26,20 @@ function createWindow(): void {
width: mainWindowState.width,
height: mainWindowState.height,
minWidth: 1080,
minHeight: 500,
minHeight: 600,
show: true,
autoHideMenuBar: true,
titleBarStyle: 'hiddenInset',
transparent: process.platform === 'darwin',
vibrancy: 'fullscreen-ui',
titleBarStyle: 'hidden',
titleBarOverlay: theme === 'dark' ? titleBarOverlayDark : titleBarOverlayLight,
trafficLightPosition: { x: 8, y: 12 },
...(process.platform === 'linux' ? { icon } : {}),
webPreferences: {
preload: join(__dirname, '../preload/index.js'),
sandbox: false,
devTools: !app.isPackaged
webSecurity: false
// devTools: !app.isPackaged,
}
})
@@ -45,6 +55,11 @@ function createWindow(): void {
menu.popup()
})
mainWindow.webContents.on('will-navigate', (event, url) => {
event.preventDefault()
shell.openExternal(url)
})
mainWindow.on('ready-to-show', () => {
mainWindow.show()
})
@@ -61,6 +76,8 @@ function createWindow(): void {
} else {
mainWindow.loadFile(join(__dirname, '../renderer/index.html'))
}
return mainWindow
}
// This method will be called when Electron has finished
@@ -77,28 +94,50 @@ app.whenReady().then(() => {
optimizer.watchWindowShortcuts(window)
})
// IPC
ipcMain.handle('get-app-info', () => ({
version: app.getVersion()
}))
createWindow()
app.on('activate', function () {
// On macOS it's common to re-create a window in the app when the
// dock icon is clicked and there are no other windows open.
if (BrowserWindow.getAllWindows().length === 0) createWindow()
})
installExtension(REDUX_DEVTOOLS)
.then((name) => console.log(`Added Extension: ${name}`))
.catch((err) => console.log('An error occurred: ', err))
const mainWindow = createWindow()
if (app.isPackaged) {
setTimeout(() => new AppUpdater(), 3000)
const { autoUpdater } = new AppUpdater(mainWindow)
// IPC
ipcMain.handle('get-app-info', () => ({
version: app.getVersion(),
isPackaged: app.isPackaged
}))
ipcMain.handle('open-website', (_, url: string) => {
shell.openExternal(url)
})
ipcMain.handle('set-proxy', (_, proxy: string) => {
session.defaultSession.setProxy(proxy ? { proxyRules: proxy } : {})
})
ipcMain.handle('save-file', saveFile)
ipcMain.handle('set-theme', (_, theme: 'light' | 'dark') => {
appConfig.set('theme', theme)
mainWindow?.setTitleBarOverlay &&
mainWindow.setTitleBarOverlay(theme === 'dark' ? titleBarOverlayDark : titleBarOverlayLight)
})
// 触发检查更新(此方法用于被渲染线程调用,例如页面点击检查更新按钮来调用此方法)
ipcMain.handle('check-for-update', async () => {
autoUpdater.logger?.info('触发检查更新')
return {
currentVersion: autoUpdater.currentVersion,
update: await autoUpdater.checkForUpdates()
}
})
installExtension(REDUX_DEVTOOLS)
})
// Quit when all windows are closed, except on macOS. There, it's common
// for applications and their menu bar to stay active until the user quits
// explicitly with Cmd + Q.
@@ -110,3 +149,6 @@ app.on('window-all-closed', () => {
// In this file you can include the rest of your app"s specific main process
// code. You can also put them in separate files and require them here.
Sentry.init({
dsn: 'https://f0e972deff79c2df3e887e232d8a46a3@o4507610668007424.ingest.us.sentry.io/4507610670563328'
})

View File

@@ -1,24 +1,20 @@
import { autoUpdater, UpdateInfo } from 'electron-updater'
import { BrowserWindow, dialog } from 'electron'
import logger from 'electron-log'
import { dialog, ipcMain } from 'electron'
import { AppUpdater as _AppUpdater, autoUpdater, UpdateInfo } from 'electron-updater'
export default class AppUpdater {
constructor() {
autoUpdater: _AppUpdater = autoUpdater
constructor(mainWindow: BrowserWindow) {
logger.transports.file.level = 'debug'
autoUpdater.logger = logger
autoUpdater.forceDevUpdateConfig = true
autoUpdater.autoDownload = false
autoUpdater.checkForUpdates()
// 触发检查更新(此方法用于被渲染线程调用,例如页面点击检查更新按钮来调用此方法)
ipcMain.on('check-for-update', () => {
logger.info('触发检查更新')
return autoUpdater.checkForUpdates()
})
// 检测下载错误
autoUpdater.on('error', (error) => {
logger.error('更新异常', error)
mainWindow.webContents.send('update-error', error)
})
// 检测是否需要更新
@@ -28,6 +24,7 @@ export default class AppUpdater {
autoUpdater.on('update-available', (releaseInfo: UpdateInfo) => {
autoUpdater.logger?.info('检测到新版本,确认是否下载')
mainWindow.webContents.send('update-available', releaseInfo)
const releaseNotes = releaseInfo.releaseNotes
let releaseContent = ''
if (releaseNotes) {
@@ -49,10 +46,12 @@ export default class AppUpdater {
title: '应用有新的更新',
detail: releaseContent,
message: '发现新版本,是否现在更新?',
buttons: ['', '']
buttons: ['下次再说', '更新']
})
.then(({ response }) => {
if (response === 1) {
logger.info('用户选择更新,准备下载更新')
mainWindow.webContents.send('download-update')
autoUpdater.downloadUpdate()
}
})
@@ -61,11 +60,13 @@ export default class AppUpdater {
// 检测到不需要更新时
autoUpdater.on('update-not-available', () => {
logger.info('现在使用的就是最新版本,不用更新')
mainWindow.webContents.send('update-not-available')
})
// 更新下载进度
autoUpdater.on('download-progress', (progress) => {
logger.info('下载进度', progress)
mainWindow.webContents.send('download-progress', progress)
})
// 当需要更新的内容下载完成后
@@ -80,5 +81,7 @@ export default class AppUpdater {
setImmediate(() => autoUpdater.quitAndInstall())
})
})
this.autoUpdater = autoUpdater
}
}

View File

@@ -6,7 +6,13 @@ declare global {
api: {
getAppInfo: () => Promise<{
version: string
isPackaged: boolean
}>
checkForUpdate: () => void
openWebsite: (url: string) => void
setProxy: (proxy: string | undefined) => void
saveFile: (path: string, content: string) => void
setTheme: (theme: 'light' | 'dark') => void
}
}
}

View File

@@ -1,10 +1,14 @@
import { contextBridge, ipcRenderer } from 'electron'
import { electronAPI } from '@electron-toolkit/preload'
import { contextBridge, ipcRenderer } from 'electron'
// Custom APIs for renderer
const api = {
getAppInfo: () => ipcRenderer.invoke('get-app-info'),
checkForUpdate: () => ipcRenderer.invoke('check-for-update')
checkForUpdate: () => ipcRenderer.invoke('check-for-update'),
openWebsite: (url: string) => ipcRenderer.invoke('open-website', url),
setProxy: (proxy: string) => ipcRenderer.invoke('set-proxy', proxy),
saveFile: (path: string, content: string) => ipcRenderer.invoke('save-file', path, content),
setTheme: (theme: 'light' | 'dark') => ipcRenderer.invoke('set-theme', theme)
}
// Use `contextBridge` APIs to expose Electron APIs to

View File

@@ -4,13 +4,11 @@
<meta charset="UTF-8" />
<title>Cherry Studio</title>
<meta name="viewport" content="initial-scale=1, width=device-width" />
<!-- https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP -->
<meta
http-equiv="Content-Security-Policy"
content="default-src 'self'; connect-src *; script-src 'self'; worker-src 'self' blob:; style-src 'self' 'unsafe-inline' *; font-src 'self' *; img-src 'self' data:" />
content="default-src 'self'; connect-src *; script-src 'self' *; worker-src 'self' blob:; style-src 'self' 'unsafe-inline' *; font-src 'self' data: *; img-src 'self' data:" />
</head>
<body theme-mode="dark">
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>

View File

@@ -1,34 +1,38 @@
import '@fontsource/inter'
import store, { persistor } from '@renderer/store'
import { ConfigProvider } from 'antd'
import { Provider } from 'react-redux'
import { HashRouter, Route, Routes } from 'react-router-dom'
import { PersistGate } from 'redux-persist/integration/react'
import Sidebar from './components/app/Sidebar'
import TopViewContainer from './components/TopView'
import { AntdThemeConfig, getAntdLocale } from './config/antd'
import AppsPage from './pages/apps/AppsPage'
import AgentsPage from './pages/agents/AgentsPage'
import HomePage from './pages/home/HomePage'
import SettingsPage from './pages/settings/SettingsPage'
import TranslatePage from './pages/translate/TranslatePage'
import AntdProvider from './providers/AntdProvider'
import { ThemeProvider } from './providers/ThemeProvider'
function App(): JSX.Element {
return (
<ConfigProvider theme={AntdThemeConfig} locale={getAntdLocale()}>
<Provider store={store}>
<ThemeProvider>
<AntdProvider>
<PersistGate loading={null} persistor={persistor}>
<TopViewContainer>
<HashRouter>
<Sidebar />
<Routes>
<Route path="/" element={<HomePage />} />
<Route path="/apps" element={<AppsPage />} />
<Route path="/apps" element={<AgentsPage />} />
<Route path="/translate" element={<TranslatePage />} />
<Route path="/settings/*" element={<SettingsPage />} />
</Routes>
</HashRouter>
</TopViewContainer>
</PersistGate>
</AntdProvider>
</ThemeProvider>
</Provider>
</ConfigProvider>
)
}

View File

@@ -1,20 +0,0 @@
# CHANGES LOG
### v0.2.3 - 2024-07-16
1. Fixed multi-language prompt errors
2. Fixed default model error issues with ZHIPU AI
3. Fixed OpenRouter API detection error issues
4. Fixed multi-language translation errors with model providers
### v0.2.2 - 2024-07-15
1. Fix the issue where the default assistant name is empty.
2. Fix the problem with default language detection during the first installation.
3. Adjust the changelog style.
### v0.2.1 - 2024-07-15
1. **Feature**: Add new feature for pausing message sending
2. **Fix**: Resolve incomplete translation issue upon language switch
3. **Build**: Support for macOS Intel architecture

View File

@@ -1,21 +0,0 @@
# 更新日志
### v0.2.3 - 2024-07-16
1. 修复多语言提示错误
2. 修复智谱AI默认模型错误问题
3. 修复 OpenRouter API 检测出错问题
4. 修复模型提供商多语言翻译错误问题
### v0.2.2 - 2024-07-15
1. 修复默认助理名称为空的问题
2. 修复首次安装默认语言检测问题
3. 更新日志样式微调
### v0.2.1 - 2024-07-15
1. 【功能】新增消息暂停发送功能
2. 【修复】修复多语言切换不彻底问题
3. 【构建】支持 macOS Intel 架构

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -0,0 +1,55 @@
@font-face {
font-family: 'Ubuntu';
src: url('Ubuntu-Regular.ttf') format('truetype');
font-weight: normal;
font-style: normal;
}
@font-face {
font-family: 'Ubuntu';
src: url('Ubuntu-Italic.ttf') format('truetype');
font-weight: normal;
font-style: italic;
}
@font-face {
font-family: 'Ubuntu';
src: url('Ubuntu-Bold.ttf') format('truetype');
font-weight: bold;
font-style: normal;
}
@font-face {
font-family: 'Ubuntu';
src: url('Ubuntu-BoldItalic.ttf') format('truetype');
font-weight: bold;
font-style: italic;
}
@font-face {
font-family: 'Ubuntu';
src: url('Ubuntu-Light.ttf') format('truetype');
font-weight: 300;
font-style: normal;
}
@font-face {
font-family: 'Ubuntu';
src: url('Ubuntu-LightItalic.ttf') format('truetype');
font-weight: 300;
font-style: italic;
}
@font-face {
font-family: 'Ubuntu';
src: url('Ubuntu-Medium.ttf') format('truetype');
font-weight: 500;
font-style: normal;
}
@font-face {
font-family: 'Ubuntu';
src: url('Ubuntu-MediumItalic.ttf') format('truetype');
font-weight: 500;
font-style: italic;
}

View File

@@ -0,0 +1,55 @@
@font-face {
font-family: "iconfont"; /* Project id 4563475 */
src: url('iconfont.woff2?t=1722242729348') format('woff2'),
url('iconfont.woff?t=1722242729348') format('woff'),
url('iconfont.ttf?t=1722242729348') format('truetype');
}
.iconfont {
font-family: "iconfont" !important;
font-size: 16px;
font-style: normal;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
.icon-dark1:before {
content: "\e72f";
}
.icon-theme-light:before {
content: "\e6b7";
}
.icon-translate_line:before {
content: "\e7de";
}
.icon-history:before {
content: "\e758";
}
.icon-hidesidebarhoriz:before {
content: "\e8eb";
}
.icon-showsidebarhoriz:before {
content: "\e944";
}
.icon-a-addchat:before {
content: "\e658";
}
.icon-appstore:before {
content: "\e792";
}
.icon-chat:before {
content: "\e615";
}
.icon-setting:before {
content: "\e78e";
}

Binary file not shown.

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

View File

@@ -1,78 +0,0 @@
$background-color: #121212;
$text-color: #ffffff;
$heading-color: #00b96b;
$link-color: #3498db;
$code-background: #1e1e1e;
$code-color: #f0e7db;
.markdown {
body {
background-color: $background-color;
color: $text-color;
font-family: Arial, sans-serif;
padding: 20px;
}
h1,
h2,
h3,
h4,
h5,
h6 {
color: $heading-color;
}
h1 {
font-size: 22px;
font-weight: 700;
}
h3 {
margin: 10px 0;
font-weight: 500;
font-family: Arial, sans-serif;
}
a {
color: $link-color;
text-decoration: none;
&:hover {
text-decoration: underline;
}
}
strong {
font-weight: bold;
}
pre {
background-color: $code-background;
padding: 10px;
border-radius: 5px;
overflow-x: auto;
}
code {
background-color: $code-background;
color: $code-color;
padding: 2px 4px;
border-radius: 3px;
}
blockquote {
border-left: 4px solid $heading-color;
padding-left: 10px;
margin-left: 0;
color: #b3b3b3;
}
ul,
ol {
padding-left: 30px;
}
li {
margin-bottom: 5px;
}
}

View File

@@ -1,10 +1,7 @@
@import 'https://at.alicdn.com/t/c/font_4563475_hrx8c92awui.css';
@import './markdown.scss';
// @font-face {
// font-family: 'Playwrite';
// src: url(../fonts/Playwrite.ttf) format('truetype');
// }
@import './scrollbar.scss';
@import '../fonts/icon-fonts/iconfont.css';
@import '../fonts/Ubuntu/Ubuntu.css';
:root {
--color-white: #ffffff;
@@ -12,7 +9,7 @@
--color-white-mute: #f2f2f2;
--color-black: #1b1b1f;
--color-black-soft: #303030;
--color-black-soft: #262626;
--color-black-mute: #363636;
--color-gray-1: #515c67;
@@ -27,18 +24,69 @@
--color-background-soft: var(--color-black-soft);
--color-background-mute: var(--color-black-mute);
--color-primary: #135200;
--color-primary-soft: #13520099;
--color-primary-mute: #13520033;
--color-text: var(--color-text-1);
--color-icon: #ffffff99;
--color-icon-white: #ffffff;
--color-border: #ffffff20;
--color-error: #f44336;
--color-link: #1677ff;
--color-code-background: #323232;
--color-scrollbar-thumb: rgba(255, 255, 255, 0.15);
--color-scrollbar-thumb-hover: rgba(255, 255, 255, 0.3);
--navbar-background: rgba(0, 0, 0, 0.8);
--sidebar-background: rgba(0, 0, 0, 0.8);
--navbar-height: 42px;
--sidebar-width: 55px;
--assistants-width: 235px;
--topic-list-width: 250px;
--assistants-width: 245px;
--topic-list-width: 260px;
--settings-width: var(--assistants-width);
--status-bar-height: 40px;
--input-bar-height: 120px;
--input-bar-height: 115px;
}
body[theme-mode='light'] {
--color-white: #ffffff;
--color-white-soft: #f8f8f8;
--color-white-mute: #efefef;
--color-black: #1b1b1f;
--color-black-soft: #262626;
--color-black-mute: #363636;
--color-gray-1: #8e8e93;
--color-gray-2: #aeaeb2;
--color-gray-3: #c7c7cc;
--color-text-1: rgba(0, 0, 0, 1);
--color-text-2: rgba(0, 0, 0, 0.6);
--color-text-3: rgba(0, 0, 0, 0.38);
--color-background: #ffffff;
--color-background-soft: var(--color-white-soft);
--color-background-mute: var(--color-white-mute);
--color-primary: #00b96b;
--color-primary-soft: #00b96b99;
--color-primary-mute: #00b96b33;
--color-text: var(--color-text-1);
--color-icon: #00000099;
--color-icon-white: #000000;
--color-border: #00000028;
--color-error: #f44336;
--color-link: #1677ff;
--color-code-background: #e3e3e3;
--color-scrollbar-thumb: rgba(0, 0, 0, 0.15);
--color-scrollbar-thumb-hover: rgba(0, 0, 0, 0.3);
--navbar-background: rgba(255, 255, 255, 0.8);
--sidebar-background: rgba(255, 255, 255, 0.8);
}
*,
@@ -57,23 +105,11 @@ body {
display: flex;
min-height: 100vh;
color: var(--color-text);
background: var(--color-background);
line-height: 1.6;
overflow: hidden;
background-size: cover;
font-family:
Inter,
-apple-system,
BlinkMacSystemFont,
'Segoe UI',
Roboto,
Oxygen,
Ubuntu,
Cantarell,
'Fira Sans',
'Droid Sans',
'Helvetica Neue',
sans-serif;
background: transparent !important;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans',
'Helvetica Neue', sans-serif;
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
@@ -102,29 +138,20 @@ body,
resize: none;
}
/* 全局初始化滚动条样式 */
::-webkit-scrollbar {
width: 5px;
height: 5px;
.chat-nav-dropdown {
.ant-dropdown-menu {
padding-bottom: 12px;
}
}
::-webkit-scrollbar-track {
background: transparent;
}
::-webkit-scrollbar-thumb {
background: rgba(255, 255, 255, 0.2);
border-radius: 10px;
}
::-webkit-scrollbar-thumb:hover {
background: rgba(255, 255, 255, 0.4);
}
/* Safari 和 Chrome */
@media screen and (-webkit-min-device-pixel-ratio: 0) {
body {
scrollbar-width: thin; /* 告诉 FF 用细滚动条 */
scrollbar-color: rgba(0, 0, 0, 0.4) rgba(0, 0, 0, 0.1); /* FF 前面色后面色 */
}
.loader {
width: 16px;
height: 16px;
border-radius: 50%;
background-color: #000;
box-shadow:
32px 0 #000,
-32px 0 #000;
position: relative;
animation: flash 0.5s ease-out infinite alternate;
}

View File

@@ -1,20 +1,15 @@
.markdown {
color: #fff;
color: var(--color-text);
font-size: 15px;
line-height: 1.6;
user-select: text;
margin-top: 4px;
p:first-of-type {
margin-top: 0;
}
h1:first-of-type,
h2:first-of-type,
h3:first-of-type,
h4:first-of-type,
h5:first-of-type,
h6:first-of-type {
h1:first-child,
h2:first-child,
h3:first-child,
h4:first-child,
h5:first-child,
h6:first-child {
margin-top: 0;
}
@@ -26,62 +21,221 @@
h6 {
margin: 1em 0 1em 0;
font-weight: 800;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans',
'Helvetica Neue', sans-serif;
}
h1 {
font-size: 2em;
color: #fff;
border-bottom: 0.5px solid var(--color-border);
padding-bottom: 0.3em;
}
h2 {
font-size: 1.5em;
color: #fff;
border-bottom: 0.5px solid var(--color-border);
padding-bottom: 0.3em;
}
h3 {
font-size: 1.2em;
color: #fff;
}
h4 {
font-size: 1em;
color: #fff;
}
h5 {
font-size: 0.9em;
color: #fff;
}
h6 {
font-size: 0.8em;
color: #fff;
}
p {
margin: 1em 0;
color: #fff;
&:last-child {
margin-bottom: 5px;
}
&:first-child {
margin-top: 0;
}
}
ul,
ol {
padding-left: 1.5em;
margin: 1em 0;
color: #ccc;
}
li {
margin-bottom: 0.5em;
}
li > ul,
li > ol {
margin: 0.5em 0;
}
hr {
border: none;
border-top: 1px solid #555;
border-top: 0.5px solid var(--color-border);
margin: 20px 0;
background-color: #555;
background-color: var(--color-border);
}
span {
word-break: break-all;
}
code {
white-space: pre-wrap;
font-family: 'Courier New', Courier, monospace;
}
p code {
background: var(--color-background-mute);
padding: 3px 5px;
border-radius: 5px;
}
pre {
white-space: pre-wrap;
padding: 1em;
border-radius: 5px;
overflow-x: auto;
font-family: 'Fira Code', 'Courier New', Courier, monospace;
pre {
margin: 0 !important;
}
code {
background: none;
padding: 0;
border-radius: 0;
}
}
blockquote {
margin: 1em 0;
padding-left: 1em;
color: var(--color-text-light);
border-left: 4px solid var(--color-border);
font-family: Georgia, 'Times New Roman', Times, serif;
}
table {
border-collapse: collapse;
margin: 1em 0;
width: 100%;
}
th,
td {
border: 0.5px solid var(--color-border);
padding: 0.5em;
}
th {
background-color: var(--color-background-mute);
font-weight: bold;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans',
'Helvetica Neue', sans-serif;
}
img {
max-width: 100%;
height: auto;
}
a,
.link {
color: var(--color-link);
text-decoration: none;
cursor: pointer;
&:hover {
text-decoration: underline;
}
}
strong {
font-weight: bold;
}
em {
font-style: italic;
}
del {
text-decoration: line-through;
}
sup,
sub {
font-size: 75%;
line-height: 0;
position: relative;
vertical-align: baseline;
}
sup {
top: -0.5em;
}
sub {
bottom: -0.25em;
}
.footnote-ref {
font-size: 0.8em;
vertical-align: super;
line-height: 0;
margin: 0 2px;
color: var(--color-primary);
text-decoration: none;
&:hover {
text-decoration: underline;
}
}
.footnotes {
margin-top: 1em;
padding-top: 1em;
border-top: 1px solid var(--color-border);
ol {
padding-left: 1em;
}
li {
font-size: 0.9em;
margin-bottom: 0.5em;
color: var(--color-text-light);
p {
display: inline;
margin: 0;
}
}
.footnote-backref {
font-size: 0.8em;
vertical-align: super;
line-height: 0;
margin-left: 5px;
color: var(--color-primary);
text-decoration: none;
&:hover {
text-decoration: underline;
}
}
}
}
emoji-picker {
--border-size: 0;
}

View File

@@ -0,0 +1,16 @@
/* 全局初始化滚动条样式 */
::-webkit-scrollbar {
width: 3px;
height: 3px;
}
::-webkit-scrollbar-track {
background: transparent;
}
::-webkit-scrollbar-thumb {
background: var(--color-scrollbar-thumb);
&:hover {
background: var(--color-scrollbar-thumb-hover);
}
}

View File

@@ -0,0 +1,49 @@
import { DragDropContext, Draggable, Droppable, DropResult } from '@hello-pangea/dnd'
import { droppableReorder } from '@renderer/utils'
import { FC } from 'react'
interface Props<T> {
list: T[]
children: (item: T, index: number) => React.ReactNode
onUpdate: (list: T[]) => void
onDragStart?: () => void
onDragEnd?: () => void
}
const DragableList: FC<Props<any>> = ({ children, list, onDragStart, onUpdate, onDragEnd }) => {
const _onDragEnd = (result: DropResult) => {
onDragEnd?.()
if (result.destination) {
const sourceIndex = result.source.index
const destIndex = result.destination.index
const reorderAgents = droppableReorder(list, sourceIndex, destIndex)
onUpdate(reorderAgents)
}
}
return (
<DragDropContext onDragStart={onDragStart} onDragEnd={_onDragEnd}>
<Droppable droppableId="droppable">
{(provided) => (
<div {...provided.droppableProps} ref={provided.innerRef}>
{list.map((item, index) => (
<Draggable key={`draggable_${item.id}_${index}`} draggableId={item.id} index={index}>
{(provided) => (
<div
ref={provided.innerRef}
{...provided.draggableProps}
{...provided.dragHandleProps}
style={{ ...provided.draggableProps.style, marginBottom: 8 }}>
{children(item, index)}
</div>
)}
</Draggable>
))}
</div>
)}
</Droppable>
</DragDropContext>
)
}
export default DragableList

View File

@@ -0,0 +1,25 @@
import { useTheme } from '@renderer/providers/ThemeProvider'
import { FC, useEffect, useRef } from 'react'
interface Props {
onEmojiClick: (emoji: string) => void
}
const EmojiPicker: FC<Props> = ({ onEmojiClick }) => {
const { theme } = useTheme()
const ref = useRef<HTMLDivElement>(null)
useEffect(() => {
if (ref.current) {
ref.current.addEventListener('emoji-click', (event: any) => {
event.stopPropagation()
onEmojiClick(event.detail.emoji.unicode)
})
}
}, [onEmojiClick])
// @ts-ignore next-line
return <emoji-picker ref={ref} class={theme === 'dark' ? 'dark' : 'light'} style={{ border: 'none' }} />
}
export default EmojiPicker

View File

@@ -150,11 +150,12 @@ export const BaseTypography = styled(Box)<{
`
export const TypographyNormal = styled(BaseTypography)`
font-family: 'Poppins';
font-family: 'Ubuntu';
`
export const TypographyBold = styled(BaseTypography)`
font-family: 'Poppins Bold';
font-family: 'Ubuntu';
font-weight: bold;
`
export const Container = styled.main<ContainerProps>`

View File

@@ -1,11 +1,12 @@
import { Input, Modal } from 'antd'
import { useState } from 'react'
import { TopView } from '../TopView'
import { Box } from '../Layout'
import { Assistant } from '@renderer/types'
import { Input, Modal } from 'antd'
import TextArea from 'antd/es/input/TextArea'
import { useState } from 'react'
import { useTranslation } from 'react-i18next'
import { Box } from '../Layout'
import { TopView } from '../TopView'
interface AssistantSettingPopupShowParams {
assistant: Assistant
}
@@ -56,18 +57,19 @@ const AssistantSettingPopupContainer: React.FC<Props> = ({ assistant, resolve })
export default class AssistantSettingPopup {
static topviewId = 0
static hide() {
TopView.hide(this.topviewId)
TopView.hide('AssistantSettingPopup')
}
static show(props: AssistantSettingPopupShowParams) {
return new Promise<Assistant>((resolve) => {
this.topviewId = TopView.show(
TopView.show(
<AssistantSettingPopupContainer
{...props}
resolve={(v) => {
resolve(v)
this.hide()
}}
/>
/>,
'AssistantSettingPopup'
)
})
}

View File

@@ -1,7 +1,8 @@
import { Input, InputProps, Modal } from 'antd'
import { useState } from 'react'
import { TopView } from '../TopView'
import { Box } from '../Layout'
import { TopView } from '../TopView'
interface PromptPopupShowParams {
title: string
@@ -57,18 +58,19 @@ const PromptPopupContainer: React.FC<Props> = ({
export default class PromptPopup {
static topviewId = 0
static hide() {
TopView.hide(this.topviewId)
TopView.hide('PromptPopup')
}
static show(props: PromptPopupShowParams) {
return new Promise<string>((resolve) => {
this.topviewId = TopView.show(
TopView.show(
<PromptPopupContainer
{...props}
resolve={(v) => {
resolve(v)
this.hide()
}}
/>
/>,
'PromptPopup'
)
})
}

View File

@@ -1,7 +1,8 @@
import { Modal } from 'antd'
import { useState } from 'react'
import { TopView } from '../TopView'
import { Box } from '../Layout'
import { TopView } from '../TopView'
interface ShowParams {
title: string
@@ -36,18 +37,19 @@ const PopupContainer: React.FC<Props> = ({ title, resolve }) => {
export default class TemplatePopup {
static topviewId = 0
static hide() {
TopView.hide(this.topviewId)
TopView.hide('TemplatePopup')
}
static show(props: ShowParams) {
return new Promise<any>((resolve) => {
this.topviewId = TopView.show(
TopView.show(
<PopupContainer
{...props}
resolve={(v) => {
resolve(v)
this.hide()
}}
/>
/>,
'TemplatePopup'
)
})
}

View File

@@ -1,83 +1,104 @@
import { useAppInitEffect } from '@renderer/hooks/useAppInitEffect'
import { useAppInit } from '@renderer/hooks/useAppInit'
import { message, Modal } from 'antd'
import { findIndex, pullAt } from 'lodash'
import React, { useEffect, useState } from 'react'
import React, { PropsWithChildren, useCallback, useEffect, useRef, useState } from 'react'
import { Box } from '../Layout'
let id = 0
let onPop = () => {}
let onShow = ({ element, key }: { element: React.FC | React.ReactNode; key: number }) => {
let onShow = ({ element, id }: { element: React.FC | React.ReactNode; id: string }) => {
element
key
id
}
let onHide = ({ key }: { key: number }) => {
key
let onHide = (id: string) => {
id
}
let onHideAll = () => {}
interface Props {
children?: React.ReactNode
}
type ElementItem = {
key: number
id: string
element: React.FC | React.ReactNode
}
const TopViewContainer: React.FC<Props> = ({ children }) => {
const [elements, setElements] = useState<ElementItem[]>([])
const elementsRef = useRef<ElementItem[]>([])
elementsRef.current = elements
const [messageApi, messageContextHolder] = message.useMessage()
const [modal, modalContextHolder] = Modal.useModal()
useAppInitEffect()
onPop = () => {
const views = [...elements]
views.pop()
setElements(views)
}
onShow = ({ element, key }: { element: React.FC | React.ReactNode; key: number }) => {
setElements(elements.concat([{ element, key }]))
}
onHide = ({ key }: { key: number }) => {
const views = [...elements]
pullAt(views, findIndex(views, { key }))
setElements(views)
}
useAppInit()
useEffect(() => {
window.message = messageApi
window.modal = modal
}, [messageApi, modal])
onPop = () => {
console.debug('[TopView] onPop')
const views = [...elementsRef.current]
views.pop()
elementsRef.current = views
setElements(elementsRef.current)
}
onShow = ({ element, id }: ElementItem) => {
console.debug('[TopView] onShow', id)
if (!elementsRef.current.find((el) => el.id === id)) {
elementsRef.current = elementsRef.current.concat([{ element, id }])
setElements(elementsRef.current)
}
}
onHide = (id: string) => {
console.debug('[TopView] onHide', id, elementsRef.current)
elementsRef.current = elementsRef.current.filter((el) => el.id !== id)
setElements(elementsRef.current)
}
onHideAll = () => {
console.debug('[TopView] onHideAll')
setElements([])
elementsRef.current = []
}
const FullScreenContainer: React.FC<PropsWithChildren> = useCallback(({ children }) => {
return (
<Box flex={1} position="absolute" w="100%" h="100%">
<Box position="absolute" w="100%" h="100%" onClick={onPop} />
{children}
</Box>
)
}, [])
console.debug(
'[TopView]',
elements.map((el) => [el.id, el.element])
)
return (
<>
{children}
{messageContextHolder}
{modalContextHolder}
{elements.length > 0 && (
<div style={{ display: 'flex', flex: 1, position: 'absolute', width: '100%', height: '100%' }}>
<div style={{ position: 'absolute', width: '100%', height: '100%' }} onClick={onPop} />
{elements.map(({ element: Element, key }) =>
typeof Element === 'function' ? <Element key={`TOPVIEW_${key}`} /> : Element
)}
</div>
)}
{elements.map(({ element: Element, id }) => (
<FullScreenContainer key={`TOPVIEW_${id}`}>
{typeof Element === 'function' ? <Element /> : Element}
</FullScreenContainer>
))}
</>
)
}
export const TopView = {
show: (element: React.FC | React.ReactNode) => {
id = id + 1
onShow({ element, key: id })
return id
},
hide: (key: number) => {
onHide({ key })
},
show: (element: React.FC | React.ReactNode, id: string) => onShow({ element, id }),
hide: (id: string) => onHide(id),
clear: () => onHideAll(),
pop: onPop
}

View File

@@ -1,3 +1,4 @@
import { isMac } from '@renderer/config/constant'
import { FC, PropsWithChildren } from 'react'
import styled from 'styled-components'
@@ -25,32 +26,32 @@ const NavbarContainer = styled.div`
flex-direction: row;
min-height: var(--navbar-height);
max-height: var(--navbar-height);
border-bottom: 0.5px solid var(--color-border);
-webkit-app-region: drag;
background-color: #1f1f1f;
margin-left: calc(var(--sidebar-width) * -1);
padding-left: var(--sidebar-width);
padding-left: ${isMac ? 'var(--sidebar-width)' : 0};
border-bottom: 0.5px solid var(--color-border);
background-color: var(--navbar-background);
`
const NavbarLeftContainer = styled.div`
min-width: var(--assistants-width);
border-right: 1px solid var(--color-border);
min-width: ${isMac ? 'var(--assistants-width)' : 'calc(var(--sidebar-width) + var(--assistants-width))'};
padding: 0 10px;
display: flex;
flex-direction: row;
align-items: center;
font-size: 14px;
font-weight: bold;
color: var(--color-text-1);
`
const NavbarCenterContainer = styled.div`
flex: 1;
display: flex;
align-items: center;
padding: 0 ${isMac ? '20px' : '15px'};
font-size: 14px;
font-weight: bold;
color: var(--color-text-1);
text-align: center;
border-right: 1px solid var(--color-border);
padding: 0 20px;
`
const NavbarRightContainer = styled.div`

View File

@@ -1,8 +1,9 @@
import { FC } from 'react'
import { TranslationOutlined } from '@ant-design/icons'
import Logo from '@renderer/assets/images/logo.png'
import styled from 'styled-components'
import { Link, useLocation } from 'react-router-dom'
import useAvatar from '@renderer/hooks/useAvatar'
import { FC } from 'react'
import { Link, useLocation } from 'react-router-dom'
import styled from 'styled-components'
const Sidebar: FC = () => {
const { pathname } = useLocation()
@@ -27,6 +28,11 @@ const Sidebar: FC = () => {
<i className="iconfont icon-appstore"></i>
</Icon>
</StyledLink>
<StyledLink to="/translate">
<Icon className={isRoute('/translate')}>
<TranslationOutlined />
</Icon>
</StyledLink>
</Menus>
</MainMenus>
<Menus>
@@ -44,14 +50,14 @@ const Container = styled.div`
display: flex;
flex-direction: column;
align-items: center;
padding: 12px 0;
min-width: var(--sidebar-width);
min-height: 100%;
padding: 8px 0;
width: var(--sidebar-width);
height: calc(100vh - var(--navbar-height));
-webkit-app-region: drag !important;
background-color: #1f1f1f;
border-right: 0.5px solid var(--color-border);
margin-top: var(--navbar-height);
padding-bottom: calc(var(--navbar-height) + 6px);
margin-bottom: var(--navbar-height);
background-color: var(--sidebar-background);
`
const AvatarImg = styled.img`
@@ -60,6 +66,7 @@ const AvatarImg = styled.img`
height: 28px;
background-color: var(--color-background-soft);
margin: 5px 0;
margin-top: 5px;
`
const MainMenus = styled.div`
display: flex;
@@ -82,22 +89,28 @@ const Icon = styled.div`
margin-bottom: 5px;
transition: background-color 0.2s ease;
-webkit-app-region: none;
.iconfont {
.iconfont,
.anticon {
color: var(--color-icon);
font-size: 20px;
transition: color 0.2s ease;
text-decoration: none;
}
.anticon {
font-size: 17px;
}
&:hover {
background-color: #ffffff30;
background-color: var(--color-background-soft);
cursor: pointer;
.iconfont {
.iconfont,
.anticon {
color: var(--color-icon-white);
}
}
&.active {
background-color: #ffffff20;
.iconfont {
background-color: var(--color-background-mute);
.iconfont,
.anticon {
color: var(--color-icon-white);
}
}

View File

@@ -0,0 +1,242 @@
[
{
"id": "1",
"name": "产品经理 - Product Manager",
"emoji": "🎯",
"group": "职业",
"prompt": "你现在是一名经验丰富的产品经理,你具有深厚的技术背景,并且对市场和用户需求有敏锐的洞察力。你擅长解决复杂的问题,制定有效的产品策略,并优秀地平衡各种资源以实现产品目标。你具有卓越的项目管理能力和出色的沟通技巧,能够有效地协调团队内部和外部的资源。请在这个角色下为我解答以下问题。",
"description": "你现在是一名经验丰富的产品经理,你具有深厚的技术背景,并且对市场和用户需求有敏锐的洞察力。你擅长解决复杂的问题,制定有效的产品策略,并优秀地平衡各种资源以实现产品目标。你具有卓越的项目管理能力和出色的沟通技巧,能够有效地协调团队内部和外部的资源。请在这个角色下为我解答以下问题。"
},
{
"id": "2",
"name": "策略产品经理 - Strategy Product Manager",
"emoji": "🎯 ",
"group": "职业",
"prompt": "你现在是一名策略产品经理,你擅长进行市场研究和竞品分析,以制定产品策略。你能把握行业趋势,了解用户需求,并在此基础上优化产品功能和用户体验。请在这个角色下为我解答以下问题。",
"description": "你现在是一名策略产品经理,你擅长进行市场研究和竞品分析,以制定产品策略。你能把握行业趋势,了解用户需求,并在此基础上优化产品功能和用户体验。请在这个角色下为我解答以下问题。"
},
{
"id": "3",
"name": "社群运营 - Community Operations",
"emoji": "👥",
"group": "职业",
"prompt": "你现在是一名社群运营专家,你擅长激发社群活力,增强用户的参与度和忠诚度。你了解如何管理和引导社群文化,以及如何解决社群内的问题和冲突。请在这个角色下为我解答以下问题。",
"description": "你现在是一名社群运营专家,你擅长激发社群活力,增强用户的参与度和忠诚度。你了解如何管理和引导社群文化,以及如何解决社群内的问题和冲突。请在这个角色下为我解答以下问题。"
},
{
"id": "4",
"name": "内容运营 - Content Operations",
"emoji": "✍️",
"group": "职业",
"prompt": "你现在是一名专业的内容运营人员,你精通内容创作、编辑、发布和优化。你对读者需求有敏锐的感知,擅长通过高质量的内容吸引和保留用户。请在这个角色下为我解答以下问题。",
"description": "你现在是一名专业的内容运营人员,你精通内容创作、编辑、发布和优化。你对读者需求有敏锐的感知,擅长通过高质量的内容吸引和保留用户。请在这个角色下为我解答以下问题。"
},
{
"id": "5",
"name": "商家运营 - Merchant Operations",
"emoji": "🛍️",
"group": "职业",
"prompt": "你现在是一名经验丰富的商家运营专家,你擅长管理商家关系,优化商家业务流程,提高商家满意度。你对电商行业有深入的了解,并有优秀的商业洞察力。请在这个角色下为我解答以下问题。",
"description": "你现在是一名经验丰富的商家运营专家,你擅长管理商家关系,优化商家业务流程,提高商家满意度。你对电商行业有深入的了解,并有优秀的商业洞察力。请在这个角色下为我解答以下问题。"
},
{
"id": "6",
"name": "产品运营 - Product Operations",
"emoji": "🚀",
"group": "职业",
"prompt": "你现在是一名经验丰富的产品运营专家,你擅长分析市场和用户需求,并对产品生命周期各阶段的运营策略有深刻的理解。你有出色的团队协作能力和沟通技巧,能在不同部门间进行有效的协调。请在这个角色下为我解答以下问题。\n",
"description": "你现在是一名经验丰富的产品运营专家,你擅长分析市场和用户需求,并对产品生命周期各阶段的运营策略有深刻的理解。你有出色的团队协作能力和沟通技巧,能在不同部门间进行有效的协调。请在这个角色下为我解答以下问题。\n"
},
{
"id": "7",
"name": "销售运营 - Sales Operations",
"emoji": "💼",
"group": "职业",
"prompt": "你现在是一名销售运营经理,你懂得如何优化销售流程,管理销售数据,提升销售效率。你能制定销售预测和目标,管理销售预算,并提供销售支持。请在这个角色下为我解答以下问题。",
"description": "你现在是一名销售运营经理,你懂得如何优化销售流程,管理销售数据,提升销售效率。你能制定销售预测和目标,管理销售预算,并提供销售支持。请在这个角色下为我解答以下问题。"
},
{
"id": "8",
"name": "用户运营 - User Operations",
"emoji": "👨‍💻",
"group": "职业",
"prompt": "你现在是一名用户运营专家,你了解用户行为和需求,能够制定并执行针对性的用户运营策略。你有出色的用户服务能力,能有效处理用户反馈和投诉。请在这个角色下为我解答以下问题。\n",
"description": "你现在是一名用户运营专家,你了解用户行为和需求,能够制定并执行针对性的用户运营策略。你有出色的用户服务能力,能有效处理用户反馈和投诉。请在这个角色下为我解答以下问题。\n"
},
{
"id": "9",
"name": "市场营销 - Marketing",
"emoji": "📢",
"group": "职业",
"prompt": "你现在是一名专业的市场营销专家,你对营销策略和品牌推广有深入的理解。你熟知如何有效利用不同的渠道和工具来达成营销目标,并对消费者心理有深入的理解。请在这个角色下为我解答以下问题。",
"description": "你现在是一名专业的市场营销专家,你对营销策略和品牌推广有深入的理解。你熟知如何有效利用不同的渠道和工具来达成营销目标,并对消费者心理有深入的理解。请在这个角色下为我解答以下问题。"
},
{
"id": "10",
"name": "商业数据分析 - Business Data Analysis",
"emoji": "📈",
"group": "职业",
"prompt": "你现在是一名商业数据分析师,你精通数据分析方法和工具,能够从大量数据中提取出有价值的商业洞察。你对业务运营有深入的理解,并能提供数据驱动的优化建议。请在这个角色下为我解答以下问题。",
"description": "你现在是一名商业数据分析师,你精通数据分析方法和工具,能够从大量数据中提取出有价值的商业洞察。你对业务运营有深入的理解,并能提供数据驱动的优化建议。请在这个角色下为我解答以下问题。"
},
{
"id": "11",
"name": "项目管理 - Project Management",
"emoji": "🗂️",
"group": "职业",
"prompt": "你现在是一名资深的项目经理,你精通项目管理的各个方面,包括规划、组织、执行和控制。你擅长处理项目风险,解决问题,并有效地协调团队成员以实现项目目标。请在这个角色下为我解答以下问题。",
"description": "你现在是一名资深的项目经理,你精通项目管理的各个方面,包括规划、组织、执行和控制。你擅长处理项目风险,解决问题,并有效地协调团队成员以实现项目目标。请在这个角色下为我解答以下问题。"
},
{
"id": "12",
"name": "SEO专家 - SEO Expert",
"emoji": "🔎",
"group": "职业",
"prompt": "你现在是一名知识丰富的SEO专家你了解搜索引擎的工作原理熟知如何优化网页以提高其在搜索引擎中的排名。你对关键词研究、内容优化、链接建设等SEO策略有深入的了解。请在这个角色下为我解答以下问题。",
"description": "你现在是一名知识丰富的SEO专家你了解搜索引擎的工作原理熟知如何优化网页以提高其在搜索引擎中的排名。你对关键词研究、内容优化、链接建设等SEO策略有深入的了解。请在这个角色下为我解答以下问题。"
},
{
"id": "13",
"name": "网站运营数据分析 - Website Operations Data Analysis",
"emoji": "💻",
"group": "职业",
"prompt": "你现在是一名网站运营数据分析师,你擅长收集和分析网站数据,以了解用户行为和网站性能。你可以提供关于网站设计、内容和营销策略的数据支持。请在这个角色下为我解答以下问题。\n",
"description": "你现在是一名网站运营数据分析师,你擅长收集和分析网站数据,以了解用户行为和网站性能。你可以提供关于网站设计、内容和营销策略的数据支持。请在这个角色下为我解答以下问题。\n"
},
{
"id": "14",
"name": "数据分析师 - Data Analyst",
"emoji": "📊",
"group": "职业",
"prompt": "你现在是一名数据分析师,你精通各种统计分析方法,懂得如何清洗、处理和解析数据以获得有价值的洞察。你擅长利用数据驱动的方式来解决问题和提升决策效率。请在这个角色下为我解答以下问题。",
"description": "你现在是一名数据分析师,你精通各种统计分析方法,懂得如何清洗、处理和解析数据以获得有价值的洞察。你擅长利用数据驱动的方式来解决问题和提升决策效率。请在这个角色下为我解答以下问题。"
},
{
"id": "15",
"name": "前端工程师 - Frontend Engineer",
"emoji": "🖥️",
"group": "职业",
"prompt": "你现在是一名专业的前端工程师你对HTML、CSS、JavaScript等前端技术有深入的了解能够制作和优化用户界面。你能够解决浏览器兼容性问题提升网页性能并实现优秀的用户体验。请在这个角色下为我解答以下问题。\n",
"description": "你现在是一名专业的前端工程师你对HTML、CSS、JavaScript等前端技术有深入的了解能够制作和优化用户界面。你能够解决浏览器兼容性问题提升网页性能并实现优秀的用户体验。请在这个角色下为我解答以下问题。\n"
},
{
"id": "16",
"name": "运维工程师 - Operations Engineer",
"emoji": "🛠️",
"group": "职业",
"prompt": "你现在是一名运维工程师,你负责保障系统和服务的正常运行。你熟悉各种监控工具,能够高效地处理故障和进行系统优化。你还懂得如何进行数据备份和恢复,以保证数据安全。请在这个角色下为我解答以下问题。",
"description": "你现在是一名运维工程师,你负责保障系统和服务的正常运行。你熟悉各种监控工具,能够高效地处理故障和进行系统优化。你还懂得如何进行数据备份和恢复,以保证数据安全。请在这个角色下为我解答以下问题。"
},
{
"id": "17",
"name": "开发工程师 - Software Engineer",
"emoji": "💻",
"group": "职业",
"prompt": "你现在是一名资深的软件工程师,你熟悉多种编程语言和开发框架,对软件开发的生命周期有深入的理解。你擅长解决技术问题,并具有优秀的逻辑思维能力。请在这个角色下为我解答以下问题。",
"description": "你现在是一名资深的软件工程师,你熟悉多种编程语言和开发框架,对软件开发的生命周期有深入的理解。你擅长解决技术问题,并具有优秀的逻辑思维能力。请在这个角色下为我解答以下问题。"
},
{
"id": "18",
"name": "测试工程师 - Test Engineer",
"emoji": "🧪",
"group": "职业",
"prompt": "你现在是一名专业的测试工程师,你对软件测试方法论和测试工具有深入的了解。你的主要任务是发现和记录软件的缺陷,并确保软件的质量。你在寻找和解决问题上有出色的技能。请在这个角色下为我解答以下问题。",
"description": "你现在是一名专业的测试工程师,你对软件测试方法论和测试工具有深入的了解。你的主要任务是发现和记录软件的缺陷,并确保软件的质量。你在寻找和解决问题上有出色的技能。请在这个角色下为我解答以下问题。"
},
{
"id": "19",
"name": "HR人力资源管理 - Human Resources Management",
"emoji": "👥",
"group": "职业",
"prompt": "你现在是一名人力资源管理专家,你了解如何招聘、培训、评估和激励员工。你精通劳动法规,擅长处理员工关系,并且在组织发展和变革管理方面有深入的见解。请在这个角色下为我解答以下问题。",
"description": "你现在是一名人力资源管理专家,你了解如何招聘、培训、评估和激励员工。你精通劳动法规,擅长处理员工关系,并且在组织发展和变革管理方面有深入的见解。请在这个角色下为我解答以下问题。"
},
{
"id": "20",
"name": "行政 - Administration",
"emoji": "📋",
"group": "职业",
"prompt": "你现在是一名行政专员,你擅长组织和管理公司的日常运营事务,包括文件管理、会议安排、办公设施管理等。你有良好的人际沟通和组织能力,能在多任务环境中有效工作。请在这个角色下为我解答以下问题。",
"description": "你现在是一名行政专员,你擅长组织和管理公司的日常运营事务,包括文件管理、会议安排、办公设施管理等。你有良好的人际沟通和组织能力,能在多任务环境中有效工作。请在这个角色下为我解答以下问题。"
},
{
"id": "21",
"name": "财务顾问 - Financial Advisor",
"emoji": "💰",
"group": "职业",
"prompt": "你现在是一名财务顾问,你对金融市场、投资策略和财务规划有深厚的理解。你能提供财务咨询服务,帮助客户实现其财务目标。你擅长理解和解决复杂的财务问题。请在这个角色下为我解答以下问题。",
"description": "你现在是一名财务顾问,你对金融市场、投资策略和财务规划有深厚的理解。你能提供财务咨询服务,帮助客户实现其财务目标。你擅长理解和解决复杂的财务问题。请在这个角色下为我解答以下问题。"
},
{
"id": "22",
"name": "医生 - Doctor",
"emoji": "🩺",
"group": "职业",
"prompt": "你现在是一名医生,具备丰富的医学知识和临床经验。你擅长诊断和治疗各种疾病,能为病人提供专业的医疗建议。你有良好的沟通技巧,能与病人和他们的家人建立信任关系。请在这个角色下为我解答以下问题。",
"description": "你现在是一名医生,具备丰富的医学知识和临床经验。你擅长诊断和治疗各种疾病,能为病人提供专业的医疗建议。你有良好的沟通技巧,能与病人和他们的家人建立信任关系。请在这个角色下为我解答以下问题。"
},
{
"id": "23",
"name": "编辑 - Editor",
"emoji": "✒️",
"group": "职业",
"prompt": "你现在是一名编辑,你对文字有敏锐的感觉,擅长审校和修订稿件以确保其质量。你有出色的语言和沟通技巧,能与作者有效地合作以改善他们的作品。你对出版流程有深入的了解。请在这个角色下为我解答以下问题。\n",
"description": "你现在是一名编辑,你对文字有敏锐的感觉,擅长审校和修订稿件以确保其质量。你有出色的语言和沟通技巧,能与作者有效地合作以改善他们的作品。你对出版流程有深入的了解。请在这个角色下为我解答以下问题。\n"
},
{
"id": "24",
"name": "哲学家 - Philosopher",
"emoji": "🧠",
"group": "职业",
"prompt": "你现在是一名哲学家,你对世界的本质和人类存在的意义有深入的思考。你熟悉多种哲学流派,并能从哲学的角度分析和解决问题。你具有深刻的思维和出色的逻辑分析能力。请在这个角色下为我解答以下问题。\n",
"description": "你现在是一名哲学家,你对世界的本质和人类存在的意义有深入的思考。你熟悉多种哲学流派,并能从哲学的角度分析和解决问题。你具有深刻的思维和出色的逻辑分析能力。请在这个角色下为我解答以下问题。\n"
},
{
"id": "25",
"name": "采购 - Procurement",
"emoji": "🛒",
"group": "职业",
"prompt": "你现在是一名采购经理,你熟悉供应链管理,擅长进行供应商评估和价格谈判。你负责制定和执行采购策略,以保证货物的质量和供应的稳定。请在这个角色下为我解答以下问题。\n",
"description": "你现在是一名采购经理,你熟悉供应链管理,擅长进行供应商评估和价格谈判。你负责制定和执行采购策略,以保证货物的质量和供应的稳定。请在这个角色下为我解答以下问题。\n"
},
{
"id": "26",
"name": "法务 - Legal Affairs",
"emoji": "⚖️",
"group": "职业",
"prompt": "你现在是一名法务专家,你了解公司法、合同法等相关法律,能为企业提供法律咨询和风险评估。你还擅长处理法律争端,并能起草和审核合同。请在这个角色下为我解答以下问题。",
"description": "你现在是一名法务专家,你了解公司法、合同法等相关法律,能为企业提供法律咨询和风险评估。你还擅长处理法律争端,并能起草和审核合同。请在这个角色下为我解答以下问题。"
},
{
"id": "27",
"name": "翻译成中文 - Chinese",
"emoji": "🇨🇳",
"group": "语言",
"prompt": "你是一个好用的翻译助手。请将我的英文翻译成中文,将所有非中文的翻译成中文。我发给你所有的话都是需要翻译的内容,你只需要回答翻译结果。翻译结果请符合中文的语言习惯。",
"description": ""
},
{
"id": "28",
"name": "翻译成英文 - English",
"emoji": "🌐",
"group": "语言",
"prompt": "你是一个好用的翻译助手。请将我的中文翻译成英文,将所有非中文的翻译成英文。我发给你所有的话都是需要翻译的内容,你只需要回答翻译结果。翻译结果请符合英文的语言习惯。",
"description": ""
},
{
"id": "29",
"name": "英语单词背诵助手",
"emoji": "📕",
"group": "语言",
"prompt": "- 版本0.1\n- 语言:中文\n- 描述:您是一位语言专家,擅长阐释英语词汇的复杂性。您的角色是将复杂的英语单词分解为简单的概念,提供易懂的英语解释,提供中文翻译,并提供助记设备以帮助记忆。\n\n技能\n1. 分析高级英语单词的拼写、发音和含义。\n2. 使用简单的英语词汇进行解释,然后提供中文翻译。\n3. 使用音标联想、形象联想和词源等记忆技巧。\n4. 创作高质量的句子,以示范单词在语境中的使用。\n\n规则\n1. 总是以使用简单的英语词汇进行解释为开头。\n2. 在适当的时候,保持解释和例句的清晰、准确和幽默。\n3. 确保助记设备与记忆相关且有效。\n\n工作流程\n1. 问候用户并询问他们感兴趣的英语单词。\n2. 分解单词,分析其拼写、发音和复杂含义。\n3. 用简单的英语词汇解释,使含义更易理解。\n4. 提供单词的中文翻译和简单的英语解释。\n5. 针对单词的特点提供个性化的助记策略。\n6. 使用单词构建高质量、信息丰富且引人入胜的句子。\n\n初始化\n作为一名<角色>,您必须遵循<规则>并使用<语言>进行沟通。在问候用户时,确认他们想要理解和记忆的英语单词,然后按照<工作流程>进行操作。",
"description": ""
},
{
"id": "30",
"name": "文章总结 - Summarize",
"emoji": "📖",
"group": "阅读",
"prompt": "总结下面的文章,给出总结、摘要、观点三个部分内容,其中观点部分要使用列表列出,使用 Markdown 回复",
"description": ""
}
]

View File

@@ -1,26 +0,0 @@
import store from '@renderer/store'
import { theme, ThemeConfig } from 'antd'
import zhCN from 'antd/locale/zh_CN'
export const colorPrimary = '#00b96b'
export const AntdThemeConfig: ThemeConfig = {
token: {
colorPrimary,
borderRadius: 5
},
algorithm: [theme.darkAlgorithm]
}
export function getAntdLocale() {
const language = store.getState().settings.language
switch (language) {
case 'zh-CN':
return zhCN
case 'en-US':
return undefined
default:
return zhCN
}
}

View File

@@ -1,130 +0,0 @@
import { SystemAssistant } from '@renderer/types'
export const SYSTEM_ASSISTANTS: SystemAssistant[] = [
{
id: '6B1D8E9F-9B7F-4E2B-8FBB-0F5B6F7B0D29',
name: '文章总结',
prompt: '总结下面的文章,给出总结、摘要、观点三个部分内容,其中观点部分要使用列表列出,使用 Markdown 回复',
group: '文章'
},
{
id: '6B1D8E9F-9B7F-4E2B-8FBB-0F5B6F7B0D30',
name: '论文',
prompt:
'我希望你能作为一名学者行事。你将负责研究一个你选择的主题,并将研究结果以论文或文章的形式呈现出来。你的任务是确定可靠的来源,以结构良好的方式组织材料,并以引用的方式准确记录。',
group: '写作'
},
{
id: '6B1D8E9F-9B7F-4E2B-8FBB-0F5B6F7B0D40',
name: '翻译成中文',
prompt:
'你是一个好用的翻译助手。请将我的英文翻译成中文,将所有非中文的翻译成中文。我发给你所有的话都是需要翻译的内容,你只需要回答翻译结果。翻译结果请符合中文的语言习惯。',
group: '翻译'
},
{
id: '6B1D8E9F-9B7F-4E2B-8FBB-0F5B6F7B0D41',
name: '翻译成英文',
prompt:
'你是一个好用的翻译助手。请将我的中文翻译成英文,将所有非中文的翻译成英文。我发给你所有的话都是需要翻译的内容,你只需要回答翻译结果。翻译结果请符合英文的语言习惯。',
group: '翻译'
},
{
id: '43CEDACF-C9EB-431B-848C-4D08EC26EB90',
name: '软件工程师',
prompt:
'你是一个高级软件工程师,你需要帮我解答各种技术难题、设计技术方案以及编写代码。你编写的代码必须可以正常运行,而且没有任何 Bug 和其他问题。',
group: '软件工程师'
},
{
id: '6B1D8E9F-9B7F-4E2B-8FBB-0F5B6F7B0D2A',
name: '前端工程师',
prompt:
'你擅长使用 TypeScript, JavaScript, HMLT, CSS 等编程语言。同时你还会使用 Node.js 及各种包来解决开发中遇到的问题。你还会使用 React, Vue 等前端框架。对于我的问题希望你能给出具体的代码示例,最好能够封装成一个函数方便我复制运行测试。',
group: '软件工程师'
},
{
id: '6B1D8E9F-9B7F-4E2B-8FBB-0F5B6F7B0D2B',
name: '后端工程师',
prompt:
'高级后端工程师技术难题解答服务器架构数据库优化API设计网络安全代码审查性能调优微服务分布式系统容器技术持续集成/持续部署(CI/CD)。',
group: '软件工程师'
},
{
id: '6B1D8E9F-9B7F-4E2B-8FBB-0F5B6F7B0D2D',
name: '测试工程师',
prompt: '你是一个高级测试工程师,你需要帮我解答各种技术难题',
group: '软件工程师'
},
{
id: '6B1D8E9F-9B7F-4E2B-8FBB-0F5B6F7B0D2E',
name: 'Python 工程师',
prompt: '你是一个高级Python工程师你需要帮我解答各种技术难题',
group: '编程语言'
},
{
id: '6B1D8E9F-9B7F-4E2B-8FBB-0F5B6F7B0D2F',
name: 'Java 工程师',
prompt: '你是一个高级Java工程师你需要帮我解答各种技术难题',
group: '编程语言'
},
{
id: '6B1D8E9F-9B7F-4E2B-8FBB-0F5B6F7B0D30',
name: 'C# 工程师',
prompt: '你是一个高级C#工程师,你需要帮我解答各种技术难题',
group: '编程语言'
},
{
id: '6B1D8E9F-9B7F-4E2B-8FBB-0F5B6F7B0D31',
name: 'C++ 工程师',
prompt: '你是一个高级C++工程师,你需要帮我解答各种技术难题',
group: '编程语言'
},
{
id: '6B1D8E9F-9B7F-4E2B-8FBB-0F5B6F7B0D32',
name: 'C 工程师',
prompt: '你是一个高级C工程师你需要帮我解答各种技术难题',
group: '编程语言'
},
{
id: '6B1D8E9F-9B7F-4E2B-8FBB-0F5B6F7B0D33',
name: 'Go 工程师',
prompt: '你是一个高级Go工程师你需要帮我解答各种技术难题',
group: '编程语言'
},
{
id: '6B1D8E9F-9B7F-4E2B-8FBB-0F5B6F7B0D34',
name: 'Rust 工程师',
prompt: '你是一个高级Rust工程师你需要帮我解答各种技术难题',
group: '编程语言'
},
{
id: '6B1D8E9F-9B7F-4E2B-8FBB-0F5B6F7B0D35',
name: 'PHP 工程师',
prompt: '你是一个高级PHP工程师你需要帮我解答各种技术难题',
group: '编程语言'
},
{
id: '6B1D8E9F-9B7F-4E2B-8FBB-0F5B6F7B0D36',
name: 'Ruby 工程师',
prompt: '你是一个高级Ruby工程师你需要帮我解答各种技术难题',
group: '编程语言'
},
{
id: '6B1D8E9F-9B7F-4E2B-8FBB-0F5B6F7B0D37',
name: 'Swift 工程师',
prompt: '你是一个高级Swift工程师你需要帮我解答各种技术难题',
group: '编程语言'
},
{
id: '6B1D8E9F-9B7F-4E2B-8FBB-0F5B6F7B0D38',
name: 'Kotlin 工程师',
prompt: '你是一个高级Kotlin工程师你需要帮我解答各种技术难题',
group: '编程语言'
},
{
id: '6B1D8E9F-9B7F-4E2B-8FBB-0F5B6F7B0D39',
name: 'Dart 工程师',
prompt: '你是一个高级Dart工程师你需要帮我解答各种技术难题',
group: '编程语言'
}
]

View File

@@ -0,0 +1,8 @@
export const DEFAULT_TEMPERATURE = 0.7
export const DEFAULT_CONEXTCOUNT = 5
export const DEFAULT_MAX_TOKENS = 4096
export const FONT_FAMILY =
"Ubuntu, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif"
export const platform = window.electron?.process?.platform === 'darwin' ? 'macos' : 'windows'
export const isMac = platform === 'macos'
export const isWindows = platform === 'windows'

View File

@@ -1,36 +1,36 @@
import { Model } from '@renderer/types'
type SystemModel = Model & { defaultEnabled: boolean }
type SystemModel = Model & { enabled: boolean }
export const SYSTEM_MODELS: Record<string, SystemModel[]> = {
openai: [
{
id: 'gpt-3.5-turbo',
id: 'gpt-4o',
provider: 'openai',
name: 'GPT-3.5 Turbo',
group: 'GPT 3.5',
defaultEnabled: true
name: ' GPT-4o',
group: 'GPT 4o',
enabled: true
},
{
id: 'gpt-4o-mini',
provider: 'openai',
name: ' GPT-4o-mini',
group: 'GPT 4o',
enabled: true
},
{
id: 'gpt-4-turbo',
provider: 'openai',
name: ' GPT-4 Turbo',
group: 'GPT 4',
defaultEnabled: true
enabled: true
},
{
id: 'gpt-4',
provider: 'openai',
name: ' GPT-4',
group: 'GPT 4',
defaultEnabled: true
},
{
id: 'gpt-4o',
provider: 'openai',
name: ' GPT-4o',
group: 'GPT 4o',
defaultEnabled: true
enabled: true
}
],
silicon: [
@@ -39,112 +39,112 @@ export const SYSTEM_MODELS: Record<string, SystemModel[]> = {
provider: 'silicon',
name: 'Qwen2-7B-Instruct',
group: 'Qwen2',
defaultEnabled: true
enabled: true
},
{
id: 'Qwen/Qwen2-1.5B-Instruct',
provider: 'silicon',
name: 'Qwen2-1.5B-Instruct',
group: 'Qwen2',
defaultEnabled: false
enabled: false
},
{
id: 'Qwen/Qwen1.5-7B-Chat',
provider: 'silicon',
name: 'Qwen1.5-7B-Chat',
group: 'Qwen1.5',
defaultEnabled: false
enabled: false
},
{
id: 'Qwen/Qwen2-72B-Instruct',
provider: 'silicon',
name: 'Qwen2-72B-Instruct',
group: 'Qwen2',
defaultEnabled: true
enabled: true
},
{
id: 'Qwen/Qwen2-57B-A14B-Instruct',
provider: 'silicon',
name: 'Qwen2-57B-A14B-Instruct',
group: 'Qwen2',
defaultEnabled: false
enabled: false
},
{
id: 'Qwen/Qwen1.5-110B-Chat',
provider: 'silicon',
name: 'Qwen1.5-110B-Chat',
group: 'Qwen1.5',
defaultEnabled: false
enabled: false
},
{
id: 'Qwen/Qwen1.5-32B-Chat',
provider: 'silicon',
name: 'Qwen1.5-32B-Chat',
group: 'Qwen1.5',
defaultEnabled: false
enabled: false
},
{
id: 'Qwen/Qwen1.5-14B-Chat',
provider: 'silicon',
name: 'Qwen1.5-14B-Chat',
group: 'Qwen1.5',
defaultEnabled: false
enabled: false
},
{
id: 'deepseek-ai/DeepSeek-V2-Chat',
provider: 'silicon',
name: 'DeepSeek-V2-Chat',
group: 'DeepSeek',
defaultEnabled: false
enabled: false
},
{
id: 'deepseek-ai/DeepSeek-Coder-V2-Instruct',
provider: 'silicon',
name: 'DeepSeek-Coder-V2-Instruct',
group: 'DeepSeek',
defaultEnabled: false
enabled: false
},
{
id: 'deepseek-ai/deepseek-llm-67b-chat',
provider: 'silicon',
name: 'Deepseek-LLM-67B-Chat',
group: 'DeepSeek',
defaultEnabled: false
enabled: false
},
{
id: 'THUDM/glm-4-9b-chat',
provider: 'silicon',
name: 'GLM-4-9B-Chat',
group: 'GLM',
defaultEnabled: true
enabled: true
},
{
id: 'THUDM/chatglm3-6b',
provider: 'silicon',
name: 'GhatGLM3-6B',
group: 'GLM',
defaultEnabled: false
enabled: false
},
{
id: '01-ai/Yi-1.5-9B-Chat-16K',
provider: 'silicon',
name: 'Yi-1.5-9B-Chat-16K',
group: 'Yi',
defaultEnabled: false
enabled: false
},
{
id: '01-ai/Yi-1.5-6B-Chat',
provider: 'silicon',
name: 'Yi-1.5-6B-Chat',
group: 'Yi',
defaultEnabled: false
enabled: false
},
{
id: '01-ai/Yi-1.5-34B-Chat-16K',
provider: 'silicon',
name: 'Yi-1.5-34B-Chat-16K',
group: 'Yi',
defaultEnabled: false
enabled: false
}
],
deepseek: [
@@ -153,14 +153,14 @@ export const SYSTEM_MODELS: Record<string, SystemModel[]> = {
provider: 'deepseek',
name: 'DeepSeek Chat',
group: 'DeepSeek Chat',
defaultEnabled: true
enabled: true
},
{
id: 'deepseek-coder',
provider: 'deepseek',
name: 'DeepSeek Coder',
group: 'DeepSeek Coder',
defaultEnabled: true
enabled: true
}
],
yi: [
@@ -169,42 +169,42 @@ export const SYSTEM_MODELS: Record<string, SystemModel[]> = {
provider: 'yi',
name: 'Yi-Large',
group: 'Yi',
defaultEnabled: false
enabled: false
},
{
id: 'yi-large-turbo',
provider: 'yi',
name: 'Yi-Large-Turbo',
group: 'Yi',
defaultEnabled: true
enabled: true
},
{
id: 'yi-large-rag',
provider: 'yi',
name: 'Yi-Large-Rag',
group: 'Yi',
defaultEnabled: false
enabled: false
},
{
id: 'yi-medium',
provider: 'yi',
name: 'Yi-Medium',
group: 'Yi',
defaultEnabled: true
enabled: true
},
{
id: 'yi-medium-200k',
provider: 'yi',
name: 'Yi-Medium-200k',
group: 'Yi',
defaultEnabled: false
enabled: false
},
{
id: 'yi-spark',
provider: 'yi',
name: 'Yi-Spark',
group: 'Yi',
defaultEnabled: false
enabled: false
}
],
zhipu: [
@@ -213,42 +213,42 @@ export const SYSTEM_MODELS: Record<string, SystemModel[]> = {
provider: 'zhipu',
name: 'GLM-4-0520',
group: 'GLM',
defaultEnabled: true
enabled: true
},
{
id: 'glm-4',
provider: 'zhipu',
name: 'GLM-4',
group: 'GLM',
defaultEnabled: false
enabled: false
},
{
id: 'glm-4-airx',
provider: 'zhipu',
name: 'GLM-4-AirX',
group: 'GLM',
defaultEnabled: false
enabled: false
},
{
id: 'glm-4-air',
provider: 'zhipu',
name: 'GLM-4-Air',
group: 'GLM',
defaultEnabled: true
enabled: true
},
{
id: 'glm-4v',
provider: 'zhipu',
name: 'GLM-4V',
group: 'GLM',
defaultEnabled: false
enabled: false
},
{
id: 'glm-4-alltools',
provider: 'zhipu',
name: 'GLM-4-AllTools',
group: 'GLM',
defaultEnabled: false
enabled: false
}
],
moonshot: [
@@ -257,21 +257,83 @@ export const SYSTEM_MODELS: Record<string, SystemModel[]> = {
provider: 'moonshot',
name: 'Moonshot V1 8k',
group: 'Moonshot V1',
defaultEnabled: true
enabled: true
},
{
id: 'moonshot-v1-32k',
provider: 'moonshot',
name: 'Moonshot V1 32k',
group: 'Moonshot V1',
defaultEnabled: true
enabled: true
},
{
id: 'moonshot-v1-128k',
provider: 'moonshot',
name: 'Moonshot V1 128k',
group: 'Moonshot V1',
defaultEnabled: true
enabled: true
}
],
baichuan: [
{
id: 'Baichuan4',
provider: 'baichuan',
name: 'Baichuan4',
group: 'Baichuan4',
enabled: true
},
{
id: 'Baichuan3-Turbo',
provider: 'baichuan',
name: 'Baichuan3 Turbo',
group: 'Baichuan3',
enabled: true
},
{
id: 'Baichuan3-Turbo-128k',
provider: 'baichuan',
name: 'Baichuan3 Turbo 128k',
group: 'Baichuan3',
enabled: true
}
],
dashscope: [
{
id: 'qwen-turbo',
provider: 'dashscope',
name: 'Qwen Turbo',
group: 'Qwen',
enabled: true
},
{
id: 'qwen-plus',
provider: 'dashscope',
name: 'Qwen Plus',
group: 'Qwen',
enabled: true
},
{
id: 'qwen-max',
provider: 'dashscope',
name: 'Qwen Max',
group: 'Qwen',
enabled: true
}
],
aihubmix: [
{
id: 'gpt-4o-mini',
provider: 'aihubmix',
name: 'GPT-4o Mini',
group: 'GPT-4o',
enabled: true
},
{
id: 'aihubmix-Llama-3-70B-Instruct',
provider: 'aihubmix',
name: 'Llama 3 70B Instruct',
group: 'Llama3',
enabled: true
}
],
openrouter: [
@@ -280,35 +342,35 @@ export const SYSTEM_MODELS: Record<string, SystemModel[]> = {
provider: 'openrouter',
name: 'Google: Gemma 2 9B',
group: 'Gemma',
defaultEnabled: true
enabled: true
},
{
id: 'microsoft/phi-3-mini-128k-instruct:free',
provider: 'openrouter',
name: 'Phi-3 Mini 128K Instruct',
group: 'Phi',
defaultEnabled: true
enabled: true
},
{
id: 'microsoft/phi-3-medium-128k-instruct:free',
provider: 'openrouter',
name: 'Phi-3 Medium 128K Instruct',
group: 'Phi',
defaultEnabled: true
enabled: true
},
{
id: 'meta-llama/llama-3-8b-instruct:free',
provider: 'openrouter',
name: 'Meta: Llama 3 8B Instruct',
group: 'Llama3',
defaultEnabled: true
enabled: true
},
{
id: 'mistralai/mistral-7b-instruct:free',
provider: 'openrouter',
name: 'Mistral: Mistral 7B Instruct',
group: 'Mistral',
defaultEnabled: true
enabled: true
}
],
groq: [
@@ -317,28 +379,58 @@ export const SYSTEM_MODELS: Record<string, SystemModel[]> = {
provider: 'groq',
name: 'LLaMA3 8B',
group: 'Llama3',
defaultEnabled: false
enabled: false
},
{
id: 'llama3-70b-8192',
provider: 'groq',
name: 'LLaMA3 70B',
group: 'Llama3',
defaultEnabled: true
enabled: true
},
{
id: 'mixtral-8x7b-32768',
provider: 'groq',
name: 'Mixtral 8x7B',
group: 'Mixtral',
defaultEnabled: false
enabled: false
},
{
id: 'gemma-7b-it',
provider: 'groq',
name: 'Gemma 7B',
group: 'Gemma',
defaultEnabled: false
enabled: false
}
],
anthropic: [
{
id: 'claude-3-5-sonnet-20240620',
provider: 'anthropic',
name: 'Claude 3.5 Sonnet',
group: 'Claude 3.5',
enabled: true
},
{
id: 'claude-3-opus-20240229',
provider: 'anthropic',
name: 'Claude 3 Opus',
group: 'Claude 3',
enabled: true
},
{
id: 'claude-3-sonnet-20240229',
provider: 'anthropic',
name: 'Claude 3 Sonnet',
group: 'Claude 3',
enabled: true
},
{
id: 'claude-3-haiku-20240307',
provider: 'anthropic',
name: 'Claude 3 Haiku',
group: 'Claude 3',
enabled: true
}
]
}

View File

@@ -1,5 +1,98 @@
import BaichuanModelLogo from '@renderer/assets/images/models/baichuan.png'
import ChatGLMModelLogo from '@renderer/assets/images/models/chatglm.jpeg'
import ChatGPTModelLogo from '@renderer/assets/images/models/chatgpt.jpeg'
import ClaudeModelLogo from '@renderer/assets/images/models/claude.png'
import DeepSeekModelLogo from '@renderer/assets/images/models/deepseek.png'
import GemmaModelLogo from '@renderer/assets/images/models/gemma.jpeg'
import LlamaModelLogo from '@renderer/assets/images/models/llama.jpeg'
import MicrosoftModelLogo from '@renderer/assets/images/models/microsoft.png'
import MixtralModelLogo from '@renderer/assets/images/models/mixtral.jpeg'
import QwenModelLogo from '@renderer/assets/images/models/qwen.png'
import YiModelLogo from '@renderer/assets/images/models/yi.svg'
import AiHubMixProviderLogo from '@renderer/assets/images/providers/aihubmix.jpg'
import AnthropicProviderLogo from '@renderer/assets/images/providers/anthropic.jpeg'
import BaichuanProviderLogo from '@renderer/assets/images/providers/baichuan.png'
import DashScopeProviderLogo from '@renderer/assets/images/providers/dashscope.png'
import DeepSeekProviderLogo from '@renderer/assets/images/providers/deepseek.png'
import GroqProviderLogo from '@renderer/assets/images/providers/groq.png'
import MoonshotProviderLogo from '@renderer/assets/images/providers/moonshot.jpeg'
import MoonshotModelLogo from '@renderer/assets/images/providers/moonshot.jpeg'
import OllamaProviderLogo from '@renderer/assets/images/providers/ollama.png'
import OpenAiProviderLogo from '@renderer/assets/images/providers/openai.jpeg'
import OpenRouterProviderLogo from '@renderer/assets/images/providers/openrouter.png'
import SiliconFlowProviderLogo from '@renderer/assets/images/providers/silicon.png'
import YiProviderLogo from '@renderer/assets/images/providers/yi.svg'
import ZhipuProviderLogo from '@renderer/assets/images/providers/zhipu.png'
export function getProviderLogo(providerId: string) {
switch (providerId) {
case 'openai':
return OpenAiProviderLogo
case 'silicon':
return SiliconFlowProviderLogo
case 'deepseek':
return DeepSeekProviderLogo
case 'yi':
return YiProviderLogo
case 'groq':
return GroqProviderLogo
case 'zhipu':
return ZhipuProviderLogo
case 'ollama':
return OllamaProviderLogo
case 'moonshot':
return MoonshotProviderLogo
case 'openrouter':
return OpenRouterProviderLogo
case 'baichuan':
return BaichuanProviderLogo
case 'dashscope':
return DashScopeProviderLogo
case 'anthropic':
return AnthropicProviderLogo
case 'aihubmix':
return AiHubMixProviderLogo
default:
return undefined
}
}
export function getModelLogo(modelId: string) {
if (!modelId) {
return undefined
}
const logoMap = {
gpt: ChatGPTModelLogo,
glm: ChatGLMModelLogo,
deepseek: DeepSeekModelLogo,
qwen: QwenModelLogo,
gemma: GemmaModelLogo,
'yi-': YiModelLogo,
llama: LlamaModelLogo,
mixtral: MixtralModelLogo,
mistral: MixtralModelLogo,
moonshot: MoonshotModelLogo,
phi: MicrosoftModelLogo,
baichuan: BaichuanModelLogo,
claude: ClaudeModelLogo
}
for (const key in logoMap) {
if (modelId.toLowerCase().includes(key)) {
return logoMap[key]
}
}
return undefined
}
export const PROVIDER_CONFIG = {
openai: {
api: {
url: 'https://api.openai.com',
editable: true
},
websites: {
official: 'https://openai.com/',
apiKey: 'https://platform.openai.com/api-keys',
@@ -8,14 +101,22 @@ export const PROVIDER_CONFIG = {
}
},
silicon: {
api: {
url: 'https://cloud.siliconflow.cn',
editable: false
},
websites: {
official: 'https://www.siliconflow.cn/',
apiKey: 'https://cloud.siliconflow.cn/account/ak',
apiKey: 'https://cloud.siliconflow.cn/account/ak?referrer=clxty1xuy0014lvqwh5z50i88',
docs: 'https://docs.siliconflow.cn/',
models: 'https://docs.siliconflow.cn/docs/model-names'
}
},
deepseek: {
api: {
url: 'https://api.deepseek.com',
editable: false
},
websites: {
official: 'https://deepseek.com/',
apiKey: 'https://platform.deepseek.com/api_keys',
@@ -24,6 +125,10 @@ export const PROVIDER_CONFIG = {
}
},
yi: {
api: {
url: 'https://api.lingyiwanwu.com',
editable: false
},
websites: {
official: 'https://platform.lingyiwanwu.com/',
apiKey: 'https://platform.lingyiwanwu.com/apikeys',
@@ -32,6 +137,10 @@ export const PROVIDER_CONFIG = {
}
},
zhipu: {
api: {
url: 'https://open.bigmodel.cn/api/paas/v4/',
editable: false
},
websites: {
official: 'https://open.bigmodel.cn/',
apiKey: 'https://open.bigmodel.cn/usercenter/apikeys',
@@ -40,6 +149,10 @@ export const PROVIDER_CONFIG = {
}
},
moonshot: {
api: {
url: 'https://api.moonshot.cn',
editable: false
},
websites: {
official: 'https://moonshot.ai/',
apiKey: 'https://platform.moonshot.cn/console/api-keys',
@@ -47,7 +160,35 @@ export const PROVIDER_CONFIG = {
models: 'https://platform.moonshot.cn/docs/intro#%E6%A8%A1%E5%9E%8B%E5%88%97%E8%A1%A8'
}
},
baichuan: {
api: {
url: 'https://api.baichuan-ai.com',
editable: false
},
websites: {
official: 'https://www.baichuan-ai.com/',
apiKey: 'https://platform.baichuan-ai.com/console/apikey',
docs: 'https://platform.baichuan-ai.com/docs',
models: 'https://platform.baichuan-ai.com/price'
}
},
dashscope: {
api: {
url: 'https://dashscope.aliyuncs.com/compatible-mode/v1/',
editable: false
},
websites: {
official: 'https://dashscope.aliyun.com/',
apiKey: 'https://help.aliyun.com/zh/dashscope/developer-reference/acquisition-and-configuration-of-api-key',
docs: 'https://help.aliyun.com/zh/dashscope/',
models: 'https://dashscope.console.aliyun.com/model'
}
},
openrouter: {
api: {
url: 'https://openrouter.ai/api/v1/',
editable: false
},
websites: {
official: 'https://openrouter.ai/',
apiKey: 'https://openrouter.ai/settings/keys',
@@ -56,6 +197,10 @@ export const PROVIDER_CONFIG = {
}
},
groq: {
api: {
url: 'https://api.groq.com/openai',
editable: false
},
websites: {
official: 'https://groq.com/',
apiKey: 'https://console.groq.com/keys',
@@ -64,10 +209,38 @@ export const PROVIDER_CONFIG = {
}
},
ollama: {
api: {
url: 'http://localhost:11434/v1/',
editable: true
},
websites: {
official: 'https://ollama.com/',
docs: 'https://github.com/ollama/ollama/tree/main/docs',
models: 'https://ollama.com/library'
}
},
anthropic: {
api: {
url: 'https://api.anthropic.com/',
editable: false
},
websites: {
official: 'https://anthropic.com/',
apiKey: 'https://console.anthropic.com/settings/keys',
docs: 'https://docs.anthropic.com/en/docs',
models: 'https://docs.anthropic.com/en/docs/about-claude/models'
}
},
aihubmix: {
api: {
url: 'https://aihubmix.com',
editable: false
},
websites: {
official: 'https://aihubmix.com/',
apiKey: 'https://aihubmix.com/token',
docs: 'https://doc.aihubmix.com/',
models: 'https://aihubmix.com/models'
}
}
}

View File

@@ -1,13 +1,14 @@
/// <reference types="vite/client" />
import type KeyvStorage from '@kangfenmao/keyv-storage'
import { MessageInstance } from 'antd/es/message/interface'
import { HookAPI } from 'antd/es/modal/useModal'
import type KeyvStorage from '@kangfenmao/keyv-storage'
declare global {
interface Window {
message: MessageInstance
modal: HookAPI
keyv: KeyvStorage
mermaid: any
}
}

View File

@@ -0,0 +1,17 @@
import { RootState } from '@renderer/store'
import { addAgent, removeAgent, updateAgent, updateAgents } from '@renderer/store/agents'
import { Agent } from '@renderer/types'
import { useDispatch, useSelector } from 'react-redux'
export function useAgents() {
const agents = useSelector((state: RootState) => state.agents.agents)
const dispatch = useDispatch()
return {
agents,
addAgent: (agent: Agent) => dispatch(addAgent(agent)),
removeAgent: (agent: Agent) => dispatch(removeAgent(agent)),
updateAgent: (agent: Agent) => dispatch(updateAgent(agent)),
updateAgents: (agents: Agent[]) => dispatch(updateAgents(agents))
}
}

View File

@@ -0,0 +1,36 @@
import i18n from '@renderer/i18n'
import LocalStorage from '@renderer/services/storage'
import { useAppDispatch } from '@renderer/store'
import { setAvatar } from '@renderer/store/runtime'
import { runAsyncFunction } from '@renderer/utils'
import { useEffect } from 'react'
import { useSettings } from './useSettings'
export function useAppInit() {
const dispatch = useAppDispatch()
const { proxyUrl } = useSettings()
const { language } = useSettings()
useEffect(() => {
runAsyncFunction(async () => {
const storedImage = await LocalStorage.getImage('avatar')
storedImage && dispatch(setAvatar(storedImage))
})
}, [dispatch])
useEffect(() => {
runAsyncFunction(async () => {
const { isPackaged } = await window.api.getAppInfo()
isPackaged && setTimeout(window.api.checkForUpdate, 3000)
})
}, [])
useEffect(() => {
proxyUrl && window.api.setProxy(proxyUrl)
}, [proxyUrl])
useEffect(() => {
i18n.changeLanguage(language || navigator.language || 'en-US')
}, [language])
}

View File

@@ -1,18 +0,0 @@
import { i18nInit } from '@renderer/i18n'
import LocalStorage from '@renderer/services/storage'
import { useAppDispatch } from '@renderer/store'
import { setAvatar } from '@renderer/store/runtime'
import { runAsyncFunction } from '@renderer/utils'
import { useEffect } from 'react'
export function useAppInitEffect() {
const dispatch = useAppDispatch()
useEffect(() => {
runAsyncFunction(async () => {
const storedImage = await LocalStorage.getImage('avatar')
storedImage && dispatch(setAvatar(storedImage))
})
i18nInit()
}, [dispatch])
}

View File

@@ -1,20 +1,21 @@
import { getDefaultTopic } from '@renderer/services/assistant'
import { useAppDispatch, useAppSelector } from '@renderer/store'
import {
addTopic as _addTopic,
removeAllTopics as _removeAllTopics,
removeTopic as _removeTopic,
setModel as _setModel,
updateAssistants as _updateAssistants,
updateDefaultAssistant as _updateDefaultAssistant,
updateTopic as _updateTopic,
updateTopics as _updateTopics,
addAssistant,
addTopic,
removeAllTopics,
removeAssistant,
updateAssistant
removeTopic,
setModel,
updateAssistant,
updateAssistants,
updateAssistantSettings,
updateDefaultAssistant,
updateTopic,
updateTopics
} from '@renderer/store/assistants'
import { setDefaultModel as _setDefaultModel, setTopicNamingModel as _setTopicNamingModel } from '@renderer/store/llm'
import { Assistant, Model, Topic } from '@renderer/types'
import { setDefaultModel, setTopicNamingModel, setTranslateModel } from '@renderer/store/llm'
import { Assistant, AssistantSettings, Model, Topic } from '@renderer/types'
import localforage from 'localforage'
export function useAssistants() {
@@ -23,9 +24,8 @@ export function useAssistants() {
return {
assistants,
updateAssistants: (assistants: Assistant[]) => dispatch(_updateAssistants(assistants)),
updateAssistants: (assistants: Assistant[]) => dispatch(updateAssistants(assistants)),
addAssistant: (assistant: Assistant) => dispatch(addAssistant(assistant)),
updateAssistant: (assistant: Assistant) => dispatch(updateAssistant(assistant)),
removeAssistant: (id: string) => {
dispatch(removeAssistant({ id }))
const assistant = assistants.find((a) => a.id === id)
@@ -44,17 +44,21 @@ export function useAssistant(id: string) {
return {
assistant,
model: assistant?.model ?? defaultModel,
addTopic: (topic: Topic) => dispatch(_addTopic({ assistantId: assistant.id, topic })),
removeTopic: (topic: Topic) => dispatch(_removeTopic({ assistantId: assistant.id, topic })),
updateTopic: (topic: Topic) => dispatch(_updateTopic({ assistantId: assistant.id, topic })),
updateTopics: (topics: Topic[]) => dispatch(_updateTopics({ assistantId: assistant.id, topics })),
removeAllTopics: () => dispatch(_removeAllTopics({ assistantId: assistant.id })),
setModel: (model: Model) => dispatch(_setModel({ assistantId: assistant.id, model }))
addTopic: (topic: Topic) => dispatch(addTopic({ assistantId: assistant.id, topic })),
removeTopic: (topic: Topic) => dispatch(removeTopic({ assistantId: assistant.id, topic })),
updateTopic: (topic: Topic) => dispatch(updateTopic({ assistantId: assistant.id, topic })),
updateTopics: (topics: Topic[]) => dispatch(updateTopics({ assistantId: assistant.id, topics })),
removeAllTopics: () => dispatch(removeAllTopics({ assistantId: assistant.id })),
setModel: (model: Model) => dispatch(setModel({ assistantId: assistant.id, model })),
updateAssistant: (assistant: Assistant) => dispatch(updateAssistant(assistant)),
updateAssistantSettings: (settings: AssistantSettings) => {
dispatch(updateAssistantSettings({ assistantId: assistant.id, settings }))
}
}
}
export function useDefaultAssistant() {
const { defaultAssistant } = useAppSelector((state) => state.assistants)
const defaultAssistant = useAppSelector((state) => state.assistants.defaultAssistant)
const dispatch = useAppDispatch()
return {
@@ -62,18 +66,20 @@ export function useDefaultAssistant() {
...defaultAssistant,
topics: [getDefaultTopic()]
},
updateDefaultAssistant: (assistant: Assistant) => dispatch(_updateDefaultAssistant({ assistant }))
updateDefaultAssistant: (assistant: Assistant) => dispatch(updateDefaultAssistant({ assistant }))
}
}
export function useDefaultModel() {
const { defaultModel, topicNamingModel } = useAppSelector((state) => state.llm)
const { defaultModel, topicNamingModel, translateModel } = useAppSelector((state) => state.llm)
const dispatch = useAppDispatch()
return {
defaultModel,
topicNamingModel,
setDefaultModel: (model: Model) => dispatch(_setDefaultModel({ model })),
setTopicNamingModel: (model: Model) => dispatch(_setTopicNamingModel({ model }))
translateModel,
setDefaultModel: (model: Model) => dispatch(setDefaultModel({ model })),
setTopicNamingModel: (model: Model) => dispatch(setTopicNamingModel({ model })),
setTranslateModel: (model: Model) => dispatch(setTranslateModel({ model }))
}
}

View File

@@ -0,0 +1,18 @@
import store, { useAppSelector } from '@renderer/store'
import { setOllamaKeepAliveTime } from '@renderer/store/llm'
import { useDispatch } from 'react-redux'
export function useOllamaSettings() {
const settings = useAppSelector((state) => state.llm.settings.ollama)
const dispatch = useDispatch()
return { ...settings, setKeepAliveTime: (time: number) => dispatch(setOllamaKeepAliveTime(time)) }
}
export function getOllamaSettings() {
return store.getState().llm.settings.ollama
}
export function getOllamaKeepAliveTime() {
return store.getState().llm.settings.ollama.keepAliveTime + 'm'
}

View File

@@ -1,20 +1,32 @@
import { createSelector } from '@reduxjs/toolkit'
import { useAppDispatch, useAppSelector } from '@renderer/store'
import {
addModel as _addModel,
removeModel as _removeModel,
updateProvider as _updateProvider,
updateProviders as _updateProviders
addModel,
addProvider,
removeModel,
removeProvider,
updateProvider,
updateProviders
} from '@renderer/store/llm'
import { Assistant, Model, Provider } from '@renderer/types'
import { useDefaultModel } from './useAssistant'
const selectEnabledProviders = createSelector(
(state) => state.llm.providers,
(providers) => providers.filter((p) => p.enabled)
)
export function useProviders() {
const providers = useAppSelector((state) => state.llm.providers.filter((p) => p.enabled))
const providers: Provider[] = useAppSelector(selectEnabledProviders)
const dispatch = useAppDispatch()
return {
providers,
updateProviders: (providers: Provider[]) => dispatch(_updateProviders(providers))
addProvider: (provider: Provider) => dispatch(addProvider(provider)),
removeProvider: (provider: Provider) => dispatch(removeProvider(provider)),
updateProvider: (provider: Provider) => dispatch(updateProvider(provider)),
updateProviders: (providers: Provider[]) => dispatch(updateProviders(providers))
}
}
@@ -22,16 +34,24 @@ export function useSystemProviders() {
return useAppSelector((state) => state.llm.providers.filter((p) => p.isSystem))
}
export function useUserProviders() {
return useAppSelector((state) => state.llm.providers.filter((p) => !p.isSystem))
}
export function useAllProviders() {
return useAppSelector((state) => state.llm.providers)
}
export function useProvider(id: string) {
const provider = useAppSelector((state) => state.llm.providers.find((p) => p.id === id) as Provider)
const dispatch = useAppDispatch()
return {
provider,
models: provider.models,
updateProvider: (provider: Provider) => dispatch(_updateProvider(provider)),
addModel: (model: Model) => dispatch(_addModel({ providerId: id, model })),
removeModel: (model: Model) => dispatch(_removeModel({ providerId: id, model }))
models: provider?.models || [],
updateProvider: (provider: Provider) => dispatch(updateProvider(provider)),
addModel: (model: Model) => dispatch(addModel({ providerId: id, model })),
removeModel: (model: Model) => dispatch(removeModel({ providerId: id, model }))
}
}

View File

@@ -1,5 +1,10 @@
import { useAppDispatch, useAppSelector } from '@renderer/store'
import { setSendMessageShortcut as _setSendMessageShortcut, SendMessageShortcut } from '@renderer/store/settings'
import {
SendMessageShortcut,
setSendMessageShortcut as _setSendMessageShortcut,
setTheme,
ThemeMode
} from '@renderer/store/settings'
export function useSettings() {
const settings = useAppSelector((state) => state.settings)
@@ -9,6 +14,9 @@ export function useSettings() {
...settings,
setSendMessageShortcut(shortcut: SendMessageShortcut) {
dispatch(_setSendMessageShortcut(shortcut))
},
setTheme(theme: ThemeMode) {
dispatch(setTheme(theme))
}
}
}

View File

@@ -1,12 +1,28 @@
import { useAppDispatch, useAppSelector } from '@renderer/store'
import { toggleRightSidebar } from '@renderer/store/settings'
import { setShowRightSidebar, toggleRightSidebar, toggleShowAssistants } from '@renderer/store/settings'
export function useShowRightSidebar() {
const showRightSidebar = useAppSelector((state) => state.settings.showRightSidebar)
const dispatch = useAppDispatch()
return {
showRightSidebar,
setShowRightSidebar: () => dispatch(toggleRightSidebar())
rightSidebarShown: showRightSidebar,
toggleRightSidebar: () => dispatch(toggleRightSidebar()),
showRightSidebar: () => dispatch(setShowRightSidebar(true)),
hideRightSidebar: () => dispatch(setShowRightSidebar(false))
}
}
export function useShowAssistants() {
const showAssistants = useAppSelector((state) => state.settings.showAssistants)
const dispatch = useAppDispatch()
return {
showAssistants,
toggleShowAssistants: () => dispatch(toggleShowAssistants())
}
}
export function useRuntime() {
return useAppSelector((state) => state.runtime)
}

View File

@@ -1,9 +1,11 @@
import { Assistant } from '@renderer/types'
import { Assistant, Topic } from '@renderer/types'
import { find } from 'lodash'
import { useEffect, useState } from 'react'
let _activeTopic: Topic
export function useActiveTopic(assistant: Assistant) {
const [activeTopic, setActiveTopic] = useState(assistant?.topics[0])
const [activeTopic, setActiveTopic] = useState(_activeTopic || assistant?.topics[0])
useEffect(() => {
// activeTopic not in assistant.topics

View File

@@ -1,4 +1,3 @@
import store from '@renderer/store'
import i18n from 'i18next'
import { initReactI18next } from 'react-i18next'
@@ -23,10 +22,16 @@ const resources = {
duplicate: 'Duplicate',
copy: 'Copy',
regenerate: 'Regenerate',
provider: 'Provider'
provider: 'Provider',
you: 'You',
save: 'Save',
footnotes: 'References',
select: 'Select',
search: 'Search'
},
button: {
add: 'Add',
added: 'Added',
manage: 'Manage',
select_model: 'Select Model'
},
@@ -38,23 +43,24 @@ const resources = {
'error.enter.api.key': 'Please enter your API key first',
'error.enter.api.host': 'Please enter your API host first',
'error.enter.model': 'Please select a model first',
'error.invalid.proxy.url': 'Invalid proxy URL',
'api.connection.failed': 'Connection failed',
'api.connection.success': 'Connection successful',
'chat.completion.paused': 'Chat completion paused'
'chat.completion.paused': 'Chat completion paused',
'switch.disabled': 'Switching is disabled while the assistant is generating'
},
assistant: {
'default.name': 'Default Assistant',
chat: {
save: 'Save',
'default.name': '😀 Default Assistant',
'default.description': "Hello, I'm Default Assistant. You can start chatting with me right away",
'default.topic.name': 'Default Topic',
'topics.title': 'Topics',
'topics.hide_topics': 'Hide Topics',
'topics.show_topics': 'Show Topics',
'topics.auto_rename': 'Auto Rename',
'topics.edit.title': 'Rename',
'topics.edit.placeholder': 'Enter new name',
'topics.delete.all.title': 'Delete all topics',
'topics.delete.all.content': 'Are you sure you want to delete all topics?',
'input.new_chat': ' New Chat ',
'input.new_topic': 'New Topic',
'input.topics': ' Topics ',
'input.clear': 'Clear',
'input.expand': 'Expand',
@@ -63,10 +69,39 @@ const resources = {
'input.clear.content': 'Are you sure to clear all messages?',
'input.placeholder': 'Type your message here...',
'input.send': 'Send',
'input.pause': 'Pause'
'input.pause': 'Pause',
'input.settings': 'Settings',
'input.context_count.tip': 'Context Count',
'input.estimated_tokens.tip': 'Estimated tokens',
'settings.temperature': 'Temperature',
'settings.temperature.tip':
'Lower values make the model more creative and unpredictable, while higher values make it more deterministic and precise.',
'settings.conext_count': 'Context',
'settings.conext_count.tip': 'The number of previous messages to keep in the context.',
'settings.max_tokens': 'Enable Max Tokens Limit',
'settings.max_tokens.tip':
'The maximum number of tokens the model can generate. Normal chat suggests 500-800. Short text generation suggests 800-2000. Code generation suggests 2000-3600. Long text generation suggests above 4000.',
'settings.reset': 'Reset',
'settings.set_as_default': 'Apply to default assistant',
'settings.max': 'Max',
'suggestions.title': 'Suggested Questions',
'add.assistant.title': 'Add Assistant'
},
apps: {
title: 'Agents'
agents: {
title: 'Agents',
my_agents: 'My Agents',
'add.title': 'Add Agent',
'edit.title': 'Edit Agent',
'add.name': 'Name',
'add.name.placeholder': 'Enter name',
'add.prompt': 'Prompt',
'add.prompt.placeholder': 'Enter prompt',
'add.button': 'Add',
'manage.title': 'Manage Agents',
'delete.popup.content': 'Are you sure you want to delete this agent?',
'tag.default': 'Default',
'tag.system': 'System',
'tag.user': 'Mine'
},
provider: {
openai: 'OpenAI',
@@ -77,16 +112,28 @@ const resources = {
yi: 'Yi',
zhipu: 'ZHIPU AI',
groq: 'Groq',
ollama: 'Ollama'
ollama: 'Ollama',
baichuan: 'Baichuan',
dashscope: 'DashScope',
anthropic: 'Anthropic',
aihubmix: 'AiHubMix'
},
settings: {
title: 'Settings',
general: 'General',
general: 'General Settings',
provider: 'Model Provider',
model: 'Model Settings',
model: 'Default Model',
assistant: 'Default Assistant',
about: 'About',
about: 'About & Feedback',
'messages.model.title': 'Model Settings',
'messages.title': 'Message Settings',
'messages.divider': 'Show divider between messages',
'messages.use_serif_font': 'Use serif font',
'messages.input.title': 'Input Settings',
'messages.input.show_estimated_tokens': 'Show estimated input tokens',
'general.title': 'General Settings',
'general.user_name': 'User Name',
'general.user_name.placeholder': 'Enter your name',
'provider.api_key': 'API Key',
'provider.check': 'Check',
'provider.get_api_key': 'Get API Key',
@@ -94,10 +141,11 @@ const resources = {
'provider.docs_check': 'Check',
'provider.docs_more_details': 'for more details',
'provider.search_placeholder': 'Search model id or name',
'provider.api.url.reset': 'Reset',
'models.default_assistant_model': 'Default Assistant Model',
'models.topic_naming_model': 'Topic Naming Model',
'models.translate_model': 'Translate Model',
'models.add.add_model': 'Add Model',
'models.add.provider_name.placeholder': 'Provider Name',
'models.add.model_id.placeholder': 'Required e.g. gpt-3.5-turbo',
'models.add.model_id': 'Model ID',
'models.add.model_id.tooltip': 'Example: gpt-3.5-turbo',
@@ -108,7 +156,60 @@ const resources = {
'models.add.group_name.placeholder': 'Optional e.g. ChatGPT',
'models.empty': 'No models found',
'assistant.title': 'Default Assistant',
'about.description': 'A powerful AI assistant for producer'
'assistant.model_params': 'Model Parameters',
'about.description': 'A powerful AI assistant for producer',
'about.updateNotAvailable': 'You are using the latest version',
'about.checkingUpdate': 'Checking for updates...',
'about.updateError': 'Update error',
'about.checkUpdate': 'Check Update',
'about.downloading': 'Downloading...',
'provider.delete.title': 'Delete Provider',
'provider.delete.content': 'Are you sure you want to delete this provider?',
'provider.edit.name': 'Provider Name',
'provider.edit.name.placeholder': 'Example: OpenAI',
'about.title': 'About',
'about.releases.title': '📔 Release Notes',
'about.releases.button': 'Releases',
'about.website.title': '🌐 Official Website',
'about.website.button': 'Website',
'about.feedback.title': '📝 Feedback',
'about.feedback.button': 'Feedback',
'about.contact.title': '📧 Contact',
'about.license.title': '📄 License',
'about.license.button': 'License',
'about.contact.button': 'Email',
'proxy.title': 'Proxy Address',
'theme.title': 'Theme',
'theme.dark': 'Dark',
'theme.light': 'Light',
'theme.auto': 'Auto'
},
translate: {
title: 'Translation',
'any.language': 'Any language',
'button.translate': 'Translate',
'error.not_configured': 'Translation model is not configured',
'input.placeholder': 'Enter text to translate',
'output.placeholder': 'Translation'
},
languages: {
english: 'English',
chinese: 'Chinese',
'chinese-traditional': 'Traditional Chinese',
japanese: 'Japanese',
korean: 'Korean',
russian: 'Russian',
spanish: 'Spanish',
french: 'French',
italian: 'Italian',
portuguese: 'Portuguese',
arabic: 'Arabic'
},
ollama: {
title: 'Ollama',
'keep_alive_time.title': 'Keep Alive Time',
'keep_alive_time.placeholder': 'Minutes',
'keep_alive_time.description': 'The time in minutes to keep the connection alive, default is 5 minutes.'
}
}
},
@@ -132,38 +233,44 @@ const resources = {
duplicate: '复制',
copy: '复制',
regenerate: '重新生成',
provider: '提供商'
provider: '提供商',
you: '用户',
footnote: '引用内容',
select: '选择',
search: '搜索'
},
button: {
add: '添加',
added: '已添加',
manage: '管理',
select_model: '选择模型'
},
message: {
copied: '已复制!',
copied: '已复制',
'assistant.added.content': '智能体添加成功',
'message.delete.title': '删除消息',
'message.delete.content': '确定要删除此消息吗?',
'error.enter.api.key': '请输入您的 API 密钥',
'error.enter.api.host': '请输入您的 API 地址',
'error.enter.model': '请选择一个模型',
'error.invalid.proxy.url': '无效的代理地址',
'api.connection.failed': '连接失败',
'api.connection.successful': '连接成功',
'chat.completion.paused': '会话已停止'
'api.connection.success': '连接成功',
'chat.completion.paused': '会话已停止',
'switch.disabled': '模型回复完成后才能切换'
},
assistant: {
'default.name': '默认助手',
chat: {
save: '保存',
'default.name': '😃 默认助手 - Assistant',
'default.description': '你好,我是默认助手。你可以立刻开始跟我聊天。',
'default.topic.name': '默认话题',
'topics.title': '话题',
'topics.hide_topics': '隐藏话题',
'topics.show_topics': '显示话题',
'topics.auto_rename': 'AI 重命名',
'topics.edit.title': '重命名',
'topics.auto_rename': '生成话题',
'topics.edit.title': '编辑话题',
'topics.edit.placeholder': '输入新名称',
'topics.delete.all.title': '删除所有话题',
'topics.delete.all.content': '确定要删除所有话题吗?',
'input.new_chat': ' 新聊天 ',
'input.new_topic': '新话题',
'input.topics': ' 话题 ',
'input.clear': '清除',
'input.expand': '展开',
@@ -172,10 +279,40 @@ const resources = {
'input.clear.content': '确定要清除所有消息吗?',
'input.placeholder': '在这里输入消息...',
'input.send': '发送',
'input.pause': '暂停'
'input.pause': '暂停',
'input.settings': '设置',
'input.context_count.tip': '上下文数',
'input.estimated_tokens.tip': '预估 token 数',
'settings.temperature': '模型温度',
'settings.temperature.tip':
'模型生成文本的随机程度。值越大,回复内容越赋有多样性、创造性、随机性;设为 0 根据事实回答。日常聊天建议设置为 0.7',
'settings.conext_count': '上下文数',
'settings.conext_count.tip':
'要保留在上下文中的消息数量,数值越大,上下文越长,消耗的 token 越多。普通聊天建议 5-10',
'settings.max_tokens': '开启消息长度限制',
'settings.max_tokens.tip':
'单次交互所用的最大 Token 数, 会影响返回结果的长度。普通聊天建议 500-800短文生成建议 800-2000代码生成建议 2000-3600长文生成建议切换模型到 4000 左右',
'settings.reset': '重置',
'settings.set_as_default': '应用到默认助手',
'settings.max': '不限',
'suggestions.title': '建议的问题',
'add.assistant.title': '添加智能体'
},
apps: {
title: '智能体'
agents: {
title: '智能体',
my_agents: '我的智能体',
'add.title': '添加智能体',
'edit.title': '编辑智能体',
'add.name': '名称',
'add.name.placeholder': '输入名称',
'add.prompt': '提示词',
'add.prompt.placeholder': '输入提示词',
'add.button': '添加',
'manage.title': '管理智能体',
'delete.popup.content': '确定要删除此智能体吗?',
'tag.default': '默认',
'tag.system': '系统',
'tag.user': '我的'
},
provider: {
openai: 'OpenAI',
@@ -186,16 +323,28 @@ const resources = {
yi: '零一万物',
zhipu: '智谱AI',
groq: 'Groq',
ollama: 'Ollama'
ollama: 'Ollama',
baichuan: '百川',
dashscope: '阿里云灵积',
anthropic: 'Anthropic',
aihubmix: 'AiHubMix'
},
settings: {
title: '设置',
general: '常规',
general: '常规设置',
provider: '模型提供商',
model: '模型设置',
model: '默认模型',
assistant: '默认助手',
about: '关于',
about: '关于我们',
'messages.model.title': '模型设置',
'messages.title': '消息设置',
'messages.divider': '消息分割线',
'messages.use_serif_font': '使用衬线字体',
'messages.input.title': '输入设置',
'messages.input.show_estimated_tokens': '状态显示',
'general.title': '常规设置',
'general.user_name': '用户名',
'general.user_name.placeholder': '请输入用户名',
'provider.api_key': 'API 密钥',
'provider.check': '检查',
'provider.get_api_key': '点击这里获取密钥',
@@ -203,10 +352,11 @@ const resources = {
'provider.docs_check': '查看',
'provider.docs_more_details': '获取更多详情',
'provider.search_placeholder': '搜索模型 ID 或名称',
'provider.api.url.reset': '重置',
'models.default_assistant_model': '默认助手模型',
'models.topic_naming_model': '话题命名模型',
'models.translate_model': '翻译模型',
'models.add.add_model': '添加模型',
'models.add.provider_name.placeholder': '必填 例如 OpenAI',
'models.add.model_id.placeholder': '必填 例如 gpt-3.5-turbo',
'models.add.model_id': '模型 ID',
'models.add.model_id.tooltip': '例如 gpt-3.5-turbo',
@@ -217,7 +367,60 @@ const resources = {
'models.add.group_name.placeholder': '例如 ChatGPT',
'models.empty': '没有模型',
'assistant.title': '默认助手',
'about.description': '一个为创造者而生的 AI 助手'
'assistant.model_params': '模型参数',
'about.description': '一款为创造者而生的 AI 助手',
'about.updateNotAvailable': '你的软件已是最新版本',
'about.checkingUpdate': '正在检查更新...',
'about.updateError': '更新出错',
'about.checkUpdate': '检查更新',
'about.downloading': '正在下载更新...',
'provider.delete.title': '删除提供商',
'provider.delete.content': '确定要删除此模型提供商吗?',
'provider.edit.name': '模型提供商名称',
'provider.edit.name.placeholder': '例如 OpenAI',
'about.title': '关于我们',
'about.releases.title': '📔 更新日志',
'about.releases.button': '查看',
'about.website.title': '🌐 官方网站',
'about.website.button': '查看',
'about.feedback.title': '📝 意见反馈',
'about.feedback.button': '反馈',
'about.contact.title': '📧 邮件联系',
'about.license.title': '📄 许可证',
'about.license.button': '查看',
'about.contact.button': '邮件',
'proxy.title': '代理地址',
'theme.title': '主题',
'theme.dark': '深色主题',
'theme.light': '浅色主题',
'theme.auto': '跟随系统'
},
translate: {
title: '翻译',
'any.language': '任意语言',
'button.translate': '翻译',
'error.not_configured': '翻译模型未配置',
'input.placeholder': '输入文本进行翻译',
'output.placeholder': '翻译'
},
languages: {
english: '英文',
chinese: '简体中文',
'chinese-traditional': '繁体中文',
japanese: '日文',
korean: '韩文',
russian: '俄文',
spanish: '西班牙文',
french: '法文',
italian: '意大利文',
portuguese: '葡萄牙文',
arabic: '阿拉伯文'
},
ollama: {
title: 'Ollama',
'keep_alive_time.title': '保持活跃时间',
'keep_alive_time.placeholder': '分钟',
'keep_alive_time.description': '对话后模型在内存中保持的时间默认5分钟'
}
}
}
@@ -232,8 +435,4 @@ i18n.use(initReactI18next).init({
}
})
export function i18nInit() {
i18n.changeLanguage(store.getState().settings.language || 'en-US')
}
export default i18n

View File

@@ -1,5 +1,39 @@
import localforage from 'localforage'
import KeyvStorage from '@kangfenmao/keyv-storage'
import * as Sentry from '@sentry/electron/renderer'
import localforage from 'localforage'
import { ThemeMode } from './store/settings'
import { isProduction, loadScript } from './utils'
async function initSentry() {
if (await isProduction()) {
Sentry.init({
integrations: [Sentry.browserTracingIntegration(), Sentry.replayIntegration()],
// Set tracesSampleRate to 1.0 to capture 100%
// of transactions for performance monitoring.
// We recommend adjusting this value in production
tracesSampleRate: 1.0,
// Capture Replay for 10% of all sessions,
// plus for 100% of sessions with an error
replaysSessionSampleRate: 0.1,
replaysOnErrorSampleRate: 1.0
})
}
}
export async function initMermaid(theme: ThemeMode) {
if (!window.mermaid) {
await loadScript('https://unpkg.com/mermaid@10.9.1/dist/mermaid.min.js')
window.mermaid.initialize({
startOnLoad: true,
theme: theme === ThemeMode.dark ? 'dark' : 'default',
securityLevel: 'loose'
})
window.mermaid.contentLoaded()
}
}
function init() {
localforage.config({
@@ -9,8 +43,11 @@ function init() {
storeName: 'cherryai',
description: 'Cherry Studio Storage'
})
window.keyv = new KeyvStorage()
window.keyv.init()
initSentry()
}
init()

View File

@@ -1,10 +1,12 @@
import React from 'react'
import ReactDOM from 'react-dom/client'
import App from './App'
import './assets/styles/index.scss'
import './init'
import './i18n'
import React from 'react'
import ReactDOM from 'react-dom/client'
import App from './App'
ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render(
<React.StrictMode>
<App />

View File

@@ -0,0 +1,120 @@
import { UnorderedListOutlined } from '@ant-design/icons'
import { Navbar, NavbarCenter } from '@renderer/components/app/Navbar'
import { HStack } from '@renderer/components/Layout'
import Agents from '@renderer/config/agents.json'
import { useAgents } from '@renderer/hooks/useAgents'
import { useAssistants } from '@renderer/hooks/useAssistant'
import { covertAgentToAssistant } from '@renderer/services/assistant'
import { Agent } from '@renderer/types'
import { Col, Row, Typography } from 'antd'
import { find, groupBy } from 'lodash'
import { FC } from 'react'
import { useTranslation } from 'react-i18next'
import styled from 'styled-components'
import AgentCard from './components/AgentCard'
import ManageAgentsPopup from './components/ManageAgentsPopup'
import UserAgents from './components/UserAgents'
const { Title } = Typography
const AppsPage: FC = () => {
const { assistants, addAssistant } = useAssistants()
const { agents } = useAgents()
const agentGroups = groupBy(Agents, 'group')
const { t } = useTranslation()
const onAddAgentConfirm = (agent: Agent) => {
const added = find(assistants, { id: agent.id })
window.modal.confirm({
title: agent.emoji + ' ' + agent.name,
content: agent.description || agent.prompt,
icon: null,
closable: true,
maskClosable: true,
okButtonProps: { type: 'primary', disabled: Boolean(added) },
okText: added ? t('button.added') : t('button.add'),
onOk: () => onAddAgent(agent)
})
}
const onAddAgent = (agent: Agent) => {
addAssistant(covertAgentToAssistant(agent))
window.message.success({
content: t('message.assistant.added.content'),
key: 'agent-added',
style: { marginTop: '5vh' }
})
}
return (
<Container>
<Navbar>
<NavbarCenter style={{ borderRight: 'none' }}>{t('agents.title')}</NavbarCenter>
</Navbar>
<ContentContainer>
<AssistantsContainer>
<HStack alignItems="center" style={{ marginBottom: 16 }}>
<Title level={3}>{t('agents.my_agents')}</Title>
{agents.length > 0 && <ManageIcon onClick={ManageAgentsPopup.show} />}
</HStack>
<UserAgents onAdd={onAddAgentConfirm} />
{Object.keys(agentGroups).map((group) => (
<div key={group}>
<Title level={3} key={group} style={{ marginBottom: 16 }}>
{group}
</Title>
<Row gutter={16}>
{agentGroups[group].map((agent, index) => {
return (
<Col span={8} key={group + index}>
<AgentCard onClick={() => onAddAgentConfirm(agent)} agent={agent as any} />
</Col>
)
})}
</Row>
</div>
))}
<div style={{ minHeight: 20 }} />
</AssistantsContainer>
</ContentContainer>
</Container>
)
}
const Container = styled.div`
display: flex;
flex: 1;
flex-direction: column;
height: 100%;
`
const ContentContainer = styled.div`
display: flex;
flex: 1;
flex-direction: row;
justify-content: center;
height: 100%;
overflow-y: scroll;
background-color: var(--color-background);
`
const AssistantsContainer = styled.div`
display: flex;
flex: 1;
flex-direction: column;
height: calc(100vh - var(--navbar-height));
padding: 20px;
max-width: 1000px;
`
const ManageIcon = styled(UnorderedListOutlined)`
font-size: 18px;
color: var(--color-icon);
cursor: pointer;
margin-bottom: 0.5em;
margin-left: 0.5em;
`
export default AppsPage

View File

@@ -0,0 +1,137 @@
import 'emoji-picker-element'
import EmojiPicker from '@renderer/components/EmojiPicker'
import { TopView } from '@renderer/components/TopView'
import { useAgents } from '@renderer/hooks/useAgents'
import { syncAgentToAssistant } from '@renderer/services/assistant'
import { Agent } from '@renderer/types'
import { getLeadingEmoji, uuid } from '@renderer/utils'
import { Button, Form, FormInstance, Input, Modal, Popover } from 'antd'
import TextArea from 'antd/es/input/TextArea'
import { useEffect, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next'
interface Props {
agent?: Agent
resolve: (data: Agent | null) => void
}
type FieldType = {
id: string
name: string
prompt: string
}
const PopupContainer: React.FC<Props> = ({ agent, resolve }) => {
const [open, setOpen] = useState(true)
const [form] = Form.useForm()
const { t } = useTranslation()
const { addAgent, updateAgent } = useAgents()
const formRef = useRef<FormInstance>(null)
const [emoji, setEmoji] = useState(agent?.emoji)
const onFinish = (values: FieldType) => {
const _emoji = emoji || getLeadingEmoji(values.name)
if (values.name.trim() === '' || values.prompt.trim() === '') {
return
}
if (agent) {
const _agent = {
...agent,
name: values.name,
emoji: _emoji,
prompt: values.prompt
}
updateAgent(_agent)
syncAgentToAssistant(_agent)
resolve(_agent)
setOpen(false)
return
}
const _agent = {
id: uuid(),
name: values.name,
emoji: _emoji,
prompt: values.prompt,
group: 'user'
}
addAgent(_agent)
resolve(_agent)
setOpen(false)
}
const onCancel = () => {
setOpen(false)
}
const onClose = () => {
resolve(null)
}
useEffect(() => {
if (agent) {
form.setFieldsValue({
name: agent.name,
prompt: agent.prompt
})
}
}, [agent, form])
return (
<Modal
style={{ marginTop: '10vh' }}
title={agent ? t('agents.edit.title') : t('agents.add.title')}
open={open}
onOk={() => formRef.current?.submit()}
onCancel={onCancel}
maskClosable={false}
afterClose={onClose}
okText={agent ? t('common.save') : t('agents.add.button')}>
<Form
ref={formRef}
form={form}
labelCol={{ flex: '80px' }}
labelAlign="left"
colon={false}
style={{ marginTop: 25 }}
onFinish={onFinish}>
<Form.Item name="name" label="Emoji">
<Popover content={<EmojiPicker onEmojiClick={setEmoji} />} trigger="click" arrow>
<Button icon={emoji && <span style={{ fontSize: 20 }}>{emoji}</span>}>{t('common.select')}</Button>
</Popover>
</Form.Item>
<Form.Item name="name" label={t('agents.add.name')} rules={[{ required: true }]}>
<Input placeholder={t('agents.add.name.placeholder')} spellCheck={false} allowClear />
</Form.Item>
<Form.Item name="prompt" label={t('agents.add.prompt')} rules={[{ required: true }]}>
<TextArea placeholder={t('agents.add.prompt.placeholder')} spellCheck={false} rows={4} />
</Form.Item>
</Form>
</Modal>
)
}
export default class AddAgentPopup {
static topviewId = 0
static hide() {
TopView.hide('AddAgentPopup')
}
static show(agent?: Agent) {
return new Promise<Agent | null>((resolve) => {
TopView.show(
<PopupContainer
agent={agent}
resolve={(v) => {
resolve(v)
this.hide()
}}
/>,
'AddAgentPopup'
)
})
}
}

View File

@@ -0,0 +1,81 @@
import { Agent } from '@renderer/types'
import { Col, Typography } from 'antd'
import styled from 'styled-components'
interface Props {
agent: Agent
onClick?: () => void
}
const { Title } = Typography
const AgentCard: React.FC<Props> = ({ agent, onClick }) => {
return (
<Container onClick={onClick}>
{agent.emoji && <EmojiHeader>{agent.emoji}</EmojiHeader>}
<Col>
<AgentHeader>
<AgentName level={5} style={{ marginBottom: 0 }}>
{agent.name}
</AgentName>
</AgentHeader>
<AgentCardPrompt>{agent.prompt}</AgentCardPrompt>
</Col>
</Container>
)
}
const Container = styled.div`
display: flex;
flex-direction: row;
margin-bottom: 16px;
background-color: var(--color-background-soft);
border: 0.5px solid var(--color-border);
border-radius: 10px;
padding: 15px;
position: relative;
cursor: pointer;
transition: all 0.2s ease-in-out;
&:hover {
background-color: var(--color-background-mute);
}
`
const EmojiHeader = styled.div`
width: 25px;
display: flex;
flex-direction: row;
justify-content: center;
align-items: center;
margin-right: 5px;
font-size: 25px;
line-height: 25px;
`
const AgentHeader = styled.div`
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
`
const AgentName = styled(Title)`
font-size: 18px;
line-height: 1.2;
display: -webkit-box;
-webkit-line-clamp: 1;
-webkit-box-orient: vertical;
overflow: hidden;
color: var(--color-white);
font-weight: 900;
`
const AgentCardPrompt = styled.div`
color: #666;
margin-top: 6px;
display: -webkit-box;
-webkit-line-clamp: 1;
-webkit-box-orient: vertical;
overflow: hidden;
`
export default AgentCard

View File

@@ -0,0 +1,109 @@
import { DeleteOutlined, EditOutlined, MenuOutlined } from '@ant-design/icons'
import DragableList from '@renderer/components/DragableList'
import { Box, HStack } from '@renderer/components/Layout'
import { TopView } from '@renderer/components/TopView'
import { useAgents } from '@renderer/hooks/useAgents'
import { Empty, Modal, Popconfirm } from 'antd'
import { useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next'
import styled from 'styled-components'
import AddAgentPopup from './AddAgentPopup'
const PopupContainer: React.FC = () => {
const [open, setOpen] = useState(true)
const { t } = useTranslation()
const { agents, removeAgent, updateAgents } = useAgents()
const onOk = () => {
setOpen(false)
}
const onCancel = () => {
setOpen(false)
}
const onClose = async () => {
ManageAgentsPopup.hide()
}
useEffect(() => {
if (agents.length === 0) {
setOpen(false)
}
}, [agents])
return (
<Modal
style={{ marginTop: '10vh' }}
title={t('agents.manage.title')}
open={open}
onOk={onOk}
onCancel={onCancel}
afterClose={onClose}
footer={null}>
<Container>
{agents.length > 0 && (
<DragableList list={agents} onUpdate={updateAgents}>
{(item) => (
<AgentItem>
<Box mr={8}>
{item.emoji} {item.name}
</Box>
<HStack gap="15px">
<Popconfirm
title={t('agents.delete.popup.content')}
okButtonProps={{ danger: true }}
onConfirm={() => removeAgent(item)}>
<DeleteOutlined style={{ color: 'var(--color-error)' }} />
</Popconfirm>
<EditOutlined style={{ cursor: 'pointer' }} onClick={() => AddAgentPopup.show(item)} />
<MenuOutlined style={{ cursor: 'move' }} />
</HStack>
</AgentItem>
)}
</DragableList>
)}
{agents.length === 0 && <Empty description="" />}
</Container>
</Modal>
)
}
const Container = styled.div`
padding: 12px 0;
height: 50vh;
overflow-y: auto;
&::-webkit-scrollbar {
display: none;
}
`
const AgentItem = styled.div`
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-between;
padding: 8px;
border-radius: 8px;
user-select: none;
background-color: var(--color-background-soft);
margin-bottom: 8px;
.anticon {
font-size: 16px;
color: var(--color-icon);
}
&:hover {
background-color: var(--color-background-mute);
}
`
export default class ManageAgentsPopup {
static topviewId = 0
static hide() {
TopView.hide('ManageAgentsPopup')
}
static show() {
TopView.show(<PopupContainer />, 'ManageAgentsPopup')
}
}

View File

@@ -0,0 +1,57 @@
import { PlusOutlined } from '@ant-design/icons'
import { useAgents } from '@renderer/hooks/useAgents'
import { Agent } from '@renderer/types'
import { Col, Row } from 'antd'
import { FC } from 'react'
import styled from 'styled-components'
import AddAssistantPopup from './AddAgentPopup'
import AgentCard from './AgentCard'
interface Props {
onAdd: (agent: Agent) => void
}
const UserAgents: FC<Props> = ({ onAdd }) => {
const { agents } = useAgents()
const onAddMyAgentClick = () => {
AddAssistantPopup.show()
}
return (
<Row gutter={16} style={{ marginBottom: 16 }}>
{agents.map((agent) => (
<Col span={8} key={agent.id}>
<AgentCard agent={agent} onClick={() => onAdd(agent)} />
</Col>
))}
<Col span={8}>
<AssistantCardContainer style={{ borderStyle: 'dashed' }} onClick={onAddMyAgentClick}>
<PlusOutlined />
</AssistantCardContainer>
</Col>
</Row>
)
}
const AssistantCardContainer = styled.div`
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 20px;
border: 1px dashed var(--color-border);
border-radius: 10px;
cursor: pointer;
min-height: 84px;
.anticon {
font-size: 16px;
color: var(--color-icon);
}
&:hover {
background-color: var(--color-background-soft);
}
`
export default UserAgents

View File

@@ -1,126 +0,0 @@
import { Navbar, NavbarCenter } from '@renderer/components/app/Navbar'
import { SYSTEM_ASSISTANTS } from '@renderer/config/assistant'
import { Button, Col, Row, Tooltip, Typography } from 'antd'
import { find, groupBy } from 'lodash'
import { FC } from 'react'
import styled from 'styled-components'
import { CheckOutlined, PlusOutlined } from '@ant-design/icons'
import { SystemAssistant } from '@renderer/types'
import { getDefaultAssistant } from '@renderer/services/assistant'
import { useAssistants } from '@renderer/hooks/useAssistant'
import { colorPrimary } from '@renderer/config/antd'
import { useTranslation } from 'react-i18next'
const { Title } = Typography
const AppsPage: FC = () => {
const { assistants, addAssistant } = useAssistants()
const assistantGroups = groupBy(SYSTEM_ASSISTANTS, 'group')
const { t } = useTranslation()
const onAddAssistant = (assistant: SystemAssistant) => {
addAssistant({
...getDefaultAssistant(),
...assistant
})
window.message.success({
content: t('message.assistant.added.content'),
key: 'assistant-added',
style: { marginTop: '5vh' }
})
}
return (
<Container>
<Navbar>
<NavbarCenter style={{ borderRight: 'none' }}>{t('apps.title')}</NavbarCenter>
</Navbar>
<ContentContainer>
{Object.keys(assistantGroups).map((group) => (
<div key={group}>
<Title level={3} key={group} style={{ marginBottom: 16 }}>
{group}
</Title>
<Row gutter={16}>
{assistantGroups[group].map((assistant, index) => {
const added = find(assistants, { id: assistant.id })
return (
<Col span={6} key={group + index} style={{ marginBottom: 16 }}>
<AssistantCard>
<AssistantHeader>
<Title level={5} style={{ marginBottom: 0, color: colorPrimary }}>
{assistant.name}
</Title>
{added && (
<Button
type="primary"
shape="circle"
size="small"
ghost
icon={<CheckOutlined style={{ fontSize: 12 }} />}
/>
)}
{!added && (
<Tooltip placement="top" title=" Add to assistant list " arrow>
<Button
type="default"
shape="circle"
size="small"
style={{ padding: 0 }}
icon={<PlusOutlined style={{ fontSize: 12 }} />}
onClick={() => onAddAssistant(assistant)}
/>
</Tooltip>
)}
</AssistantHeader>
<AssistantCardPrompt>{assistant.prompt}</AssistantCardPrompt>
</AssistantCard>
</Col>
)
})}
</Row>
</div>
))}
</ContentContainer>
</Container>
)
}
const Container = styled.div`
display: flex;
flex: 1;
flex-direction: column;
height: 100%;
`
const ContentContainer = styled.div`
display: flex;
flex: 1;
flex-direction: column;
height: calc(100vh - var(--navbar-height));
padding: 20px;
overflow-y: scroll;
`
const AssistantCard = styled.div`
margin-bottom: 16px;
background-color: #141414;
border-radius: 10px;
padding: 20px;
`
const AssistantHeader = styled.div`
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
`
const AssistantCardPrompt = styled.div`
color: white;
margin-top: 10px;
margin-bottom: 10px;
line-height: 1.5;
`
export default AppsPage

View File

@@ -1,54 +1,76 @@
import { Navbar, NavbarLeft, NavbarRight } from '@renderer/components/app/Navbar'
import { isMac, isWindows } from '@renderer/config/constant'
import { useAssistants, useDefaultAssistant } from '@renderer/hooks/useAssistant'
import { useShowAssistants, useShowRightSidebar } from '@renderer/hooks/useStore'
import { useTheme } from '@renderer/providers/ThemeProvider'
import { Assistant } from '@renderer/types'
import { uuid } from '@renderer/utils'
import { Switch } from 'antd'
import { FC, useState } from 'react'
import styled from 'styled-components'
import Chat from './components/Chat'
import AddAssistantPopup from './components/AddAssistantPopup'
import Assistants from './components/Assistants'
import { uuid } from '@renderer/utils'
import { useShowRightSidebar } from '@renderer/hooks/useStore'
import { Tooltip } from 'antd'
import Navigation from './components/Navigation'
import { useTranslation } from 'react-i18next'
import Chat from './components/Chat'
import Navigation from './components/NavigationCenter'
let _activeAssistant: Assistant
const HomePage: FC = () => {
const { assistants, addAssistant } = useAssistants()
const [activeAssistant, setActiveAssistant] = useState(assistants[0])
const { showRightSidebar, setShowRightSidebar } = useShowRightSidebar()
const [activeAssistant, setActiveAssistant] = useState(_activeAssistant || assistants[0])
const { rightSidebarShown, toggleRightSidebar } = useShowRightSidebar()
const { showAssistants, toggleShowAssistants } = useShowAssistants()
const { defaultAssistant } = useDefaultAssistant()
const { t } = useTranslation()
const { theme, toggleTheme } = useTheme()
const onCreateAssistant = () => {
_activeAssistant = activeAssistant
const onCreateDefaultAssistant = () => {
const assistant = { ...defaultAssistant, id: uuid() }
addAssistant(assistant)
setActiveAssistant(assistant)
}
const onCreateAssistant = async () => {
const assistant = await AddAssistantPopup.show()
assistant && setActiveAssistant(assistant)
}
return (
<Container>
<Navbar>
<NavbarLeft style={{ justifyContent: 'flex-end', borderRight: 'none' }}>
{showAssistants && (
<NavbarLeft style={{ justifyContent: 'space-between', borderRight: 'none', padding: '0 8px' }}>
<NewButton onClick={toggleShowAssistants} style={{ marginLeft: isMac ? 8 : 0 }}>
<i className="iconfont icon-hidesidebarhoriz" />
</NewButton>
<NewButton onClick={onCreateAssistant}>
<i className="iconfont icon-a-addchat"></i>
</NewButton>
</NavbarLeft>
)}
<Navigation activeAssistant={activeAssistant} />
<NavbarRight style={{ justifyContent: 'flex-end', padding: 5 }}>
<Tooltip
placement="left"
title={showRightSidebar ? t('assistant.topics.hide_topics') : t('assistant.topics.show_topics')}
arrow>
<NewButton onClick={setShowRightSidebar}>
<i className={`iconfont ${showRightSidebar ? 'icon-showsidebarhoriz' : 'icon-hidesidebarhoriz'}`} />
<NavbarRight style={{ justifyContent: 'flex-end', paddingRight: isWindows ? 140 : 8 }}>
<ThemeSwitch
checkedChildren={<i className="iconfont icon-theme icon-dark1" />}
unCheckedChildren={<i className="iconfont icon-theme icon-theme-light" />}
checked={theme === 'dark'}
onChange={toggleTheme}
/>
<NewButton onClick={toggleRightSidebar}>
<i className={`iconfont ${rightSidebarShown ? 'icon-showsidebarhoriz' : 'icon-hidesidebarhoriz'}`} />
</NewButton>
</Tooltip>
</NavbarRight>
</Navbar>
<ContentContainer>
{showAssistants && (
<Assistants
activeAssistant={activeAssistant}
setActiveAssistant={setActiveAssistant}
onCreateAssistant={onCreateAssistant}
onCreateAssistant={onCreateDefaultAssistant}
/>
)}
<Chat assistant={activeAssistant} />
</ContentContainer>
</Container>
@@ -59,33 +81,35 @@ const Container = styled.div`
display: flex;
flex: 1;
flex-direction: column;
height: calc(100vh - var(--navbar-height));
`
const ContentContainer = styled.div`
display: flex;
flex: 1;
flex-direction: row;
height: 100%;
background-color: var(--color-background);
`
const NewButton = styled.div`
export const NewButton = styled.div`
-webkit-app-region: none;
border-radius: 4px;
width: 34px;
height: 34px;
width: 30px;
height: 30px;
display: flex;
flex-direction: row;
justify-content: center;
align-items: center;
transition: all 0.2s ease-in-out;
color: var(--color-icon);
.iconfont {
font-size: 22px;
.icon-a-addchat {
font-size: 20px;
}
.anticon {
font-size: 19px;
}
.icon-showsidebarhoriz,
.icon-hidesidebarhoriz {
font-size: 18px;
font-size: 17px;
}
&:hover {
background-color: var(--color-background-soft);
@@ -94,4 +118,12 @@ const NewButton = styled.div`
}
`
const ThemeSwitch = styled(Switch)`
-webkit-app-region: none;
margin-right: 8px;
.icon-theme {
font-size: 14px;
}
`
export default HomePage

View File

@@ -0,0 +1,129 @@
import { TopView } from '@renderer/components/TopView'
import systemAgents from '@renderer/config/agents.json'
import { useAgents } from '@renderer/hooks/useAgents'
import { useAssistants, useDefaultAssistant } from '@renderer/hooks/useAssistant'
import { covertAgentToAssistant } from '@renderer/services/assistant'
import { Agent, Assistant } from '@renderer/types'
import { Input, Modal, Tag } from 'antd'
import { useMemo, useState } from 'react'
import { useTranslation } from 'react-i18next'
import styled from 'styled-components'
interface Props {
resolve: (value: Assistant | undefined) => void
}
const PopupContainer: React.FC<Props> = ({ resolve }) => {
const [open, setOpen] = useState(true)
const { t } = useTranslation()
const { agents: userAgents } = useAgents()
const [searchText, setSearchText] = useState('')
const { defaultAssistant } = useDefaultAssistant()
const { assistants, addAssistant } = useAssistants()
const defaultAgent: Agent = useMemo(
() => ({
id: defaultAssistant.id,
name: defaultAssistant.name,
emoji: '',
prompt: defaultAssistant.prompt,
group: 'system'
}),
[defaultAssistant.id, defaultAssistant.name, defaultAssistant.prompt]
)
const agents = useMemo(() => {
const allAgents = [defaultAgent, ...userAgents, ...systemAgents] as Agent[]
const list = allAgents.filter((agent) => !assistants.map((a) => a.id).includes(agent.id))
return searchText
? list.filter((agent) => agent.name.toLowerCase().includes(searchText.trim().toLocaleLowerCase()))
: list
}, [assistants, defaultAgent, searchText, userAgents])
const onCreateAssistant = (agent: Agent) => {
if (assistants.map((a) => a.id).includes(String(agent.id))) return
const assistant = covertAgentToAssistant(agent)
addAssistant(assistant)
resolve(assistant)
setOpen(false)
}
const onCancel = () => {
setOpen(false)
}
const onClose = async () => {
resolve(undefined)
AddAssistantPopup.hide()
}
return (
<Modal
style={{ marginTop: '5vh' }}
title={t('chat.add.assistant.title')}
open={open}
onCancel={onCancel}
afterClose={onClose}
transitionName=""
maskTransitionName=""
footer={null}>
<Input
placeholder={t('common.search')}
value={searchText}
onChange={(e) => setSearchText(e.target.value)}
allowClear
autoFocus
style={{ marginBottom: 16 }}
/>
<Container>
{agents.map((agent) => (
<AgentItem key={agent.id} onClick={() => onCreateAssistant(agent)}>
{agent.emoji} {agent.name}
{agent.group === 'system' && <Tag color="orange">{t('agents.tag.system')}</Tag>}
{agent.group === 'user' && <Tag color="green">{t('agents.tag.user')}</Tag>}
</AgentItem>
))}
</Container>
</Modal>
)
}
const Container = styled.div`
height: 50vh;
overflow-y: auto;
&::-webkit-scrollbar {
display: none;
}
`
const AgentItem = styled.div`
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-between;
padding: 8px;
border-radius: 8px;
user-select: none;
background-color: var(--color-background-soft);
margin-bottom: 8px;
cursor: pointer;
.anticon {
font-size: 16px;
color: var(--color-icon);
}
&:hover {
background-color: var(--color-background-mute);
}
`
export default class AddAssistantPopup {
static topviewId = 0
static hide() {
TopView.hide('AddAssistantPopup')
}
static show() {
return new Promise<Assistant | undefined>((resolve) => {
TopView.show(<PopupContainer resolve={resolve} />, 'AddAssistantPopup')
})
}
}

View File

@@ -1,13 +1,16 @@
import { CopyOutlined, DeleteOutlined, EditOutlined } from '@ant-design/icons'
import { DragDropContext, Draggable, Droppable, DropResult } from '@hello-pangea/dnd'
import AssistantSettingPopup from '@renderer/components/Popups/AssistantSettingPopup'
import { useAssistants } from '@renderer/hooks/useAssistant'
import { getDefaultTopic } from '@renderer/services/assistant'
import { useAssistant, useAssistants } from '@renderer/hooks/useAssistant'
import { getDefaultTopic, syncAsistantToAgent } from '@renderer/services/assistant'
import { EVENT_NAMES, EventEmitter } from '@renderer/services/event'
import { useAppSelector } from '@renderer/store'
import { Assistant } from '@renderer/types'
import { droppableReorder, uuid } from '@renderer/utils'
import { Dropdown, MenuProps } from 'antd'
import { Dropdown } from 'antd'
import { ItemType } from 'antd/es/menu/interface'
import { last } from 'lodash'
import { FC, useRef } from 'react'
import { FC, useCallback } from 'react'
import { useTranslation } from 'react-i18next'
import styled from 'styled-components'
@@ -18,39 +21,41 @@ interface Props {
}
const Assistants: FC<Props> = ({ activeAssistant, setActiveAssistant, onCreateAssistant }) => {
const { assistants, removeAssistant, updateAssistant, addAssistant, updateAssistants } = useAssistants()
const targetAssistant = useRef<Assistant | null>(null)
const { assistants, removeAssistant, addAssistant, updateAssistants } = useAssistants()
const generating = useAppSelector((state) => state.runtime.generating)
const { updateAssistant } = useAssistant(activeAssistant.id)
const { t } = useTranslation()
const onDelete = (assistant: Assistant) => {
removeAssistant(assistant.id)
setTimeout(() => {
const onDelete = useCallback(
(assistant: Assistant) => {
const _assistant = last(assistants.filter((a) => a.id !== assistant.id))
_assistant ? setActiveAssistant(_assistant) : onCreateAssistant()
}, 0)
}
removeAssistant(assistant.id)
},
[assistants, onCreateAssistant, removeAssistant, setActiveAssistant]
)
const items: MenuProps['items'] = [
const getMenuItems = useCallback(
(assistant: Assistant) =>
[
{
label: t('common.edit'),
key: 'edit',
icon: <EditOutlined />,
async onClick() {
if (targetAssistant.current) {
const _assistant = await AssistantSettingPopup.show({ assistant: targetAssistant.current })
const _assistant = await AssistantSettingPopup.show({ assistant })
updateAssistant(_assistant)
}
syncAsistantToAgent(_assistant)
}
},
{
label: t('common.duplicate'),
key: 'duplicate',
icon: <CopyOutlined />,
async onClick() {
const assistant: Assistant = { ...activeAssistant, id: uuid(), topics: [getDefaultTopic()] }
addAssistant(assistant)
setActiveAssistant(assistant)
onClick: async () => {
const _assistant: Assistant = { ...assistant, id: uuid(), topics: [getDefaultTopic()] }
addAssistant(_assistant)
setActiveAssistant(_assistant)
}
},
{ type: 'divider' },
@@ -59,20 +64,37 @@ const Assistants: FC<Props> = ({ activeAssistant, setActiveAssistant, onCreateAs
key: 'delete',
icon: <DeleteOutlined />,
danger: true,
onClick: () => {
targetAssistant.current && onDelete(targetAssistant.current)
onClick: () => onDelete(assistant)
}
}
]
] as ItemType[],
[addAssistant, onDelete, setActiveAssistant, t, updateAssistant]
)
const onDragEnd = (result: DropResult) => {
const onDragEnd = useCallback(
(result: DropResult) => {
if (result.destination) {
const sourceIndex = result.source.index
const destIndex = result.destination.index
const reorderAssistants = droppableReorder<Assistant>(assistants, sourceIndex, destIndex)
updateAssistants(reorderAssistants)
}
},
[assistants, updateAssistants]
)
const onSwitchAssistant = useCallback(
(assistant: Assistant): any => {
if (generating) {
return window.message.warning({
content: t('message.switch.disabled'),
key: 'switch-assistant'
})
}
EventEmitter.emit(EVENT_NAMES.SWITCH_TOPIC_SIDEBAR)
setActiveAssistant(assistant)
},
[generating, setActiveAssistant, t]
)
return (
<Container>
@@ -83,16 +105,16 @@ const Assistants: FC<Props> = ({ activeAssistant, setActiveAssistant, onCreateAs
{assistants.map((assistant, index) => (
<Draggable key={`draggable_${assistant.id}_${index}`} draggableId={assistant.id} index={index}>
{(provided) => (
<div ref={provided.innerRef} {...provided.draggableProps} {...provided.dragHandleProps}>
<Dropdown
key={assistant.id}
menu={{ items }}
trigger={['contextMenu']}
onOpenChange={() => (targetAssistant.current = assistant)}>
<div
ref={provided.innerRef}
{...provided.draggableProps}
{...provided.dragHandleProps}
style={{ ...provided.draggableProps.style, marginBottom: 5 }}>
<Dropdown key={assistant.id} menu={{ items: getMenuItems(assistant) }} trigger={['contextMenu']}>
<AssistantItem
onClick={() => setActiveAssistant(assistant)}
onClick={() => onSwitchAssistant(assistant)}
className={assistant.id === activeAssistant?.id ? 'active' : ''}>
<AssistantName>{assistant.name}</AssistantName>
<AssistantName className="name">{assistant.name || t('chat.default.name')}</AssistantName>
</AssistantItem>
</Dropdown>
</div>
@@ -123,9 +145,9 @@ const AssistantItem = styled.div`
flex-direction: column;
padding: 7px 10px;
position: relative;
border-radius: 5px;
margin-bottom: 5px;
border-radius: 8px;
cursor: pointer;
font-family: Ubuntu;
.anticon {
display: none;
}
@@ -139,13 +161,19 @@ const AssistantItem = styled.div`
&.active {
background-color: var(--color-background-mute);
cursor: pointer;
.name {
font-weight: 500;
}
}
`
const AssistantName = styled.div`
font-size: 14px;
color: var(--color-text-1);
font-weight: bold;
color: var(--color-text);
display: -webkit-box;
-webkit-line-clamp: 1;
-webkit-box-orient: vertical;
overflow: hidden;
`
export default Assistants

View File

@@ -1,12 +1,13 @@
import { Assistant } from '@renderer/types'
import { FC } from 'react'
import styled from 'styled-components'
import Inputbar from './Inputbar'
import Messages from './Messages'
import { Flex } from 'antd'
import Topics from './Topics'
import { useAssistant } from '@renderer/hooks/useAssistant'
import { useActiveTopic } from '@renderer/hooks/useTopic'
import { Assistant } from '@renderer/types'
import { Flex } from 'antd'
import { FC } from 'react'
import styled from 'styled-components'
import Inputbar from './input/Inputbar'
import Messages from './Messages'
import RightSidebar from './sidebar/RightSidebar'
interface Props {
assistant: Assistant
@@ -16,17 +17,13 @@ const Chat: FC<Props> = (props) => {
const { assistant } = useAssistant(props.assistant.id)
const { activeTopic, setActiveTopic } = useActiveTopic(assistant)
if (!assistant) {
return null
}
return (
<Container id="chat">
<Flex vertical flex={1} justify="space-between">
<Main vertical flex={1} justify="space-between">
<Messages assistant={assistant} topic={activeTopic} />
<Inputbar assistant={assistant} setActiveTopic={setActiveTopic} />
</Flex>
<Topics assistant={assistant} activeTopic={activeTopic} setActiveTopic={setActiveTopic} />
</Main>
<RightSidebar assistant={assistant} activeTopic={activeTopic} setActiveTopic={setActiveTopic} />
</Container>
)
}
@@ -39,4 +36,8 @@ const Container = styled.div`
justify-content: space-between;
`
const Main = styled(Flex)`
height: calc(100vh - var(--navbar-height));
`
export default Chat

View File

@@ -1,18 +1,31 @@
import { Message } from '@renderer/types'
import { Avatar, Tooltip } from 'antd'
import { FC } from 'react'
import styled from 'styled-components'
import {
CheckOutlined,
CopyOutlined,
DeleteOutlined,
EditOutlined,
MenuOutlined,
QuestionCircleOutlined,
SaveOutlined,
SyncOutlined
} from '@ant-design/icons'
import { FONT_FAMILY } from '@renderer/config/constant'
import { getModelLogo } from '@renderer/config/provider'
import { useAssistant } from '@renderer/hooks/useAssistant'
import useAvatar from '@renderer/hooks/useAvatar'
import { CopyOutlined, DeleteOutlined, EditOutlined } from '@ant-design/icons'
import Markdown from 'react-markdown'
import CodeBlock from './CodeBlock'
import { useSettings } from '@renderer/hooks/useSettings'
import { useRuntime } from '@renderer/hooks/useStore'
import { EVENT_NAMES, EventEmitter } from '@renderer/services/event'
import { getModelLogo } from '@renderer/services/provider'
import Logo from '@renderer/assets/images/logo.png'
import { SyncOutlined } from '@ant-design/icons'
import { firstLetter } from '@renderer/utils'
import { Message, Model } from '@renderer/types'
import { firstLetter, removeLeadingEmoji } from '@renderer/utils'
import { Avatar, Dropdown, Popconfirm, Tooltip } from 'antd'
import dayjs from 'dayjs'
import { upperFirst } from 'lodash'
import { FC, memo, useCallback, useMemo, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { isEmpty } from 'lodash'
import styled from 'styled-components'
import Markdown from './markdown/Markdown'
import SelectModelDropdown from './SelectModelDropdown'
interface Props {
message: Message
@@ -25,90 +38,133 @@ interface Props {
const MessageItem: FC<Props> = ({ message, index, showMenu, onDeleteMessage }) => {
const avatar = useAvatar()
const { t } = useTranslation()
const { assistant, model, setModel } = useAssistant(message.assistantId)
const { userName, showMessageDivider, messageFont } = useSettings()
const { generating } = useRuntime()
const [copied, setCopied] = useState(false)
const isLastMessage = index === 0
const canRegenerate = isLastMessage && message.role === 'assistant'
const isUserMessage = message.role === 'user'
const isAssistantMessage = message.role === 'assistant'
const canRegenerate = isLastMessage && isAssistantMessage
const onCopy = () => {
const onCopy = useCallback(() => {
navigator.clipboard.writeText(message.content)
window.message.success({ content: t('message.copied'), key: 'copy-message' })
}
setCopied(true)
setTimeout(() => setCopied(false), 2000)
}, [message.content, t])
const onDelete = async () => {
const confirmed = await window.modal.confirm({
icon: null,
title: t('message.message.delete.title'),
content: t('message.message.delete.content'),
okText: t('common.delete'),
okType: 'danger'
})
confirmed && onDeleteMessage?.(message)
}
const onEdit = useCallback(() => EventEmitter.emit(EVENT_NAMES.EDIT_MESSAGE, message), [message])
const onEdit = () => {
EventEmitter.emit(EVENT_NAMES.EDIT_MESSAGE, message)
}
const onRegenerate = useCallback(
(model: Model) => {
setModel(model)
setTimeout(() => EventEmitter.emit(EVENT_NAMES.REGENERATE_MESSAGE, model), 100)
},
[setModel]
)
const onRegenerate = () => {
onDeleteMessage?.(message)
setTimeout(() => EventEmitter.emit(EVENT_NAMES.REGENERATE_MESSAGE), 100)
}
const getUserName = useCallback(() => {
if (message.id === 'assistant') return assistant?.name
if (message.role === 'assistant') return upperFirst(message.modelId)
return userName || t('common.you')
}, [assistant?.name, message.id, message.modelId, message.role, t, userName])
const getMessageContent = (message: Message) => {
if (isEmpty(message.content) && message.status === 'paused') {
return t('message.chat.completion.paused')
const fontFamily = useMemo(() => {
return messageFont === 'serif' ? FONT_FAMILY.replace('sans-serif', 'serif').replace('Ubuntu, ', '') : FONT_FAMILY
}, [messageFont])
const messageBorder = showMessageDivider ? undefined : 'none'
const avatarSource = useMemo(() => (message.modelId ? getModelLogo(message.modelId) : undefined), [message.modelId])
const avatarName = useMemo(() => firstLetter(assistant?.name).toUpperCase(), [assistant?.name])
const username = useMemo(() => removeLeadingEmoji(getUserName()), [getUserName])
const dropdownItems = useMemo(
() => [
{
label: t('chat.save'),
key: 'save',
icon: <SaveOutlined />,
onClick: () => {
const fileName = message.createdAt + '.md'
window.api.saveFile(fileName, message.content)
}
return message.content
}
],
[t, message]
)
return (
<MessageContainer key={message.id}>
<MessageContainer key={message.id} className="message" style={{ border: messageBorder }}>
<MessageHeader>
<AvatarWrapper>
{message.role === 'assistant' ? (
<Avatar src={message.modelId ? getModelLogo(message.modelId) : Logo}>
{firstLetter(message.modelId).toUpperCase()}
{isAssistantMessage ? (
<Avatar src={avatarSource} size={35}>
{avatarName}
</Avatar>
) : (
<Avatar src={avatar} />
<Avatar src={avatar} size={35} />
)}
<UserWrap>
<UserName>{username}</UserName>
<MessageTime>{dayjs(message.createdAt).format('MM/DD HH:mm')}</MessageTime>
</UserWrap>
</AvatarWrapper>
<MessageContent>
</MessageHeader>
<MessageContent style={{ fontFamily }}>
{message.status === 'sending' && (
<MessageContentLoading>
<SyncOutlined spin size={24} />
</MessageContentLoading>
)}
{message.status !== 'sending' && (
<Markdown className="markdown" components={{ code: CodeBlock as any }}>
{getMessageContent(message)}
</Markdown>
{message.status !== 'sending' && <Markdown message={message} />}
{message.usage && !generating && (
<MessageMetadata>
Tokens: {message.usage.total_tokens} | {message.usage.prompt_tokens}{message.usage.completion_tokens}
</MessageMetadata>
)}
{showMenu && (
<MenusBar className={`menubar ${isLastMessage && 'show'}`}>
<MenusBar className={`menubar ${isLastMessage && 'show'} ${(!isLastMessage || isUserMessage) && 'user'}`}>
{message.role === 'user' && (
<Tooltip title="Edit" mouseEnterDelay={0.8}>
<EditOutlined onClick={onEdit} />
<ActionButton onClick={onEdit}>
<EditOutlined />
</ActionButton>
</Tooltip>
)}
<Tooltip title={t('common.copy')} mouseEnterDelay={0.8}>
<CopyOutlined onClick={onCopy} />
</Tooltip>
<Tooltip title={t('common.delete')} mouseEnterDelay={0.8}>
<DeleteOutlined onClick={onDelete} />
<ActionButton onClick={onCopy}>
{!copied && <CopyOutlined />}
{copied && <CheckOutlined style={{ color: 'var(--color-primary)' }} />}
</ActionButton>
</Tooltip>
{canRegenerate && (
<SelectModelDropdown model={model} onSelect={onRegenerate} placement="topRight">
<Tooltip title={t('common.regenerate')} mouseEnterDelay={0.8}>
<SyncOutlined onClick={onRegenerate} />
<ActionButton>
<SyncOutlined />
</ActionButton>
</Tooltip>
</SelectModelDropdown>
)}
<MessageMetadata>{message.modelId}</MessageMetadata>
{message.usage && (
<>
<MessageMetadata style={{ textTransform: 'uppercase' }}>
tokens used: {message.usage.total_tokens} (IN:{message.usage.prompt_tokens}/OUT:
{message.usage.completion_tokens})
</MessageMetadata>
</>
<Popconfirm
title={t('message.message.delete.content')}
okButtonProps={{ danger: true }}
icon={<QuestionCircleOutlined style={{ color: 'red' }} />}
onConfirm={() => onDeleteMessage?.(message)}>
<Tooltip title={t('common.delete')} mouseEnterDelay={1}>
<ActionButton>
<DeleteOutlined />
</ActionButton>
</Tooltip>
</Popconfirm>
{!isUserMessage && (
<Dropdown menu={{ items: dropdownItems }} trigger={['click']} placement="topRight" arrow>
<ActionButton>
<MenuOutlined />
</ActionButton>
</Dropdown>
)}
</MenusBar>
)}
@@ -119,26 +175,21 @@ const MessageItem: FC<Props> = ({ message, index, showMenu, onDeleteMessage }) =
const MessageContainer = styled.div`
display: flex;
flex-direction: row;
padding: 10px 15px;
position: relative;
`
const AvatarWrapper = styled.div`
margin-right: 10px;
`
const MessageContent = styled.div`
display: flex;
flex: 1;
flex-direction: column;
justify-content: space-between;
padding: 10px 20px;
position: relative;
border-bottom: 0.5px dotted var(--color-border);
.menubar {
opacity: 0;
transition: opacity 0.2s ease;
&.show {
opacity: 1;
}
&.user {
position: absolute;
top: 15px;
right: 10px;
}
}
&:hover {
.menubar {
@@ -147,6 +198,47 @@ const MessageContent = styled.div`
}
`
const MessageHeader = styled.div`
margin-right: 10px;
display: flex;
flex-direction: row;
align-items: center;
padding-bottom: 4px;
justify-content: space-between;
`
const AvatarWrapper = styled.div`
display: flex;
flex-direction: row;
align-items: center;
`
const UserWrap = styled.div`
display: flex;
flex-direction: column;
justify-content: space-between;
margin-left: 12px;
`
const UserName = styled.div`
font-size: 14px;
font-weight: 600;
`
const MessageTime = styled.div`
font-size: 12px;
color: var(--color-text-3);
`
const MessageContent = styled.div`
display: flex;
flex: 1;
flex-direction: column;
justify-content: space-between;
margin-left: 46px;
margin-top: 5px;
`
const MessageContentLoading = styled.div`
display: flex;
flex-direction: row;
@@ -157,17 +249,9 @@ const MessageContentLoading = styled.div`
const MenusBar = styled.div`
display: flex;
flex-direction: row;
justify-content: flex-start;
justify-content: flex-end;
align-items: center;
gap: 6px;
.anticon {
cursor: pointer;
margin-right: 8px;
font-size: 15px;
color: var(--color-icon);
&:hover {
color: var(--color-text-1);
}
}
`
const MessageMetadata = styled.div`
@@ -176,4 +260,24 @@ const MessageMetadata = styled.div`
user-select: text;
`
export default MessageItem
const ActionButton = styled.div`
cursor: pointer;
border: 1px solid var(--color-border);
border-radius: 8px;
display: flex;
flex-direction: row;
justify-content: center;
align-items: center;
width: 30px;
height: 30px;
.anticon {
cursor: pointer;
font-size: 14px;
color: var(--color-icon);
}
&:hover {
color: var(--color-text-1);
}
`
export default memo(MessageItem)

View File

@@ -1,16 +1,19 @@
import { EVENT_NAMES, EventEmitter } from '@renderer/services/event'
import { Assistant, Message, Topic } from '@renderer/types'
import localforage from 'localforage'
import { FC, useCallback, useEffect, useRef, useState } from 'react'
import styled from 'styled-components'
import MessageItem from './Message'
import { reverse } from 'lodash'
import { fetchChatCompletion, fetchMessagesSummary } from '@renderer/services/api'
import { useAssistant } from '@renderer/hooks/useAssistant'
import { runAsyncFunction } from '@renderer/utils'
import LocalStorage from '@renderer/services/storage'
import { useProviderByAssistant } from '@renderer/hooks/useProvider'
import { fetchChatCompletion, fetchMessagesSummary } from '@renderer/services/api'
import { EVENT_NAMES, EventEmitter } from '@renderer/services/event'
import { estimateHistoryTokenCount, filterAtMessages } from '@renderer/services/messages'
import LocalStorage from '@renderer/services/storage'
import { Assistant, Message, Model, Topic } from '@renderer/types'
import { getBriefInfo, runAsyncFunction, uuid } from '@renderer/utils'
import { t } from 'i18next'
import localforage from 'localforage'
import { debounce, last, reverse } from 'lodash'
import { FC, useCallback, useEffect, useMemo, useRef, useState } from 'react'
import styled from 'styled-components'
import MessageItem from './Message'
import Suggestions from './Suggestions'
interface Props {
assistant: Assistant
@@ -20,52 +23,51 @@ interface Props {
const Messages: FC<Props> = ({ assistant, topic }) => {
const [messages, setMessages] = useState<Message[]>([])
const [lastMessage, setLastMessage] = useState<Message | null>(null)
const { updateTopic } = useAssistant(assistant.id)
const provider = useProviderByAssistant(assistant)
const messagesRef = useRef<HTMLDivElement>(null)
const containerRef = useRef<HTMLDivElement>(null)
const { updateTopic } = useAssistant(assistant.id)
const assistantDefaultMessage: Message = {
const assistantDefaultMessage: Message = useMemo(
() => ({
id: 'assistant',
role: 'assistant',
content: assistant.prompt || t('assistant.default.description'),
content: assistant.description || assistant.prompt || t('chat.default.description'),
assistantId: assistant.id,
topicId: topic.id,
status: 'pending',
createdAt: new Date().toISOString()
}
}),
[assistant.description, assistant.id, assistant.prompt, topic.id]
)
const onSendMessage = useCallback(
(message: Message) => {
const _messages = [...messages, message]
setMessages(_messages)
localforage.setItem(`topic:${topic.id}`, {
...topic,
messages: _messages
})
localforage.setItem(`topic:${topic.id}`, { ...topic, messages: _messages })
},
[messages, topic]
)
const autoRenameTopic = useCallback(async () => {
if (topic.name === t('assistant.default.topic.name') && messages.length >= 2) {
if (topic.name === t('chat.default.topic.name') && messages.length >= 2) {
const summaryText = await fetchMessagesSummary({ messages, assistant })
summaryText && updateTopic({ ...topic, name: summaryText })
}
}, [assistant, messages, topic, updateTopic])
const onDeleteMessage = (message: Message) => {
const onDeleteMessage = useCallback(
(message: Message) => {
const _messages = messages.filter((m) => m.id !== message.id)
setMessages(_messages)
localforage.setItem(`topic:${topic.id}`, {
id: topic.id,
messages: _messages
})
}
localforage.setItem(`topic:${topic.id}`, { id: topic.id, messages: _messages })
},
[messages, topic.id]
)
useEffect(() => {
const unsubscribes = [
EventEmitter.on(EVENT_NAMES.SEND_MESSAGE, async (msg: Message) => {
console.debug({ assistant, provider, message: msg, topic })
onSendMessage(msg)
fetchChatCompletion({ assistant, messages: [...messages, msg], topic, onResponse: setLastMessage })
}),
@@ -74,8 +76,18 @@ const Messages: FC<Props> = ({ assistant, topic }) => {
onSendMessage(msg)
setTimeout(() => EventEmitter.emit(EVENT_NAMES.AI_AUTO_RENAME), 100)
}),
EventEmitter.on(EVENT_NAMES.REGENERATE_MESSAGE, async () => {
fetchChatCompletion({ assistant, messages: messages, topic, onResponse: setLastMessage })
EventEmitter.on(EVENT_NAMES.REGENERATE_MESSAGE, async (model: Model) => {
const lastUserMessage = last(filterAtMessages(messages).filter((m) => m.role === 'user'))
if (lastUserMessage) {
const content = `[@${model.name}](#) ${getBriefInfo(lastUserMessage.content)}`
onSendMessage({ ...lastUserMessage, id: uuid(), type: '@', content })
fetchChatCompletion({
assistant,
topic,
messages: [...messages, lastUserMessage],
onResponse: setLastMessage
})
}
}),
EventEmitter.on(EVENT_NAMES.AI_AUTO_RENAME, autoRenameTopic),
EventEmitter.on(EVENT_NAMES.CLEAR_MESSAGES, () => {
@@ -85,21 +97,31 @@ const Messages: FC<Props> = ({ assistant, topic }) => {
})
]
return () => unsubscribes.forEach((unsub) => unsub())
}, [assistant, autoRenameTopic, messages, onSendMessage, provider, topic, updateTopic])
}, [assistant, messages, provider, topic, autoRenameTopic, updateTopic, onSendMessage])
useEffect(() => {
runAsyncFunction(async () => {
const messages = await LocalStorage.getTopicMessages(topic.id)
setMessages(messages || [])
const messages = (await LocalStorage.getTopicMessages(topic.id)) || []
setMessages(messages)
})
}, [topic.id])
const scrollTop = useCallback(
debounce(() => containerRef.current?.scrollTo({ top: 100000, behavior: 'auto' }), 500),
[]
)
useEffect(() => {
messagesRef.current?.scrollTo({ top: 100000, behavior: 'auto' })
}, [messages])
scrollTop()
}, [messages, lastMessage, scrollTop])
useEffect(() => {
EventEmitter.emit(EVENT_NAMES.ESTIMATED_TOKEN_COUNT, estimateHistoryTokenCount(assistant, messages))
}, [assistant, messages])
return (
<Container id="messages" key={assistant.id} ref={messagesRef}>
<Container id="messages" key={assistant.id} ref={containerRef}>
<Suggestions assistant={assistant} messages={messages} lastMessage={lastMessage} />
{lastMessage && <MessageItem message={lastMessage} />}
{reverse([...messages]).map((message, index) => (
<MessageItem key={message.id} message={message} showMenu index={index} onDeleteMessage={onDeleteMessage} />
@@ -115,8 +137,6 @@ const Container = styled.div`
overflow-y: auto;
flex-direction: column-reverse;
max-height: calc(100vh - var(--input-bar-height) - var(--navbar-height));
padding-top: 10px;
padding-bottom: 20px;
`
export default Messages

View File

@@ -1,51 +0,0 @@
import { NavbarCenter } from '@renderer/components/app/Navbar'
import { colorPrimary } from '@renderer/config/antd'
import { useAssistant } from '@renderer/hooks/useAssistant'
import { useProviders } from '@renderer/hooks/useProvider'
import { Assistant } from '@renderer/types'
import { Button, Dropdown, MenuProps } from 'antd'
import { FC } from 'react'
import { useTranslation } from 'react-i18next'
import styled from 'styled-components'
interface Props {
activeAssistant: Assistant
}
const Navigation: FC<Props> = ({ activeAssistant }) => {
const { providers } = useProviders()
const { model, setModel } = useAssistant(activeAssistant.id)
const { t } = useTranslation()
const items: MenuProps['items'] = providers
.filter((p) => p.models.length > 0)
.map((p) => ({
key: p.id,
label: t(`provider.${p.id}`),
type: 'group',
children: p.models.map((m) => ({
key: m.id,
label: m.name,
style: m.id === model?.id ? { color: colorPrimary } : undefined,
onClick: () => setModel(m)
}))
}))
return (
<NavbarCenter style={{ border: 'none', padding: '0 15px' }}>
{activeAssistant?.name}
<DropdownMenu menu={{ items, style: { maxHeight: '80vh', overflow: 'auto' } }} trigger={['click']}>
<Button size="small" type="primary" ghost style={{ fontSize: '11px' }}>
{model ? model.name : t('button.select_model')}
</Button>
</DropdownMenu>
</NavbarCenter>
)
}
const DropdownMenu = styled(Dropdown)`
-webkit-app-region: none;
margin-left: 10px;
`
export default Navigation

View File

@@ -0,0 +1,43 @@
import { NavbarCenter } from '@renderer/components/app/Navbar'
import { isMac } from '@renderer/config/constant'
import { useAssistant } from '@renderer/hooks/useAssistant'
import { useShowAssistants } from '@renderer/hooks/useStore'
import { Assistant } from '@renderer/types'
import { removeLeadingEmoji } from '@renderer/utils'
import { FC } from 'react'
import { useTranslation } from 'react-i18next'
import styled from 'styled-components'
import { NewButton } from '../HomePage'
import SelectModelButton from './SelectModelButton'
interface Props {
activeAssistant: Assistant
}
const NavigationCenter: FC<Props> = ({ activeAssistant }) => {
const { assistant } = useAssistant(activeAssistant.id)
const { t } = useTranslation()
const { showAssistants, toggleShowAssistants } = useShowAssistants()
return (
<NavbarCenter style={{ paddingLeft: isMac ? 16 : 8 }}>
{!showAssistants && (
<NewButton onClick={toggleShowAssistants} style={{ marginRight: isMac ? 8 : 25 }}>
<i className="iconfont icon-showsidebarhoriz" />
</NewButton>
)}
<AssistantName>{removeLeadingEmoji(assistant?.name) || t('chat.default.name')}</AssistantName>
<SelectModelButton assistant={assistant} />
</NavbarCenter>
)
}
const AssistantName = styled.span`
font-weight: bold;
margin-left: 5px;
margin-right: 10px;
font-family: Ubuntu;
`
export default NavigationCenter

View File

@@ -0,0 +1,41 @@
import { getModelLogo } from '@renderer/config/provider'
import { useAssistant } from '@renderer/hooks/useAssistant'
import { Assistant } from '@renderer/types'
import { Avatar, Button } from 'antd'
import { upperFirst } from 'lodash'
import { FC } from 'react'
import { useTranslation } from 'react-i18next'
import styled from 'styled-components'
import SelectModelDropdown from './SelectModelDropdown'
interface Props {
assistant: Assistant
}
const SelectModelButton: FC<Props> = ({ assistant }) => {
const { model, setModel } = useAssistant(assistant.id)
const { t } = useTranslation()
return (
<SelectModelDropdown model={model} onSelect={setModel}>
<DropdownButton size="small" type="default">
<Avatar src={getModelLogo(model?.id || '')} style={{ width: 20, height: 20 }} />
<ModelName>{model ? upperFirst(model.name) : t('button.select_model')}</ModelName>
</DropdownButton>
</SelectModelDropdown>
)
}
const DropdownButton = styled(Button)`
font-size: 11px;
border-radius: 15px;
padding: 12px 8px 12px 3px;
`
const ModelName = styled.span`
margin-left: -2px;
font-weight: bolder;
`
export default SelectModelButton

View File

@@ -0,0 +1,55 @@
import { getModelLogo } from '@renderer/config/provider'
import { useProviders } from '@renderer/hooks/useProvider'
import { Model } from '@renderer/types'
import { Avatar, Dropdown, DropdownProps, MenuProps } from 'antd'
import { first, upperFirst } from 'lodash'
import { FC, PropsWithChildren } from 'react'
import { useTranslation } from 'react-i18next'
import styled from 'styled-components'
interface Props extends DropdownProps {
model: Model
onSelect: (model: Model) => void
}
const SelectModelDropdown: FC<Props & PropsWithChildren> = ({ children, model, onSelect, ...props }) => {
const { t } = useTranslation()
const { providers } = useProviders()
const items: MenuProps['items'] = providers
.filter((p) => p.models.length > 0)
.map((p) => ({
key: p.id,
label: p.isSystem ? t(`provider.${p.id}`) : p.name,
type: 'group',
children: p.models.map((m) => ({
key: m?.id,
label: upperFirst(m?.name),
style: m?.id === model?.id ? { color: 'var(--color-primary)' } : undefined,
icon: (
<Avatar src={getModelLogo(m?.id || '')} size={24}>
{first(m?.name)}
</Avatar>
),
onClick: () => m && onSelect(m)
}))
}))
return (
<DropdownMenu
menu={{ items, style: { maxHeight: '80vh', overflow: 'auto' } }}
trigger={['click']}
arrow
placement="bottom"
overlayClassName="chat-nav-dropdown"
{...props}>
{children}
</DropdownMenu>
)
}
const DropdownMenu = styled(Dropdown)`
-webkit-app-region: none;
`
export default SelectModelDropdown

View File

@@ -0,0 +1,119 @@
import { fetchSuggestions } from '@renderer/services/api'
import { EVENT_NAMES, EventEmitter } from '@renderer/services/event'
import { Assistant, Message, Suggestion } from '@renderer/types'
import { uuid } from '@renderer/utils'
import dayjs from 'dayjs'
import { FC, useEffect, useState } from 'react'
import BeatLoader from 'react-spinners/BeatLoader'
import styled from 'styled-components'
interface Props {
assistant: Assistant
messages: Message[]
lastMessage: Message | null
}
const suggestionsMap = new Map<string, Suggestion[]>()
const Suggestions: FC<Props> = ({ assistant, messages, lastMessage }) => {
const [suggestions, setSuggestions] = useState<Suggestion[]>(
suggestionsMap.get(messages[messages.length - 1]?.id) || []
)
const [loadingSuggestions, setLoadingSuggestions] = useState(false)
const onClick = (s: Suggestion) => {
const message: Message = {
id: uuid(),
role: 'user',
content: s.content,
assistantId: assistant.id,
topicId: assistant.topics[0].id || uuid(),
createdAt: dayjs().format('YYYY-MM-DD HH:mm:ss'),
status: 'success'
}
EventEmitter.emit(EVENT_NAMES.SEND_MESSAGE, message)
}
useEffect(() => {
const unsubscribes = [
EventEmitter.on(EVENT_NAMES.AI_CHAT_COMPLETION, async (msg: Message) => {
setLoadingSuggestions(true)
const _suggestions = await fetchSuggestions({ assistant, messages: [...messages, msg] })
if (_suggestions.length) {
setSuggestions(_suggestions)
suggestionsMap.set(msg.id, _suggestions)
}
setLoadingSuggestions(false)
})
]
return () => unsubscribes.forEach((unsub) => unsub())
}, [assistant, messages])
useEffect(() => {
setSuggestions(suggestionsMap.get(messages[messages.length - 1]?.id) || [])
}, [messages])
if (lastMessage) {
return null
}
if (loadingSuggestions) {
return (
<Container>
<BeatLoader color="var(--color-text-2)" size="10" />
</Container>
)
}
if (suggestions.length === 0) {
return <Container style={{ paddingBottom: 10 }} />
}
return (
<Container>
<SuggestionsContainer>
{suggestions.map((s, i) => (
<SuggestionItem key={i} onClick={() => onClick(s)}>
{s.content}
</SuggestionItem>
))}
</SuggestionsContainer>
</Container>
)
}
const Container = styled.div`
display: flex;
flex-direction: column;
padding: 10px 10px 20px 65px;
display: flex;
width: 100%;
flex-direction: row;
flex-wrap: wrap;
gap: 15px;
`
const SuggestionsContainer = styled.div`
display: flex;
flex-direction: row;
flex-wrap: wrap;
gap: 10px;
`
const SuggestionItem = styled.div`
display: flex;
align-items: center;
width: fit-content;
padding: 5px 10px;
border-radius: 12px;
font-size: 12px;
color: var(--color-text);
background: var(--color-background-mute);
cursor: pointer;
&:hover {
opacity: 0.9;
}
`
export default Suggestions

View File

@@ -1,196 +0,0 @@
import PromptPopup from '@renderer/components/Popups/PromptPopup'
import { useAssistant } from '@renderer/hooks/useAssistant'
import { useShowRightSidebar } from '@renderer/hooks/useStore'
import { fetchMessagesSummary } from '@renderer/services/api'
import { Assistant, Topic } from '@renderer/types'
import { Button, Dropdown, MenuProps, Popconfirm } from 'antd'
import { FC, useRef } from 'react'
import styled from 'styled-components'
import { DeleteOutlined, EditOutlined, SignatureOutlined } from '@ant-design/icons'
import LocalStorage from '@renderer/services/storage'
import { DragDropContext, Draggable, Droppable, DropResult } from '@hello-pangea/dnd'
import { droppableReorder } from '@renderer/utils'
import { useTranslation } from 'react-i18next'
interface Props {
assistant: Assistant
activeTopic: Topic
setActiveTopic: (topic: Topic) => void
}
const Topics: FC<Props> = ({ assistant, activeTopic, setActiveTopic }) => {
const { showRightSidebar } = useShowRightSidebar()
const { removeTopic, updateTopic, removeAllTopics, updateTopics } = useAssistant(assistant.id)
const currentTopic = useRef<Topic | null>(null)
const { t } = useTranslation()
const topicMenuItems: MenuProps['items'] = [
{
label: t('assistant.topics.auto_rename'),
key: 'auto-rename',
icon: <SignatureOutlined />,
async onClick() {
if (currentTopic.current) {
const messages = await LocalStorage.getTopicMessages(currentTopic.current.id)
if (messages.length >= 2) {
const summaryText = await fetchMessagesSummary({ messages, assistant })
if (summaryText) {
updateTopic({ ...currentTopic.current, name: summaryText })
}
}
}
}
},
{
label: t('common.rename'),
key: 'rename',
icon: <EditOutlined />,
async onClick() {
const name = await PromptPopup.show({
title: t('assistant.topics.edit.title'),
message: t('assistant.topics.edit.placeholder'),
defaultValue: currentTopic.current?.name || ''
})
if (name && currentTopic.current && currentTopic.current?.name !== name) {
updateTopic({ ...currentTopic.current, name })
}
}
}
]
if (assistant.topics.length > 1) {
topicMenuItems.push({ type: 'divider' })
topicMenuItems.push({
label: t('common.delete'),
danger: true,
key: 'delete',
icon: <DeleteOutlined />,
onClick() {
if (assistant.topics.length === 1) return
currentTopic.current && removeTopic(currentTopic.current)
currentTopic.current = null
setActiveTopic(assistant.topics[0])
}
})
}
const onDragEnd = (result: DropResult) => {
if (result.destination) {
const sourceIndex = result.source.index
const destIndex = result.destination.index
updateTopics(droppableReorder(assistant.topics, sourceIndex, destIndex))
}
}
if (!showRightSidebar) {
return null
}
return (
<Container className={showRightSidebar ? '' : 'collapsed'}>
<TopicTitle>
<span>
{t('assistant.topics.title')} ({assistant.topics.length})
</span>
<Popconfirm
icon={false}
title={t('assistant.topics.delete.all.title')}
description={t('assistant.topics.delete.all.content')}
placement="leftBottom"
onConfirm={removeAllTopics}
okText="Delete All"
okType="danger"
cancelText="Cancel">
<DeleteButton type="text">
<DeleteIcon />
</DeleteButton>
</Popconfirm>
</TopicTitle>
<DragDropContext onDragEnd={onDragEnd}>
<Droppable droppableId="droppable">
{(provided) => (
<div {...provided.droppableProps} ref={provided.innerRef}>
{assistant.topics.map((topic, index) => (
<Draggable key={`draggable_${topic.id}_${index}`} draggableId={topic.id} index={index}>
{(provided) => (
<div ref={provided.innerRef} {...provided.draggableProps} {...provided.dragHandleProps}>
<Dropdown
menu={{ items: topicMenuItems }}
trigger={['contextMenu']}
key={topic.id}
onOpenChange={(open) => open && (currentTopic.current = topic)}>
<TopicListItem
className={topic.id === activeTopic?.id ? 'active' : ''}
onClick={() => setActiveTopic(topic)}>
{topic.name}
</TopicListItem>
</Dropdown>
</div>
)}
</Draggable>
))}
</div>
)}
</Droppable>
</DragDropContext>
</Container>
)
}
const Container = styled.div`
width: var(--topic-list-width);
height: 100%;
border-left: 0.5px solid var(--color-border);
padding: 10px;
overflow-y: auto;
&.collapsed {
width: 0;
border-left: none;
}
`
const TopicListItem = styled.div`
padding: 8px 10px;
margin-bottom: 5px;
cursor: pointer;
border-radius: 5px;
font-size: 13px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
&:hover {
background-color: var(--color-background-soft);
}
&.active {
background-color: var(--color-background-soft);
}
`
const TopicTitle = styled.div`
font-weight: bold;
margin-bottom: 10px;
font-size: 14px;
color: var(--color-text-1);
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
`
const DeleteButton = styled(Button)`
width: 30px;
height: 30px;
border-radius: 50%;
padding: 0;
&:hover {
.anticon {
color: #ff4d4f;
}
}
`
const DeleteIcon = styled(DeleteOutlined)`
font-size: 16px;
`
export default Topics

View File

@@ -1,47 +1,53 @@
import { EVENT_NAMES, EventEmitter } from '@renderer/services/event'
import { Assistant, Message, Topic } from '@renderer/types'
import { uuid } from '@renderer/utils'
import { FC, useCallback, useEffect, useRef, useState } from 'react'
import styled from 'styled-components'
import { MoreOutlined } from '@ant-design/icons'
import { Button, Popconfirm, Tooltip } from 'antd'
import { useShowRightSidebar } from '@renderer/hooks/useStore'
import { useAssistant } from '@renderer/hooks/useAssistant'
import {
ClearOutlined,
ControlOutlined,
FullscreenExitOutlined,
FullscreenOutlined,
HistoryOutlined,
PauseCircleOutlined,
PlusCircleOutlined
PlusCircleOutlined,
QuestionCircleOutlined
} from '@ant-design/icons'
import TextArea, { TextAreaRef } from 'antd/es/input/TextArea'
import { isEmpty } from 'lodash'
import SendMessageSetting from './SendMessageSetting'
import { DEFAULT_CONEXTCOUNT } from '@renderer/config/constant'
import { useAssistant } from '@renderer/hooks/useAssistant'
import { useSettings } from '@renderer/hooks/useSettings'
import dayjs from 'dayjs'
import store, { useAppSelector } from '@renderer/store'
import { getDefaultTopic } from '@renderer/services/assistant'
import { useTranslation } from 'react-i18next'
import { EVENT_NAMES, EventEmitter } from '@renderer/services/event'
import { estimateInputTokenCount } from '@renderer/services/messages'
import store, { useAppSelector } from '@renderer/store'
import { setGenerating } from '@renderer/store/runtime'
import { Assistant, Message, Topic } from '@renderer/types'
import { uuid } from '@renderer/utils'
import { Button, Popconfirm, Tag, Tooltip } from 'antd'
import TextArea, { TextAreaRef } from 'antd/es/input/TextArea'
import dayjs from 'dayjs'
import { debounce, isEmpty } from 'lodash'
import { FC, useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next'
import styled from 'styled-components'
import SendMessageButton from './SendMessageButton'
interface Props {
assistant: Assistant
setActiveTopic: (topic: Topic) => void
}
let _text = ''
const Inputbar: FC<Props> = ({ assistant, setActiveTopic }) => {
const [text, setText] = useState('')
const { setShowRightSidebar } = useShowRightSidebar()
const [text, setText] = useState(_text)
const { addTopic } = useAssistant(assistant.id)
const { sendMessageShortcut } = useSettings()
const { sendMessageShortcut, showInputEstimatedTokens } = useSettings()
const [expended, setExpend] = useState(false)
const [estimateTokenCount, setEstimateTokenCount] = useState(0)
const generating = useAppSelector((state) => state.runtime.generating)
const inputRef = useRef<TextAreaRef>(null)
const { t } = useTranslation()
const sendMessage = () => {
_text = text
const sendMessage = useCallback(() => {
if (generating) {
return
}
@@ -63,9 +69,23 @@ const Inputbar: FC<Props> = ({ assistant, setActiveTopic }) => {
EventEmitter.emit(EVENT_NAMES.SEND_MESSAGE, message)
setText('')
}
setExpend(false)
}, [assistant.id, assistant.topics, generating, text])
const inputTokenCount = useMemo(() => estimateInputTokenCount(text), [text])
const handleKeyDown = (event: React.KeyboardEvent<HTMLTextAreaElement>) => {
if (expended) {
if (event.key === 'Escape') {
return setExpend(false)
}
if (event.key === 'Enter' && event.shiftKey) {
return sendMessage()
}
return
}
if (sendMessageShortcut === 'Enter' && event.key === 'Enter') {
if (event.shiftKey) {
return
@@ -81,7 +101,7 @@ const Inputbar: FC<Props> = ({ assistant, setActiveTopic }) => {
}
const addNewTopic = useCallback(() => {
const topic: Topic = getDefaultTopic()
const topic = getDefaultTopic()
addTopic(topic)
setActiveTopic(topic)
}, [addTopic, setActiveTopic])
@@ -99,6 +119,7 @@ const Inputbar: FC<Props> = ({ assistant, setActiveTopic }) => {
if (!generating) {
if ((e.ctrlKey || e.metaKey) && e.key === 'n') {
addNewTopic()
EventEmitter.emit(EVENT_NAMES.SHOW_TOPIC_SIDEBAR)
inputRef.current?.focus()
}
}
@@ -108,11 +129,13 @@ const Inputbar: FC<Props> = ({ assistant, setActiveTopic }) => {
}, [addNewTopic, generating])
useEffect(() => {
const _setEstimateTokenCount = debounce(setEstimateTokenCount, 100, { leading: false, trailing: true })
const unsubscribes = [
EventEmitter.on(EVENT_NAMES.EDIT_MESSAGE, (message: Message) => {
setText(message.content)
inputRef.current?.focus()
})
}),
EventEmitter.on(EVENT_NAMES.ESTIMATED_TOKEN_COUNT, _setEstimateTokenCount)
]
return () => unsubscribes.forEach((unsub) => unsub())
}, [])
@@ -122,63 +145,74 @@ const Inputbar: FC<Props> = ({ assistant, setActiveTopic }) => {
}, [assistant])
return (
<Container id="inputbar" style={{ minHeight: expended ? '35%' : 'var(--input-bar-height)' }}>
<Toolbar>
<Container id="inputbar" style={{ minHeight: expended ? '100%' : 'var(--input-bar-height)' }}>
<Toolbar onDoubleClick={() => setExpend(!expended)}>
<ToolbarMenu>
<Tooltip placement="top" title={t('assistant.input.new_chat')} arrow>
<Tooltip placement="top" title={t('chat.input.new_topic')} arrow>
<ToolbarButton type="text" onClick={addNewTopic}>
<PlusCircleOutlined />
</ToolbarButton>
</Tooltip>
<Tooltip placement="top" title={t('assistant.input.topics')} arrow>
<ToolbarButton type="text" onClick={setShowRightSidebar}>
<Tooltip placement="top" title={t('chat.input.topics')} arrow>
<ToolbarButton type="text" onClick={() => EventEmitter.emit(EVENT_NAMES.SHOW_TOPIC_SIDEBAR)}>
<HistoryOutlined />
</ToolbarButton>
</Tooltip>
<Tooltip placement="top" title={t('assistant.input.clear')} arrow>
<Tooltip placement="top" title={t('chat.input.settings')} arrow>
<ToolbarButton type="text" onClick={() => EventEmitter.emit(EVENT_NAMES.SHOW_CHAT_SETTINGS)}>
<ControlOutlined />
</ToolbarButton>
</Tooltip>
<Tooltip placement="top" title={t('chat.input.clear')} arrow>
<Popconfirm
icon={false}
title={t('assistant.input.clear.title')}
description={t('assistant.input.clear.content')}
title={t('chat.input.clear.content')}
placement="top"
onConfirm={clearTopic}
okText={t('assistant.input.clear')}>
okButtonProps={{ danger: true }}
icon={<QuestionCircleOutlined style={{ color: 'red' }} />}
okText={t('chat.input.clear')}>
<ToolbarButton type="text">
<ClearOutlined />
</ToolbarButton>
</Popconfirm>
</Tooltip>
<Tooltip placement="top" title={expended ? t('assistant.input.collapse') : t('assistant.input.expand')} arrow>
<Tooltip placement="top" title={expended ? t('chat.input.collapse') : t('chat.input.expand')} arrow>
<ToolbarButton type="text" onClick={() => setExpend(!expended)}>
{expended ? <FullscreenExitOutlined /> : <FullscreenOutlined />}
</ToolbarButton>
</Tooltip>
{showInputEstimatedTokens && (
<TextCount>
<Tooltip title={t('chat.input.context_count.tip')}>
<Tag style={{ cursor: 'pointer' }}>{assistant?.settings?.contextCount ?? DEFAULT_CONEXTCOUNT}</Tag>
</Tooltip>
<Tooltip title={t('chat.input.estimated_tokens.tip')}>
<Tag style={{ cursor: 'pointer' }}> {`${inputTokenCount} / ${estimateTokenCount}`}</Tag>
</Tooltip>
</TextCount>
)}
</ToolbarMenu>
<ToolbarMenu>
{generating && (
<Tooltip placement="top" title={t('assistant.input.pause')} arrow>
<Tooltip placement="top" title={t('chat.input.pause')} arrow>
<ToolbarButton type="text" onClick={onPause}>
<PauseCircleOutlined />
<PauseCircleOutlined style={{ color: 'var(--color-error)' }} />
</ToolbarButton>
</Tooltip>
)}
<SendMessageSetting>
<ToolbarButton type="text" style={{ marginRight: 0 }}>
<MoreOutlined />
</ToolbarButton>
</SendMessageSetting>
<SendMessageButton sendMessage={sendMessage} />
</ToolbarMenu>
</Toolbar>
<Textarea
value={text}
onChange={(e) => setText(e.target.value)}
onKeyDown={handleKeyDown}
placeholder={t('assistant.input.placeholder')}
placeholder={t('chat.input.placeholder')}
autoFocus
contextMenu="true"
variant="borderless"
styles={{ textarea: { paddingLeft: 0 } }}
ref={inputRef}
styles={{ textarea: { paddingLeft: 0 } }}
/>
</Container>
)
@@ -188,10 +222,9 @@ const Container = styled.div`
display: flex;
flex-direction: column;
width: 100%;
height: var(--input-bar-height);
border-top: 0.5px solid var(--color-border);
padding: 5px 15px;
transition: all 0.3s ease;
position: relative;
`
const Textarea = styled(TextArea)`
@@ -199,20 +232,21 @@ const Textarea = styled(TextArea)`
border-radius: 0;
display: flex;
flex: 1;
margin: 0 15px 5px 15px;
`
const Toolbar = styled.div`
display: flex;
flex-direction: row;
justify-content: space-between;
margin: 0 -5px;
margin-bottom: 5px;
padding: 3px 10px;
`
const ToolbarMenu = styled.div`
display: flex;
flex-direction: row;
align-items: center;
gap: 6px;
`
const ToolbarButton = styled(Button)`
@@ -221,7 +255,6 @@ const ToolbarButton = styled(Button)`
font-size: 18px;
border-radius: 50%;
transition: all 0.3s ease;
margin-right: 6px;
color: var(--color-icon);
&.anticon {
transition: all 0.3s ease;
@@ -230,9 +263,19 @@ const ToolbarButton = styled(Button)`
&:hover {
background-color: var(--color-background-soft);
.anticon {
color: white;
color: var(--color-text-1);
}
}
`
const TextCount = styled.div`
font-size: 11px;
color: var(--color-text-3);
z-index: 10;
padding: 2px;
border-top-left-radius: 7px;
user-select: none;
margin-right: 10px;
`
export default Inputbar

View File

@@ -1,24 +1,27 @@
import { ArrowUpOutlined, EnterOutlined } from '@ant-design/icons'
import { SendOutlined } from '@ant-design/icons'
import { useSettings } from '@renderer/hooks/useSettings'
import { Dropdown, MenuProps } from 'antd'
import { FC, PropsWithChildren } from 'react'
import { ArrowUpOutlined, EnterOutlined } from '@ant-design/icons'
import { FC } from 'react'
import { useTranslation } from 'react-i18next'
interface Props extends PropsWithChildren {}
interface Props {
sendMessage: () => void
}
const SendMessageSetting: FC<Props> = ({ children }) => {
const SendMessageButton: FC<Props> = ({ sendMessage }) => {
const { sendMessageShortcut, setSendMessageShortcut } = useSettings()
const { t } = useTranslation()
const sendSettingItems: MenuProps['items'] = [
{
label: `Enter ${t('assistant.input.send')}`,
label: `Enter ${t('chat.input.send')}`,
key: 'Enter',
icon: <EnterOutlined />,
onClick: () => setSendMessageShortcut('Enter')
},
{
label: `Shift+Enter ${t('assistant.input.send')}`,
label: `Shift+Enter ${t('chat.input.send')}`,
key: 'Shift+Enter',
icon: <ArrowUpOutlined />,
onClick: () => setSendMessageShortcut('Shift+Enter')
@@ -26,14 +29,17 @@ const SendMessageSetting: FC<Props> = ({ children }) => {
]
return (
<Dropdown
menu={{ items: sendSettingItems, selectable: true, defaultSelectedKeys: [sendMessageShortcut] }}
placement="top"
<Dropdown.Button
size="small"
onClick={sendMessage}
trigger={['click']}
arrow>
{children}
</Dropdown>
arrow
menu={{ items: sendSettingItems, selectable: true, defaultSelectedKeys: [sendMessageShortcut] }}
style={{ width: 'auto' }}>
{t('chat.input.send')}
<SendOutlined />
</Dropdown.Button>
)
}
export default SendMessageSetting
export default SendMessageButton

View File

@@ -1,9 +1,14 @@
import React from 'react'
import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter'
import { atomDark } from 'react-syntax-highlighter/dist/esm/styles/prism'
import styled from 'styled-components'
import { CopyOutlined } from '@ant-design/icons'
import { CheckOutlined, CopyOutlined } from '@ant-design/icons'
import { initMermaid } from '@renderer/init'
import { useTheme } from '@renderer/providers/ThemeProvider'
import { ThemeMode } from '@renderer/store/settings'
import React, { useState } from 'react'
import { useTranslation } from 'react-i18next'
import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter'
import { atomDark, oneLight } from 'react-syntax-highlighter/dist/esm/styles/prism'
import styled from 'styled-components'
import Mermaid from './Mermaid'
interface CodeBlockProps {
children: string
@@ -13,24 +18,34 @@ interface CodeBlockProps {
const CodeBlock: React.FC<CodeBlockProps> = ({ children, className, ...rest }) => {
const match = /language-(\w+)/.exec(className || '')
const [copied, setCopied] = useState(false)
const { theme } = useTheme()
const { t } = useTranslation()
const onCopy = () => {
navigator.clipboard.writeText(children)
window.message.success({ content: t('message.copied'), key: 'copy-code' })
setCopied(true)
setTimeout(() => setCopied(false), 2000)
}
if (match && match[1] === 'mermaid') {
initMermaid(theme)
return <Mermaid chart={children} />
}
return match ? (
<div>
<CodeHeader>
<CodeLanguage>{'<' + match[1].toUpperCase() + '>'}</CodeLanguage>
<CopyOutlined className="copy" onClick={onCopy} />
{!copied && <CopyOutlined className="copy" onClick={onCopy} />}
{copied && <CheckOutlined style={{ color: 'var(--color-primary)' }} />}
</CodeHeader>
<SyntaxHighlighter
{...rest}
language={match[1]}
style={atomDark}
style={theme === ThemeMode.dark ? atomDark : oneLight}
wrapLongLines={true}
customStyle={{ borderTopLeftRadius: 0, borderTopRightRadius: 0, marginTop: 0 }}>
{String(children).replace(/\n$/, '')}
@@ -47,10 +62,10 @@ const CodeHeader = styled.div`
display: flex;
align-items: center;
justify-content: space-between;
color: #fff;
color: var(--color-text);
font-size: 14px;
font-weight: bold;
background-color: #323232;
background-color: var(--color-code-background);
height: 40px;
padding: 0 10px;
border-top-left-radius: 8px;

View File

@@ -0,0 +1,12 @@
import { omit } from 'lodash'
import React from 'react'
const Link: React.FC = (props: React.AnchorHTMLAttributes<HTMLAnchorElement>) => {
if (props.href?.startsWith('#')) {
return <span className="link">{props.children}</span>
}
return <a {...omit(props, 'node')} target="_blank" rel="noreferrer" onClick={(e) => e.stopPropagation()} />
}
export default Link

View File

@@ -0,0 +1,48 @@
import 'katex/dist/katex.min.css'
import { Message } from '@renderer/types'
import { convertMathFormula } from '@renderer/utils'
import { isEmpty } from 'lodash'
import { FC, useMemo } from 'react'
import { useTranslation } from 'react-i18next'
import ReactMarkdown from 'react-markdown'
import rehypeKatex from 'rehype-katex'
import remarkGfm from 'remark-gfm'
import remarkMath from 'remark-math'
import CodeBlock from './CodeBlock'
import Link from './Link'
interface Props {
message: Message
}
const Markdown: FC<Props> = ({ message }) => {
const { t } = useTranslation()
const messageContent = useMemo(() => {
const empty = isEmpty(message.content)
const paused = message.status === 'paused'
const content = empty && paused ? t('message.chat.completion.paused') : message.content
return convertMathFormula(content)
}, [message.content, message.status, t])
return useMemo(() => {
return (
<ReactMarkdown
className="markdown"
rehypePlugins={[rehypeKatex]}
remarkPlugins={[[remarkMath, { singleDollarTextMath: false }], remarkGfm]}
remarkRehypeOptions={{
footnoteLabel: t('common.footnotes'),
footnoteLabelTagName: 'h4',
footnoteBackContent: ' '
}}
components={{ code: CodeBlock as any, a: Link as any }}>
{messageContent}
</ReactMarkdown>
)
}, [messageContent, t])
}
export default Markdown

View File

@@ -0,0 +1,15 @@
import React, { useEffect } from 'react'
interface Props {
chart: string
}
const Mermaid: React.FC<Props> = ({ chart }) => {
useEffect(() => {
window?.mermaid?.contentLoaded()
}, [])
return <div className="mermaid">{chart}</div>
}
export default Mermaid

View File

@@ -0,0 +1,115 @@
import { useShowRightSidebar } from '@renderer/hooks/useStore'
import { EVENT_NAMES, EventEmitter } from '@renderer/services/event'
import { Assistant, Topic } from '@renderer/types'
import { FC, useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next'
import styled from 'styled-components'
import SettingsTab from './SettingsTab'
import TopicsTab from './TopicsTab'
interface Props {
assistant: Assistant
activeTopic: Topic
setActiveTopic: (topic: Topic) => void
}
const RightSidebar: FC<Props> = (props) => {
const [tab, setTab] = useState<'topic' | 'settings'>('topic')
const { rightSidebarShown, showRightSidebar, hideRightSidebar } = useShowRightSidebar()
const { t } = useTranslation()
const isTopicTab = tab === 'topic'
const isSettingsTab = tab === 'settings'
useEffect(() => {
const unsubscribes = [
EventEmitter.on(EVENT_NAMES.SHOW_TOPIC_SIDEBAR, (): any => {
if (rightSidebarShown && isTopicTab) {
return hideRightSidebar()
}
if (rightSidebarShown) {
return setTab('topic')
}
showRightSidebar()
setTab('topic')
}),
EventEmitter.on(EVENT_NAMES.SHOW_CHAT_SETTINGS, (): any => {
if (rightSidebarShown && isSettingsTab) {
return hideRightSidebar()
}
if (rightSidebarShown) {
return setTab('settings')
}
showRightSidebar()
setTab('settings')
}),
EventEmitter.on(EVENT_NAMES.SWITCH_TOPIC_SIDEBAR, () => setTab('topic'))
]
return () => unsubscribes.forEach((unsub) => unsub())
}, [hideRightSidebar, isSettingsTab, isTopicTab, rightSidebarShown, showRightSidebar])
if (!rightSidebarShown) {
return null
}
return (
<Container>
<Tabs>
<Tab className={tab === 'topic' ? 'active' : ''} onClick={() => setTab('topic')}>
{t('common.topics')}
</Tab>
<Tab className={tab === 'settings' ? 'active' : ''} onClick={() => setTab('settings')}>
{t('settings.title')}
</Tab>
</Tabs>
<TabContent>
{tab === 'topic' && <TopicsTab {...props} />}
{tab === 'settings' && <SettingsTab assistant={props.assistant} />}
</TabContent>
</Container>
)
}
const Container = styled.div`
display: flex;
flex-direction: column;
width: var(--topic-list-width);
height: calc(100vh - var(--navbar-height));
border-left: 0.5px solid var(--color-border);
.collapsed {
width: 0;
border-left: none;
}
`
const Tabs = styled.div`
display: flex;
flex-direction: row;
border-bottom: 0.5px solid var(--color-border);
padding: 0 10px;
`
const Tab = styled.div`
padding: 8px 0;
font-weight: 500;
display: flex;
flex: 1;
justify-content: center;
align-items: center;
font-size: 13px;
cursor: pointer;
color: var(--color-text-3);
&.active {
color: var(--color-text-2);
font-weight: 600;
}
`
const TabContent = styled.div`
display: flex;
flex: 1;
flex-direction: column;
overflow-y: auto;
`
export default RightSidebar

View File

@@ -0,0 +1,268 @@
import { QuestionCircleOutlined, ReloadOutlined } from '@ant-design/icons'
import { HStack } from '@renderer/components/Layout'
import { DEFAULT_CONEXTCOUNT, DEFAULT_MAX_TOKENS, DEFAULT_TEMPERATURE } from '@renderer/config/constant'
import { useAssistant } from '@renderer/hooks/useAssistant'
import { useSettings } from '@renderer/hooks/useSettings'
import { SettingDivider, SettingRow, SettingRowTitle, SettingSubtitle } from '@renderer/pages/settings/components'
import { useAppDispatch } from '@renderer/store'
import { setMessageFont, setShowInputEstimatedTokens, setShowMessageDivider } from '@renderer/store/settings'
import { Assistant, AssistantSettings } from '@renderer/types'
import { Col, InputNumber, Row, Slider, Switch, Tooltip } from 'antd'
import { debounce } from 'lodash'
import { FC, useCallback, useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next'
import styled from 'styled-components'
interface Props {
assistant: Assistant
}
const SettingsTab: FC<Props> = (props) => {
const { assistant, updateAssistantSettings, updateAssistant } = useAssistant(props.assistant.id)
const [temperature, setTemperature] = useState(assistant?.settings?.temperature ?? DEFAULT_TEMPERATURE)
const [contextCount, setConextCount] = useState(assistant?.settings?.contextCount ?? DEFAULT_CONEXTCOUNT)
const [enableMaxTokens, setEnableMaxTokens] = useState(assistant?.settings?.enableMaxTokens ?? false)
const [maxTokens, setMaxTokens] = useState(assistant?.settings?.maxTokens ?? 0)
const { t } = useTranslation()
const dispatch = useAppDispatch()
const { showMessageDivider, messageFont, showInputEstimatedTokens } = useSettings()
const onUpdateAssistantSettings = useCallback(
debounce(
(settings: Partial<AssistantSettings>) => {
updateAssistantSettings({
temperature: settings.temperature ?? temperature,
contextCount: settings.contextCount ?? contextCount,
enableMaxTokens: settings.enableMaxTokens ?? enableMaxTokens,
maxTokens: settings.maxTokens ?? maxTokens
})
},
1000,
{ leading: true, trailing: false }
),
[temperature, contextCount, enableMaxTokens, maxTokens]
)
const onTemperatureChange = (value) => {
if (!isNaN(value as number)) {
setTemperature(value)
onUpdateAssistantSettings({ temperature: value })
}
}
const onConextCountChange = (value) => {
if (!isNaN(value as number)) {
setConextCount(value)
onUpdateAssistantSettings({ contextCount: value })
}
}
const onMaxTokensChange = (value) => {
if (!isNaN(value as number)) {
setMaxTokens(value)
onUpdateAssistantSettings({ maxTokens: value })
}
}
const onReset = () => {
setTemperature(DEFAULT_TEMPERATURE)
setConextCount(DEFAULT_CONEXTCOUNT)
updateAssistant({
...assistant,
settings: {
...assistant.settings,
temperature: DEFAULT_TEMPERATURE,
contextCount: DEFAULT_CONEXTCOUNT,
enableMaxTokens: false,
maxTokens: DEFAULT_MAX_TOKENS
}
})
}
useEffect(() => {
setTemperature(assistant?.settings?.temperature ?? DEFAULT_TEMPERATURE)
setConextCount(assistant?.settings?.contextCount ?? DEFAULT_CONEXTCOUNT)
setEnableMaxTokens(assistant?.settings?.enableMaxTokens ?? false)
setMaxTokens(assistant?.settings?.maxTokens ?? DEFAULT_MAX_TOKENS)
}, [assistant])
return (
<Container>
<SettingSubtitle>
{t('settings.messages.model.title')}{' '}
<Tooltip title={t('chat.settings.reset')}>
<ReloadOutlined onClick={onReset} style={{ cursor: 'pointer', fontSize: 12, padding: '0 3px' }} />
</Tooltip>
</SettingSubtitle>
<SettingDivider />
<Row align="middle">
<Label>{t('chat.settings.temperature')}</Label>
<Tooltip title={t('chat.settings.temperature.tip')}>
<QuestionIcon />
</Tooltip>
</Row>
<Row align="middle" gutter={10}>
<Col span={18}>
<Slider
min={0}
max={1.2}
onChange={onTemperatureChange}
value={typeof temperature === 'number' ? temperature : 0}
marks={{ 0: '0', 0.7: '0.7', 1.2: '1.2' }}
step={0.1}
/>
</Col>
<Col span={6}>
<InputNumberic
min={0}
max={1.2}
step={0.1}
value={temperature}
onChange={onTemperatureChange}
controls={false}
size="small"
/>
</Col>
</Row>
<Row align="middle">
<Label>{t('chat.settings.conext_count')}</Label>
<Tooltip title={t('chat.settings.conext_count.tip')}>
<QuestionIcon />
</Tooltip>
</Row>
<Row align="middle" gutter={10}>
<Col span={18}>
<Slider
min={0}
max={20}
marks={{ 0: '0', 10: '10', 20: t('chat.settings.max') }}
onChange={onConextCountChange}
value={typeof contextCount === 'number' ? contextCount : 0}
step={1}
/>
</Col>
<Col span={6}>
<InputNumberic
min={0}
max={20}
step={1}
value={contextCount}
onChange={onConextCountChange}
controls={false}
size="small"
/>
</Col>
</Row>
<Row align="middle" justify="space-between" style={{ marginBottom: 8 }}>
<HStack alignItems="center">
<Label>{t('chat.settings.max_tokens')}</Label>
<Tooltip title={t('chat.settings.max_tokens.tip')}>
<QuestionIcon />
</Tooltip>
</HStack>
<Switch
size="small"
checked={enableMaxTokens}
onChange={(enabled) => {
setEnableMaxTokens(enabled)
onUpdateAssistantSettings({ enableMaxTokens: enabled })
}}
/>
</Row>
{enableMaxTokens && (
<Row align="middle" gutter={10}>
<Col span={16}>
<Slider
min={0}
max={32000}
onChange={onMaxTokensChange}
value={typeof maxTokens === 'number' ? maxTokens : 0}
step={100}
/>
</Col>
<Col span={8}>
<InputNumberic
min={0}
max={32000}
step={100}
value={maxTokens}
onChange={onMaxTokensChange}
controls={true}
style={{ width: '100%' }}
size="small"
/>
</Col>
</Row>
)}
<SettingSubtitle>{t('settings.messages.title')}</SettingSubtitle>
<SettingDivider />
<SettingRow>
<SettingRowTitleSmall>{t('settings.messages.divider')}</SettingRowTitleSmall>
<Switch
size="small"
checked={showMessageDivider}
onChange={(checked) => dispatch(setShowMessageDivider(checked))}
/>
</SettingRow>
<SettingDivider />
<SettingRow>
<SettingRowTitleSmall>{t('settings.messages.use_serif_font')}</SettingRowTitleSmall>
<Switch
size="small"
checked={messageFont === 'serif'}
onChange={(checked) => dispatch(setMessageFont(checked ? 'serif' : 'system'))}
/>
</SettingRow>
<SettingDivider />
<SettingSubtitle style={{ marginTop: 20 }}>{t('settings.messages.input.title')}</SettingSubtitle>
<SettingDivider />
<SettingRow>
<SettingRowTitleSmall>{t('settings.messages.input.show_estimated_tokens')}</SettingRowTitleSmall>
<Switch
size="small"
checked={showInputEstimatedTokens}
onChange={(checked) => dispatch(setShowInputEstimatedTokens(checked))}
/>
</SettingRow>
<SettingDivider />
</Container>
)
}
const Container = styled.div`
display: flex;
flex: 1;
flex-direction: column;
padding: 0 15px;
`
const InputNumberic = styled(InputNumber)`
width: 45px;
padding: 0;
margin-left: 5px;
text-align: center;
.ant-input-number-input {
text-align: center;
}
`
const Label = styled.p`
margin: 0;
font-size: 12px;
font-weight: 600;
margin-right: 8px;
`
const QuestionIcon = styled(QuestionCircleOutlined)`
font-size: 12px;
cursor: pointer;
color: var(--color-text-3);
`
const SettingRowTitleSmall = styled(SettingRowTitle)`
font-size: 13px;
`
export default SettingsTab

Some files were not shown because too many files have changed in this diff Show More