Compare commits

...

189 Commits

Author SHA1 Message Date
kangfenmao
ebf61b1ce9 feat: plugins 2024-12-30 23:45:47 +08:00
kangfenmao
1a68587684 fix: Microsoft Visual C++ Redistributable #577 2024-12-30 15:07:31 +08:00
kangfenmao
47c455b125 feat: 增加保持并发送的功能 #527 2024-12-30 14:09:59 +08:00
kangfenmao
96124cf58e feat: 增加genspark小程序 #578 2024-12-30 13:10:27 +08:00
juzeon
ef975add01 fix: 修复zh-tw语言文件中的乱码 (#579) 2024-12-30 11:49:40 +08:00
n2yt584v2t4nh7y
ed49066bab feat: 添加自定义API参数功能 (#564)
* add custom api parameters

* allow more data types for custom api parameters

* pass parameter to api payload

* add custom parameter settings to sidebar

* remove unnecessary object and array types

* extract API custom parameter method to BaseProvider

* add i18n for custom parameter settings

---------

Co-authored-by: 亢奋猫 <kangfenmao@qq.com>
2024-12-29 20:19:07 +08:00
kangfenmao
e7545c5a94 feat: 用户自定义话题总结Prompt #562
close #562
2024-12-29 10:20:45 +08:00
kangfenmao
fc35df65b8 feat: add release notes pages 2024-12-29 09:49:22 +08:00
littel_penguin66
56ca81d245 Fix incorrect synchronization behavior of webdav auto sync (#568) 2024-12-29 08:44:21 +08:00
kangfenmao
6bc1f4b640 chore(version): 0.9.2 2024-12-27 23:03:17 +08:00
kangfenmao
ccb216e76a fix: 模型名后面标注一下服务商 #557 2024-12-27 18:09:22 +08:00
kangfenmao
60931b85ff fix: model settings params step size 2024-12-27 16:47:44 +08:00
kangfenmao
dc1dbc7bb6 feat: add jina provider 2024-12-27 16:29:17 +08:00
kangfenmao
5d2efbd62b fix: 需要只发送图片功能 #538 2024-12-27 14:40:44 +08:00
sommermorgentraum
5337017648 feat: Add capabilities for user to load custom CSS #548 2024-12-27 14:11:12 +08:00
kangfenmao
c409256ae9 fix: azure openai embedding 2024-12-27 14:02:53 +08:00
kangfenmao
4ac608052c chore: update dependencies and improve project structure 2024-12-27 12:42:17 +08:00
kangfenmao
5e6aaabb23 fix: 小程序中增加 github copilot #547 2024-12-27 12:10:41 +08:00
kangfenmao
8812daeeee fix: 某些输出包含 sub 无法正常显示 #545 2024-12-27 11:54:11 +08:00
kangfenmao
13e3a8478c feat: added topic message update and search state management 2024-12-27 11:48:12 +08:00
kangfenmao
8687985ccb feat: add windows platform support for node file detection and npm package download 2024-12-26 12:38:51 +08:00
kangfenmao
7d54f9b4fa chore(version): 0.9.1 2024-12-26 12:25:58 +08:00
kangfenmao
6b7ba35183 fix: build native module script 2024-12-26 12:25:58 +08:00
kangfenmao
5b42a6d054 feat: add embeding tag to settings 2024-12-26 12:25:58 +08:00
kangfenmao
153e7a9299 refactor: knowledge base engine change to libsql 2024-12-26 10:00:37 +08:00
littel_penguin66
77e0c5172e Add Japanese localization for i18n (#533) 2024-12-25 22:04:29 +08:00
kangfenmao
c50ac440c8 fix: knowledge base bugs 2024-12-25 21:54:46 +08:00
kangfenmao
34ebab0af8 refactor: knowledge base database engine 2024-12-25 17:42:03 +08:00
Tan Xiang
b85765915e fix: shortcut tips (#525) 2024-12-24 23:09:54 +08:00
kangfenmao
960f50e4e4 fix: gemini web serach modal 400 request error 2024-12-24 18:00:25 +08:00
kangfenmao
65e19d187c revert: cloudflare-worker.js 2024-12-24 17:38:30 +08:00
kangfenmao
aa4f94f8a4 build: download npm node native modules 2024-12-24 17:24:38 +08:00
kangfenmao
aa3812eddc fix: linux window title style 2024-12-24 14:43:32 +08:00
kangfenmao
6b9e58171b feat: update models inside 2024-12-24 13:27:40 +08:00
kangfenmao
2f64653b1e fix: knowledge base bugs 2024-12-24 12:41:58 +08:00
kangfenmao
03dd3038e0 patch: @llm-tools 2024-12-24 12:11:07 +08:00
kangfenmao
f1f7e8e11b feat: added webdav auto-sync settings synchronization and custom show message option 2024-12-24 10:25:19 +08:00
kangfenmao
fbd189c5e1 Merge branch 'knowledge' 2024-12-24 09:38:38 +08:00
little_penguin66
87c3716f75 add autoSync in WebDav 2024-12-24 09:34:16 +08:00
kangfenmao
37477587b6 fix: check provider connection use the last model 2024-12-24 09:33:43 +08:00
kangfenmao
d558572d97 chore(version): 0.9.0 2024-12-23 17:07:26 +08:00
kangfenmao
7506d04c55 build: reduce package size 2024-12-23 14:22:37 +08:00
kangfenmao
35fd5aef22 fix: knowledge bugs 2024-12-23 10:48:40 +08:00
kangfenmao
8f11d2b1c9 chore: remove release and build workflow, update release workflow for macos-latest 2024-12-19 17:24:39 +08:00
kangfenmao
9aa2a4727d build: add matrix 2024-12-19 17:20:52 +08:00
kangfenmao
ca6027dd83 feat: remove knowledge queue 2024-12-19 13:45:11 +08:00
kangfenmao
c2462fd51c feat: knowledge base 2024-12-19 09:24:20 +08:00
tanxiang
0739758469 feat(i18n): add "Switch Model" message to multiple locales and update tooltip in MessageMenubar 2024-12-18 13:35:39 +08:00
adfnekc
b2554333a9 feat: message 增加 metrics 字段 用以统计token生成速度和首字时延 (#479) 2024-12-16 17:10:36 +08:00
kangfenmao
6ced973b35 chore(version): 0.8.27 2024-12-16 15:47:07 +08:00
kangfenmao
ccbeefc546 feat: added long text paste control and threshold settings 2024-12-16 15:08:40 +08:00
kangfenmao
7fdc2db522 fix: o1模型支持流式输出 #439 2024-12-16 14:48:51 +08:00
kangfenmao
978f1342e4 feat: disable select menu text 2024-12-16 14:39:54 +08:00
kangfenmao
ff935a656e feat: add copy last message shortcuts 2024-12-16 14:13:59 +08:00
kangfenmao
15539a5609 feat: add thinkany minapp 2024-12-16 13:42:08 +08:00
kangfenmao
88cd4f2144 fix: mermaid图表代码一键复制功能 #460 2024-12-16 13:20:24 +08:00
kangfenmao
daf2e035b2 fix: 输出不显示 markdown 的小圆点 #446 2024-12-16 12:58:31 +08:00
kangfenmao
7ceb4920ec feat: added hotkey functionality and improved appstorepopover layout 2024-12-16 12:55:14 +08:00
kangfenmao
0074d5c8b4 feat: add svg preview 2024-12-16 12:35:39 +08:00
kangfenmao
96737ed695 feat: add display settings 2024-12-16 12:04:12 +08:00
kangfenmao
356da1ea67 feat: add miniapp icon to navbar right 2024-12-16 11:32:50 +08:00
kangfenmao
debf996146 feat: add n.cn to minapp list 2024-12-16 10:48:55 +08:00
kangfenmao
8d73d1e844 fix: input bar default rows #431 2024-12-16 10:26:04 +08:00
duanyongcheng77
b0d777293b feat: 🎸 可以多次点击上传文件按钮上传文件 2024-12-16 09:54:12 +08:00
kangfenmao
1a9fbbc0a2 fix: KaTeX引擎公式渲染错位 #473 2024-12-16 09:42:10 +08:00
kangfenmao
ab99a7b96d chore(version): 0.8.26 2024-12-15 18:03:36 +08:00
kangfenmao
7d561dbfb7 feat: added setshowassistants function to useshowassistants hook and updated error handling logic 2024-12-13 16:37:48 +08:00
kangfenmao
6af07c278d fix: handle unknown models in iswebsearchmodel function 2024-12-13 10:28:09 +08:00
kangfenmao
9c18b851cc build: update electron version 2024-12-13 09:52:18 +08:00
Shelly
b1ebe13b5f feat: 🎸 allowMarkdownLongTextToAutomaticallyWrap (#454)
Co-authored-by: duanyongcheng77 <duanyongcheng77@gmail.com>
2024-12-13 09:51:42 +08:00
kangfenmao
9b258734c4 chore: update dependencies and remove unused code 2024-12-13 09:35:40 +08:00
kangfenmao
25eb97902b chore(version): 0.8.25 2024-12-12 18:24:06 +08:00
kangfenmao
2fae6e4a3e feat: add web search for google gemini modal gemini-2.0-flash-exp 2024-12-12 14:26:52 +08:00
Shelly
f312c5fc40 feat: 🎸 add shortcut for command + enter (#443)
* feat: 🎸 add shortcut for command

* feat: 🎸 only command

---------

Co-authored-by: duanyongcheng77 <duanyongcheng77@gmail.com>
2024-12-12 14:22:41 +08:00
kangfenmao
afa96549a3 styles: use mac style 2024-12-11 20:02:15 +08:00
kangfenmao
6beee78ce8 fix: can not delete last message 2024-12-11 20:01:52 +08:00
kangfenmao
a230ee2c69 chore(version): 0.8.24 2024-12-11 11:41:16 +08:00
kangfenmao
28a27447a5 feat: add new social media translations and links 2024-12-10 20:36:37 +08:00
kangfenmao
408976e5dc feat: add shortcut for assistant and topic show 2024-12-10 20:28:05 +08:00
kangfenmao
7153996c35 fix: reduced message counts for messages component 2024-12-10 19:53:14 +08:00
kangfenmao
73f6a743cd feat: add enter key trigger for translate model prompt 2024-12-10 19:41:50 +08:00
kangfenmao
3b250d7d78 style: improved layout and styling 2024-12-10 19:39:00 +08:00
kangfenmao
272efaf76e feat: add top-p settings #224 2024-12-10 19:24:30 +08:00
kangfenmao
44c64a571a feat: sidebar shadow 2024-12-10 18:07:37 +08:00
kangfenmao
f817d9136b fix: 清除上下文按钮容易误点 #426 2024-12-10 17:23:00 +08:00
kangfenmao
c0f192c6f2 fix: support "ctrl+enter" as send shortcuts #244 2024-12-10 17:09:57 +08:00
kangfenmao
b5a109401c feat: add update info ui 2024-12-10 17:06:29 +08:00
牡丹凤凰
aeff59946c Update models.ts 2024-12-10 05:23:13 +08:00
kangfenmao
21ad4cfecc refactor: improve llmodel group assignment logic and sorting 2024-12-09 11:26:02 +08:00
kangfenmao
4df39179bb fix: escaped special characters in code snippets #419 2024-12-09 09:50:15 +08:00
牡丹凤凰
423fdb6992 Update cloudflare-worker.js 2024-12-07 15:28:57 +08:00
亢奋猫
f66adcd217 Merge pull request #418 from 1355873789/develop
历史消息懒加载
2024-12-07 14:41:48 +08:00
Amatsuka
465bf4006c elfix: Add the grok vision model and fix the incorrect marking of the grok beta model as a visual model. 2024-12-07 14:39:48 +08:00
首都爱护动物协会
14c9cb6001 历史消息懒加载
性能优化
2024-12-07 12:27:16 +08:00
牡丹凤凰
e35d928bcd Merge branch 'kangfenmao:main' into develop 2024-12-07 12:21:15 +08:00
duanyongcheng77
1981f2e648 style: 💄 change chinese-traditional icon to hk 2024-12-06 17:03:28 +08:00
kangfenmao
e5c1791135 chore(version): 0.8.23 2024-12-06 00:12:07 +08:00
kangfenmao
ae1960f5c6 fix: plain text render 2024-12-05 22:33:21 +08:00
kangfenmao
51ca9cb289 fix: check for update ui 2024-12-05 21:38:11 +08:00
kangfenmao
7d2df1a8c5 fix: register shortcut key error 2024-12-05 21:05:26 +08:00
kangfenmao
2757535cf0 feat: added translation support for non-english languages 2024-12-05 09:50:15 +08:00
kangfenmao
243065221d fix: add error handling and logging for shortcut registration failures, remove windows shortcut support 2024-12-04 19:37:37 +08:00
kangfenmao
2a674c169e chore(version): 0.8.22 2024-12-04 14:08:09 +08:00
kangfenmao
100dbc8101 feat: added new issue templates for documentation and question issues 2024-12-04 09:32:52 +08:00
kangfenmao
67d7ccbf10 chore(version): 0.8.21 2024-12-03 20:45:30 +08:00
kangfenmao
a71782abb6 fix: improved error handling with formatted json error messages 2024-12-03 20:42:13 +08:00
kangfenmao
73973ecb7f feat: change re-generage message logic 2024-12-03 20:32:18 +08:00
kangfenmao
368de84440 docs: add chineses issue template 2024-12-03 19:02:41 +08:00
kangfenmao
a170dbd6f0 feat: improved search functionality for agent page 2024-12-03 18:46:57 +08:00
kangfenmao
9b84176a42 wip 2024-12-03 17:45:39 +08:00
kangfenmao
0f36610e23 chore: cleaned up dependencies and database schema 2024-12-03 17:21:51 +08:00
kangfenmao
1e273834b8 fix: translate prompt and translateText funciton 2024-12-03 17:18:18 +08:00
kangfenmao
3b569131a5 chore: add github issue template 2024-12-03 16:06:17 +08:00
kangfenmao
115f111071 chore(version): 0.8.20 2024-12-03 12:52:08 +08:00
kangfenmao
a4d1bcffd9 style: update api key input field formatting and validation 2024-12-03 12:24:25 +08:00
kangfenmao
f5d37a4e53 feat: implement automatic text translation functionality 2024-12-03 11:55:52 +08:00
kangfenmao
a9d4a0885c feat: add MessageErrorBoundary component 2024-12-03 11:15:35 +08:00
kangfenmao
6596497c97 feat: added topic auto-renaming to messages page 2024-12-03 10:57:38 +08:00
kangfenmao
12d8f57dab feat: add enable topic naming settings #399 2024-12-03 10:47:43 +08:00
kangfenmao
7f2f3ad88a refactor: shortcuts feature 2024-12-03 10:23:53 +08:00
kangfenmao
cd3c053f81 feat: add translations and reset functionality for new features 2024-12-02 22:29:18 +08:00
kangfenmao
7dacd58821 feat: add shortcut feature 2024-12-02 18:03:38 +08:00
kangfenmao
744a6ac7cb feat: add translated message content 2024-12-02 13:41:28 +08:00
kangfenmao
2e9041c891 feat: auto translate input text 2024-11-30 22:37:33 +08:00
kangfenmao
3717ff25bf feat: generate random seed for image generation in usepaintings hook 2024-11-30 20:22:07 +08:00
kangfenmao
494d52ac85 fix: update ipc handle to retrieve update information 2024-11-30 20:22:00 +08:00
kangfenmao
22d2ff1518 feat: added command shortcuts for new topic input 2024-11-30 14:18:38 +08:00
kangfenmao
06ae4328ea style: update code block padding and topic position 2024-11-30 12:01:00 +08:00
kangfenmao
8de1197557 feat: add configurable request options to gemini provider 2024-11-26 13:15:25 +08:00
kangfenmao
09e86b35a5 fix: proxy 2024-11-25 15:57:16 +08:00
kangfenmao
76ea170a01 chore(version): 0.8.19 2024-11-25 14:16:33 +08:00
kangfenmao
4cd962b42f fix: 链接失败警告不消失 #382
close #382
2024-11-25 14:14:03 +08:00
kangfenmao
1cae86f93d fix: 模型回答时可以新建对话 #369
close #369
2024-11-25 14:10:09 +08:00
kangfenmao
1171100417 fix: code block copy button missing 2024-11-25 13:56:41 +08:00
kangfenmao
dcf57651fe feat: update input component and add translation features 2024-11-25 13:43:03 +08:00
kangfenmao
603b867a5f fix: auto update assistant model 2024-11-25 12:49:47 +08:00
kangfenmao
e765bf9828 feat: create assistant with search text 2024-11-25 12:30:46 +08:00
kangfenmao
33d5da7325 feat: clear app cache 2024-11-25 11:16:24 +08:00
kangfenmao
383e8255a0 feat: add localization text and model type support #378 2024-11-25 10:31:57 +08:00
kangfenmao
4cb5c128bb feat: add border to list item 2024-11-24 01:35:41 +08:00
kangfenmao
949a13b021 fix: 设置中有两个“代码风格”项 #373 2024-11-23 23:08:27 +08:00
kangfenmao
e6c9cb60dc chore(version): 0.8.18 2024-11-23 14:23:22 +08:00
kangfenmao
ad625b23a7 feat: improve proxy configuration handling 2024-11-23 14:21:08 +08:00
kangfenmao
2e34b79f26 build: add scripts/cloudflare-worker.js 2024-11-22 10:01:54 +08:00
kangfenmao
9288e7b292 feat: update electron configuration and add locale-based language selection #370 2024-11-21 18:14:27 +08:00
kangfenmao
ec703852f8 style: adjust window vibrancy style 2024-11-20 20:18:51 +08:00
kangfenmao
fca9fb0c84 chore(version): 0.8.17 2024-11-20 19:15:33 +08:00
kangfenmao
64c8831530 style: removed dark theme styles, updated search and ui 2024-11-20 19:14:09 +08:00
kangfenmao
75e396ecf0 chore(version): 0.8.16 2024-11-20 18:50:27 +08:00
kangfenmao
697c3b1838 feat: implement new ui features and styles 2024-11-20 17:49:39 +08:00
kangfenmao
efa0f4cbdb Revert "feat: remove model settings from settings tab"
This reverts commit a9aa5a8da0.

# Conflicts:
#	src/renderer/src/pages/home/Tabs/Settings.tsx
2024-11-20 17:38:10 +08:00
kangfenmao
c3414e9b6d feat: add new model visionallowedmodels 2024-11-20 16:40:00 +08:00
kangfenmao
6ba6108f43 style: update styles for improved visual consistency and layout 2024-11-20 16:37:52 +08:00
kangfenmao
c3d007b52c feat: added size attribute to preview and download buttons 2024-11-20 11:57:57 +08:00
kangfenmao
e1494d408f feat: enable auto-update and auto-install of app updates 2024-11-20 11:04:42 +08:00
kangfenmao
cd625430b2 style: adjusted layout and styling for searchpopup component 2024-11-20 11:01:02 +08:00
kangfenmao
aefb08965d chore(version): 0.8.15 2024-11-19 23:46:22 +08:00
kangfenmao
d2dd70000b docs: visual updates to shortcutsettings page 2024-11-19 20:53:12 +08:00
kangfenmao
f0a96bb34c fix: 无法恢复备份文件 #341 2024-11-19 20:30:47 +08:00
kangfenmao
0ec61e1c47 feat: new settings ui 2024-11-19 19:54:18 +08:00
kangfenmao
335ce4963b feat: added data folder 2024-11-19 18:48:57 +08:00
kangfenmao
63ef0d2df1 fix: 编辑消息无效 #313 2024-11-19 15:34:03 +08:00
kangfenmao
c0c0e8ae33 style: update antdesign segmented styles 2024-11-19 15:17:54 +08:00
kangfenmao
771b078df9 feat: adjusted searchpopup dimensions and added assistant generation checks 2024-11-19 15:08:04 +08:00
kangfenmao
64e70ea918 style: update color scheme to darker shades of black 2024-11-19 14:45:22 +08:00
kangfenmao
2d46a4494e feat: add search popup 2024-11-19 14:17:38 +08:00
kangfenmao
9b7e2282fe feat: improved agents page filtering and layout adjustments, added russian translations support 2024-11-19 12:04:01 +08:00
kangfenmao
535b7d0a92 style: update code block ui styling and logic 2024-11-19 11:49:35 +08:00
kangfenmao
223496192d feat: validate html elements in markdown, refactor cursor type, disable markdown input rendering 2024-11-19 11:45:00 +08:00
kangfenmao
db779446f0 chore(version): 0.8.14 2024-11-19 11:13:38 +08:00
kangfenmao
8ef3ef2a8f fix: has unsafe elements 2024-11-19 11:13:16 +08:00
Emmanuel Ferdman
30da183578 docs: update license reference
Signed-off-by: Emmanuel Ferdman <emmanuelferdman@gmail.com>
2024-11-19 09:19:02 +08:00
首都爱护动物协会
49c09f381c Update models.ts
douhbao matching rules update
2024-11-19 04:08:26 +08:00
kangfenmao
c8c58ddcfb feat: auto switch to topics 2024-11-18 17:51:17 +08:00
kangfenmao
5bffb86d4f feat: code collapsible settings 2024-11-18 17:44:33 +08:00
kangfenmao
84fa5b065b wip 2024-11-18 17:16:55 +08:00
kangfenmao
7ecb35dfa7 fix: 缩放快捷键不支持小键盘 #354 2024-11-18 17:05:50 +08:00
kangfenmao
2d7d403b15 fix: 重启之后无法保存界面的缩放 #354 2024-11-18 17:00:26 +08:00
kangfenmao
7342a0afef feat: added proxy settings and handling functionality 2024-11-18 14:05:17 +08:00
kangfenmao
1b8a3885f7 feat: add provider type 2024-11-18 13:04:46 +08:00
kangfenmao
c33c0b20f2 feat: enabled provider after add model 2024-11-18 11:41:17 +08:00
kangfenmao
4f75f29361 feat: added gemini experimental to vision allowed models 2024-11-18 11:26:02 +08:00
injurka
fe00eed7b9 feat: russia locale 2024-11-13 08:21:13 +08:00
kangfenmao
30fa9277ff chore: update release workflow with tag filtering 2024-11-12 19:59:44 +08:00
亢奋猫
a4d8e71916 Merge branch 'main' into develop 2024-11-05 20:50:25 +08:00
首都爱护动物协会
39cf227e42 Match the avatar for the Pixtral model. 2024-11-05 17:26:04 +08:00
首都爱护动物协会
d2cad31db4 add pixtral avatar 2024-11-05 17:22:24 +08:00
首都爱护动物协会
aa864f3876 Add new model and provider avatars. 2024-11-05 16:04:30 +08:00
首都爱护动物协会
77a8b23d76 Add Providers
1. Fix the naming error of the Grok model
2. Add new providers: Grok, Mistral, Jina, Hyperbolic
2024-11-05 15:56:36 +08:00
200 changed files with 14566 additions and 4311 deletions

View File

@@ -2,4 +2,4 @@ node_modules
dist
out
.gitignore
scripts/cloudflare-worker.js

View File

@@ -0,0 +1,73 @@
name: 🐛 错误报告
description: 创建一个报告以帮助我们改进
title: '[错误]: '
labels: ['bug']
body:
- type: markdown
attributes:
value: |
感谢您花时间填写此错误报告!
- type: dropdown
id: platform
attributes:
label: 平台
description: 您正在使用哪个平台?
options:
- Windows
- macOS
- Linux
validations:
required: true
- type: input
id: version
attributes:
label: 版本
description: 您正在运行的 Cherry Studio 版本是什么?
placeholder: 例如 v1.0.0
validations:
required: true
- type: textarea
id: description
attributes:
label: 错误描述
description: 清晰简洁地描述错误是什么
placeholder: 告诉我们发生了什么...
validations:
required: true
- type: textarea
id: reproduction
attributes:
label: 重现步骤
description: 重现行为的步骤
placeholder: |
1. 转到 '...'
2. 点击 '....'
3. 向下滚动到 '....'
4. 看到错误
validations:
required: true
- type: textarea
id: expected
attributes:
label: 预期行为
description: 清晰简洁地描述您期望发生的事情
validations:
required: true
- type: textarea
id: logs
attributes:
label: 相关日志输出
description: 请复制并粘贴任何相关的日志输出
render: shell
- type: textarea
id: additional
attributes:
label: 附加信息
description: 在此添加有关问题的任何其他上下文

View File

@@ -0,0 +1,38 @@
name: 💡 功能建议
description: 为项目提出新的想法
title: '[功能]: '
labels: ['enhancement']
body:
- type: markdown
attributes:
value: |
感谢您花时间提出新的功能建议!
- type: textarea
id: problem
attributes:
label: 您的功能建议是否与某个问题相关?
description: 请简明扼要地描述您遇到的问题
placeholder: 我总是感到沮丧,因为...
validations:
required: true
- type: textarea
id: solution
attributes:
label: 请描述您希望实现的解决方案
description: 请简明扼要地描述您希望发生的情况
validations:
required: true
- type: textarea
id: alternatives
attributes:
label: 请描述您考虑过的其他方案
description: 请简明扼要地描述您考虑过的任何其他解决方案或功能
- type: textarea
id: additional
attributes:
label: 其他补充信息
description: 在此添加任何其他与功能建议相关的上下文或截图

44
.github/ISSUE_TEMPLATE/#2_question.yml vendored Normal file
View File

@@ -0,0 +1,44 @@
name: ❓ 提问
description: 提出一个问题或寻求帮助
title: '[问题]: '
labels: ['question']
body:
- type: markdown
attributes:
value: |
感谢您的提问!请尽可能详细地描述您的问题,这样我们才能更好地帮助您。
- type: textarea
id: question
attributes:
label: 您的问题
description: 请详细描述您的问题
placeholder: 请尽可能清楚地说明您的问题...
validations:
required: true
- type: textarea
id: context
attributes:
label: 相关背景
description: 请提供一些背景信息,帮助我们更好地理解您的问题
placeholder: 例如:使用场景、已尝试的解决方案等
- type: textarea
id: additional
attributes:
label: 补充信息
description: 任何其他相关的信息、截图或代码示例
render: shell
- type: dropdown
id: priority
attributes:
label: 优先级
description: 这个问题对您来说有多紧急?
options:
- 低 (有空再看)
- 中 (希望尽快得到答复)
- 高 (阻碍工作进行)
validations:
required: true

73
.github/ISSUE_TEMPLATE/0_bug_report.yml vendored Normal file
View File

@@ -0,0 +1,73 @@
name: 🐛 Bug Report
description: Create a report to help us improve
title: '[Bug]: '
labels: ['bug']
body:
- type: markdown
attributes:
value: |
Thanks for taking the time to fill out this bug report!
- type: dropdown
id: platform
attributes:
label: Platform
description: What platform are you using?
options:
- Windows
- macOS
- Linux
validations:
required: true
- type: input
id: version
attributes:
label: Version
description: What version of Cherry Studio are you running?
placeholder: e.g. v1.0.0
validations:
required: true
- type: textarea
id: description
attributes:
label: Bug Description
description: A clear and concise description of what the bug is
placeholder: Tell us what happened...
validations:
required: true
- type: textarea
id: reproduction
attributes:
label: Steps To Reproduce
description: Steps to reproduce the behavior
placeholder: |
1. Go to '...'
2. Click on '....'
3. Scroll down to '....'
4. See error
validations:
required: true
- type: textarea
id: expected
attributes:
label: Expected Behavior
description: A clear and concise description of what you expected to happen
validations:
required: true
- type: textarea
id: logs
attributes:
label: Relevant Log Output
description: Please copy and paste any relevant log output
render: shell
- type: textarea
id: additional
attributes:
label: Additional Context
description: Add any other context about the problem here

View File

@@ -0,0 +1,38 @@
name: 💡 Feature Request
description: Suggest an idea for this project
title: '[Feature]: '
labels: ['enhancement']
body:
- type: markdown
attributes:
value: |
Thanks for taking the time to suggest a new feature!
- type: textarea
id: problem
attributes:
label: Is your feature request related to a problem?
description: A clear and concise description of what the problem is
placeholder: I'm always frustrated when...
validations:
required: true
- type: textarea
id: solution
attributes:
label: Describe the solution you'd like
description: A clear and concise description of what you want to happen
validations:
required: true
- type: textarea
id: alternatives
attributes:
label: Describe alternatives you've considered
description: A clear and concise description of any alternative solutions or features you've considered
- type: textarea
id: additional
attributes:
label: Additional Context
description: Add any other context or screenshots about the feature request here

44
.github/ISSUE_TEMPLATE/2_question.yml vendored Normal file
View File

@@ -0,0 +1,44 @@
name: ❓ Question
description: Ask a question or seek help
title: '[Question]: '
labels: ['question']
body:
- type: markdown
attributes:
value: |
Thanks for asking a question! Please provide as much detail as possible so we can better assist you.
- type: textarea
id: question
attributes:
label: Your Question
description: Please describe your question in detail
placeholder: Please explain your question as clearly as possible...
validations:
required: true
- type: textarea
id: context
attributes:
label: Context
description: Please provide some background information to help us better understand your question
placeholder: "For example: use case, solutions you've tried, etc."
- type: textarea
id: additional
attributes:
label: Additional Information
description: Any other relevant information, screenshots, or code examples
render: shell
- type: dropdown
id: priority
attributes:
label: Priority
description: How urgent is this question for you?
options:
- Low (Can wait)
- Medium (Would like a response soon)
- High (Blocking progress)
validations:
required: true

View File

@@ -1,6 +1,7 @@
name: Release
on:
workflow_dispatch:
push:
tags:
- v*.*.*
@@ -28,18 +29,37 @@ jobs:
- name: Install corepack
run: corepack enable && corepack prepare yarn@4.3.1 --activate
- name: Get yarn cache directory path
id: yarn-cache-dir-path
run: echo "dir=$(yarn config get cacheFolder)" >> $GITHUB_OUTPUT
- name: Cache yarn dependencies
uses: actions/cache@v3
with:
path: |
${{ steps.yarn-cache-dir-path.outputs.dir }}
node_modules
key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }}
restore-keys: |
${{ runner.os }}-yarn-
- name: Install Dependencies
run: yarn install
- name: Build Linux
if: matrix.os == 'ubuntu-latest'
run: yarn build:linux
run: |
yarn build:npm linux
yarn build:linux
env:
GH_TOKEN: ${{ secrets.GH_TOKEN }}
- name: Build Mac
if: matrix.os == 'macos-latest'
run: yarn build:mac
run: |
yarn build:npm mac
yarn build:mac
env:
CSC_LINK: ${{ secrets.CSC_LINK }}
CSC_KEY_PASSWORD: ${{ secrets.CSC_KEY_PASSWORD }}
@@ -55,7 +75,7 @@ jobs:
GH_TOKEN: ${{ secrets.GH_TOKEN }}
- name: Replace spaces in filenames
run: node scripts/replaceSpaces.js
run: node scripts/replace-spaces.js
- name: Release
uses: softprops/action-gh-release@v2

1
.gitignore vendored
View File

@@ -36,6 +36,7 @@ node_modules
dist
out
build/icons
stats.html
# ENV
.env

View File

@@ -1,53 +0,0 @@
diff --git a/lib/check-signature.js b/lib/check-signature.js
index 324568af71bcc4372c9f959131ecd24122848c86..677348e0a138ff608b2ac41f592d813b15ee4956 100644
--- a/lib/check-signature.js
+++ b/lib/check-signature.js
@@ -41,16 +41,12 @@ const spawn_1 = require("./spawn");
const debug_1 = __importDefault(require("debug"));
const d = (0, debug_1.default)('electron-notarize');
const codesignDisplay = (opts) => __awaiter(void 0, void 0, void 0, function* () {
- const result = yield (0, spawn_1.spawn)('codesign', ['-dv', '-vvvv', '--deep', path.basename(opts.appPath)], {
- cwd: path.dirname(opts.appPath),
- });
+ const result = yield (0, spawn_1.spawn)('codesign', ['-dv', '-vvvv', '--deep', opts.appPath]);
return result;
});
const codesign = (opts) => __awaiter(void 0, void 0, void 0, function* () {
d('attempting to check codesign of app:', opts.appPath);
- const result = yield (0, spawn_1.spawn)('codesign', ['-vvv', '--deep', '--strict', path.basename(opts.appPath)], {
- cwd: path.dirname(opts.appPath),
- });
+ const result = yield (0, spawn_1.spawn)('codesign', ['-vvv', '--deep', '--strict', opts.appPath]);
return result;
});
function checkSignatures(opts) {
diff --git a/lib/notarytool.js b/lib/notarytool.js
index 1ab090efb2101fc8bee5553445e0349c54474421..a5ddfd922197449fc56078e4a7e9a2ee5d8d207d 100644
--- a/lib/notarytool.js
+++ b/lib/notarytool.js
@@ -92,9 +92,7 @@ function notarizeAndWaitForNotaryTool(opts) {
else {
filePath = path.resolve(dir, `${path.parse(opts.appPath).name}.zip`);
d('zipping application to:', filePath);
- const zipResult = yield (0, spawn_1.spawn)('ditto', ['-c', '-k', '--sequesterRsrc', '--keepParent', path.basename(opts.appPath), filePath], {
- cwd: path.dirname(opts.appPath),
- });
+ const zipResult = yield (0, spawn_1.spawn)('ditto', ['-c', '-k', '--sequesterRsrc', '--keepParent', opts.appPath, filePath]);
if (zipResult.code !== 0) {
throw new Error(`Failed to zip application, exited with code: ${zipResult.code}\n\n${zipResult.output}`);
}
diff --git a/lib/staple.js b/lib/staple.js
index 47dbd85b2fc279d999b57f47fb8171e1cc674436..f8829e6ac54fcd630a730d12d75acc1591b953b6 100644
--- a/lib/staple.js
+++ b/lib/staple.js
@@ -43,9 +43,7 @@ const d = (0, debug_1.default)('electron-notarize:staple');
function stapleApp(opts) {
return __awaiter(this, void 0, void 0, function* () {
d('attempting to staple app:', opts.appPath);
- const result = yield (0, spawn_1.spawn)('xcrun', ['stapler', 'staple', '-v', path.basename(opts.appPath)], {
- cwd: path.dirname(opts.appPath),
- });
+ const result = yield (0, spawn_1.spawn)('xcrun', ['stapler', 'staple', '-v', opts.appPath]);
if (result.code !== 0) {
throw new Error(`Failed to staple your application with code: ${result.code}\n\n${result.output}`);
}

View File

@@ -0,0 +1,25 @@
diff --git a/src/libsql-db.js b/src/libsql-db.js
index 58c42e4910bd0e53bc497ff9b9702b1f7a961266..250bc97c50a9b790e8798441d904d040f2d2af43 100644
--- a/src/libsql-db.js
+++ b/src/libsql-db.js
@@ -41,9 +41,9 @@ export class LibSqlDb {
}
async similaritySearch(query, k) {
const statement = `SELECT id, pageContent, uniqueLoaderId, source, metadata,
- vector_distance_cos(vector, vector32('[${query.join(',')}]'))
+ vector_distance_cos(vector, vector32('[${query.join(',')}]')) as distance
FROM ${this.tableName}
- ORDER BY vector_distance_cos(vector, vector32('[${query.join(',')}]')) ASC
+ ORDER BY distance ASC
LIMIT ${k};`;
this.debug(`Executing statement - ${truncateCenterString(statement, 700)}`);
const results = await this.client.execute(statement);
@@ -52,7 +52,7 @@ export class LibSqlDb {
return {
metadata,
pageContent: result.pageContent.toString(),
- score: 1,
+ score: 1 - result.distance,
};
});
}

View File

@@ -0,0 +1,17 @@
diff --git a/src/core/rag-embedding.js b/src/core/rag-embedding.js
index 50c3c4064af17bc4c7c46554d8f2419b3afceb0e..632c9b2e04d2e0e3bb09ef1cd8f29d2560e6afc1 100644
--- a/src/core/rag-embedding.js
+++ b/src/core/rag-embedding.js
@@ -1,10 +1,8 @@
export class RAGEmbedding {
static singleton;
static async init(embeddingModel) {
- if (!this.singleton) {
- await embeddingModel.init();
- this.singleton = new RAGEmbedding(embeddingModel);
- }
+ await embeddingModel.init();
+ this.singleton = new RAGEmbedding(embeddingModel);
}
static getInstance() {
return RAGEmbedding.singleton;

View File

@@ -0,0 +1,54 @@
diff --git a/src/util/strings.cjs b/src/util/strings.cjs
index 9933cc6e3866c476b47342a29ddb206eb90fa4a5..2965c4f2808bf94af9ef3e2ec889e5552e30e6ae 100644
--- a/src/util/strings.cjs
+++ b/src/util/strings.cjs
@@ -38,13 +38,16 @@ function toTitleCase(str) {
});
}
function isValidURL(url) {
- try {
- new URL(url);
- return true;
- }
- catch {
- return false;
+ if (url.startsWith('http://') || url.startsWith('https://') || url.startsWith('ftp://')) {
+ try {
+ new URL(url);
+ return true;
+ }
+ catch {
+ return false;
+ }
}
+ return false;
}
function isValidJson(str) {
try {
diff --git a/src/util/strings.js b/src/util/strings.js
index f5c1655512099b880fc5022e95d5e0c4d1d073f2..1a64bd662a22efd2effd9d2846ffcf0b93391963 100644
--- a/src/util/strings.js
+++ b/src/util/strings.js
@@ -29,13 +29,16 @@ export function toTitleCase(str) {
});
}
export function isValidURL(url) {
- try {
- new URL(url);
- return true;
- }
- catch {
- return false;
+ if (url.startsWith('http://') || url.startsWith('https://') || url.startsWith('ftp://')) {
+ try {
+ new URL(url);
+ return true;
+ }
+ catch {
+ return false;
+ }
}
+ return false;
}
export function isValidJson(str) {
try {

View File

@@ -1,5 +1,5 @@
diff --git a/core.js b/core.js
index 00b67a48b7b5cf0029413fc84abd0c01630c3d14..5550b58495b468060f775ca86e4d849d82573ea5 100644
index 30c91e66bf595a66c09eb3dbcbda7d58154865f5..b511ff24ea1891904c60174c6ed26ecdd4d5ac51 100644
--- a/core.js
+++ b/core.js
@@ -156,7 +156,7 @@ class APIClient {
@@ -12,7 +12,7 @@ index 00b67a48b7b5cf0029413fc84abd0c01630c3d14..5550b58495b468060f775ca86e4d849d
};
}
diff --git a/core.mjs b/core.mjs
index 8bc7a0ee10d61560d7113cf3f703355bb19f7ddd..5e4c8586ea6b13fe887a22af2de05eaa4700b5ec 100644
index ac267bcfcff44b1f7c9bea5513bba94726a31795..dd5bd9f29609d3f0eea4bd5b225f302893df14ad 100644
--- a/core.mjs
+++ b/core.mjs
@@ -149,7 +149,7 @@ export class APIClient {

View File

@@ -0,0 +1,29 @@
diff --git a/lib/pdf-parse.js b/lib/pdf-parse.js
index 96bfbc705dcb4fb73cb077a75f02c115371b3477..6d02d2bb426063c3a31cb740c3d86841de162a22 100644
--- a/lib/pdf-parse.js
+++ b/lib/pdf-parse.js
@@ -21,12 +21,12 @@ function render_page(pageData) {
for (let item of textContent.items) {
if (lastY == item.transform[5] || !lastY){
text += item.str;
- }
+ }
else{
text += '\n' + item.str;
- }
+ }
lastY = item.transform[5];
- }
+ }
//let strings = textContent.items.map(item => item.str);
//let text = strings.join("\n");
//text = text.replace(/[ ]+/ig," ");
@@ -60,7 +60,7 @@ async function PDF(dataBuffer, options) {
if (typeof options.version != 'string') options.version = DEFAULT_OPTIONS.version;
if (options.version == 'default') options.version = DEFAULT_OPTIONS.version;
- PDFJS = PDFJS ? PDFJS : require(`./pdf.js/${options.version}/build/pdf.js`);
+ PDFJS = PDFJS ? PDFJS : require(`./pdf.js/v1.10.100/build/pdf.js`);
ret.version = PDFJS.version;

47
build/nsis-installer.nsh Normal file
View File

@@ -0,0 +1,47 @@
;Inspired by:
; https://gist.github.com/bogdibota/062919938e1ed388b3db5ea31f52955c
; https://stackoverflow.com/questions/34177547/detect-if-visual-c-redistributable-for-visual-studio-2013-is-installed
; https://stackoverflow.com/a/54391388
; https://github.com/GitCommons/cpp-redist-nsis/blob/main/installer.nsh
;Find latests downloads here:
; https://learn.microsoft.com/en-us/cpp/windows/latest-supported-vc-redist
!include LogicLib.nsh
; https://github.com/electron-userland/electron-builder/issues/1122
!ifndef BUILD_UNINSTALLER
Function checkVCRedist
ReadRegDWORD $0 HKLM "SOFTWARE\Microsoft\VisualStudio\14.0\VC\Runtimes\x64" "Installed"
FunctionEnd
!endif
!macro customInit
Push $0
Call checkVCRedist
${If} $0 != "1"
MessageBox MB_YESNO "\
NOTE: ${PRODUCT_NAME} requires $\r$\n\
'Microsoft Visual C++ Redistributable'$\r$\n\
to function properly.$\r$\n$\r$\n\
Download and install now?" /SD IDYES IDYES InstallVCRedist IDNO DontInstall
InstallVCRedist:
inetc::get /CAPTION " " /BANNER "Downloading Microsoft Visual C++ Redistributable..." "https://aka.ms/vs/17/release/vc_redist.x64.exe" "$TEMP\vc_redist.x64.exe"
ExecWait "$TEMP\vc_redist.x64.exe /install /norestart"
;IfErrors InstallError ContinueInstall ; vc_redist exit code is unreliable :(
Call checkVCRedist
${If} $0 == "1"
Goto ContinueInstall
${EndIf}
;InstallError:
MessageBox MB_ICONSTOP "\
There was an unexpected error installing$\r$\n\
Microsoft Visual C++ Redistributable.$\r$\n\
The installation of ${PRODUCT_NAME} cannot continue."
DontInstall:
Abort
${EndIf}
ContinueInstall:
Pop $0
!macroend

View File

@@ -1,6 +1,8 @@
# provider: generic
# url: http://127.0.0.1:8080
# updaterCacheDirName: cherry-studio-updater
provider: github
repo: cherry-studio
owner: kangfenmao
# provider: github
# repo: cherry-studio
# owner: kangfenmao
provider: generic
url: https://cherrystudio.ocool.online

View File

@@ -133,7 +133,7 @@ Cherry Studioへの貢献を歓迎します以下の方法で貢献できま
# 📃 ライセンス
[LICENSE](./LICENSE)
[LICENSE](../LICENSE)
# ⭐️ スター履歴

View File

@@ -134,7 +134,7 @@ $ yarn build:linux
# 📃 许可证
[LICENSE](./LICENSE)
[LICENSE](../LICENSE)
# ⭐️ Star 记录

View File

@@ -11,8 +11,25 @@ files:
- '!src'
- '!scripts'
- '!local'
- '!docs'
- '!packages'
- '!stats.html'
- '!*.md'
- '!**/*.{map,ts,tsx,jsx,less,scss,sass,css.d.ts,d.cts,d.mts,md,markdown,yaml,yml}'
- '!**/{test,tests,__tests__,coverage}/**'
- '!**/*.{spec,test}.{js,jsx,ts,tsx}'
- '!**/*.min.*.map'
- '!**/*.d.ts'
- '!**/{.DS_Store,Thumbs.db}'
- '!**/{LICENSE,LICENSE.txt,LICENSE-MIT.txt,*.LICENSE.txt,NOTICE.txt,README.md,CHANGELOG.md}'
- '!node_modules/rollup-plugin-visualizer'
- '!node_modules/js-tiktoken'
- '!node_modules/pdf-parse/lib/pdf.js/{v1.9.426,v1.10.88,v2.0.550}'
- '!node_modules/mammoth/{mammoth.browser.js,mammoth.browser.min.js}'
- '!node_modules/html2canvas/dist/{html2canvas.min.js,html2canvas.esm.js}'
asarUnpack:
- resources/**
- '**/*.{node,dll,metal,exp,lib}'
win:
executableName: Cherry Studio
nsis:
@@ -22,14 +39,15 @@ nsis:
createDesktopShortcut: always
allowToChangeInstallationDirectory: true
oneClick: false
include: build/nsis-installer.nsh
mac:
entitlementsInherit: build/entitlements.mac.plist
notarize: false
extendInfo:
- NSCameraUsageDescription: Application requests access to the device's camera.
- NSMicrophoneUsageDescription: Application requests access to the device's microphone.
- NSDocumentsFolderUsageDescription: Application requests access to the user's Documents folder.
- NSDownloadsFolderUsageDescription: Application requests access to the user's Downloads folder.
notarize: false
target:
- target: dmg
arch:
@@ -47,26 +65,17 @@ linux:
arch:
- arm64
- x64
# - snap
# - deb
maintainer: electronjs.org
category: Utility
appImage:
artifactName: ${productName}-${version}-${arch}.${ext}
npmRebuild: false
publish:
provider: github
repo: cherry-studio
owner: kangfenmao
provider: generic
url: https://cherrystudio.ocool.online
electronDownload:
mirror: https://npmmirror.com/mirrors/electron/
afterPack: scripts/after-pack.js
afterSign: scripts/notarize.js
releaseInfo:
releaseNotes: |
增加最小化到托盘功能 @injurka
托盘图标使用彩色图标
修复 Mermaid png 格式图表无法下载问题
选择模型弹窗支持固定一些常用模型
默认助手合并到模型设置里
设置面板移除模型相关设置项
修复一些视觉模型不能识别问题
增加 Genspark 小程序

View File

@@ -1,14 +1,40 @@
import react from '@vitejs/plugin-react'
import { defineConfig, externalizeDepsPlugin } from 'electron-vite'
import { resolve } from 'path'
import { visualizer } from 'rollup-plugin-visualizer'
const visualizerPlugin = (type: 'renderer' | 'main') => {
return process.env[`VISUALIZER_${type.toUpperCase()}`] ? [visualizer({ open: true })] : []
}
export default defineConfig({
main: {
plugins: [externalizeDepsPlugin()],
plugins: [
externalizeDepsPlugin({
exclude: [
'@llm-tools/embedjs',
'@llm-tools/embedjs-openai',
'@llm-tools/embedjs-loader-web',
'@llm-tools/embedjs-loader-markdown',
'@llm-tools/embedjs-loader-msoffice',
'@llm-tools/embedjs-loader-xml',
'@llm-tools/embedjs-loader-pdf',
'@llm-tools/embedjs-loader-sitemap',
'@llm-tools/embedjs-libsql'
]
}),
...visualizerPlugin('main')
],
resolve: {
alias: {
'@main': resolve('src/main'),
'@types': resolve('src/renderer/src/types'),
'@main': resolve('src/main')
'@shared': resolve('packages/shared')
}
},
build: {
rollupOptions: {
external: ['@libsql/client']
}
}
},
@@ -16,11 +42,15 @@ export default defineConfig({
plugins: [externalizeDepsPlugin()]
},
renderer: {
plugins: [react(), ...visualizerPlugin('renderer')],
resolve: {
alias: {
'@renderer': resolve('src/renderer/src')
'@renderer': resolve('src/renderer/src'),
'@shared': resolve('packages/shared')
}
},
plugins: [react()]
optimizeDeps: {
exclude: ['chunk-QH6N6I7P.js', 'chunk-PB73W2YU.js']
}
}
})

View File

@@ -1,6 +1,6 @@
{
"name": "CherryStudio",
"version": "0.8.13",
"version": "0.9.2",
"private": true,
"description": "A powerful AI assistant for producer.",
"main": "./out/main/index.js",
@@ -11,9 +11,11 @@
"local",
"packages/*"
],
"nohoist": [
"packages/database"
]
"installConfig": {
"hoistingLimits": [
"packages/database"
]
}
},
"scripts": {
"format": "prettier --write .",
@@ -23,22 +25,44 @@
"typecheck": "npm run typecheck:node && npm run typecheck:web",
"start": "electron-vite preview",
"dev": "electron-vite dev",
"build:check": "yarn typecheck",
"build": "npm run typecheck && electron-vite build",
"postinstall": "electron-builder install-app-deps",
"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:win": "dotenv npm run build && electron-builder --win",
"build:win:x64": "dotenv npm run build && electron-builder --win --x64",
"build:mac": "dotenv electron-vite build && electron-builder --mac",
"build:mac:arm64": "dotenv electron-vite build && electron-builder --mac --arm64",
"build:mac:x64": "dotenv electron-vite build && electron-builder --mac --x64",
"build:linux": "dotenv electron-vite build && electron-builder --linux",
"build:linux:arm64": "dotenv electron-vite build && electron-builder --linux --arm64",
"build:linux:x64": "dotenv electron-vite build && electron-builder --linux --x64",
"build:npm": "node scripts/build-npm.js",
"release": "node scripts/version.js",
"publish": "yarn release patch push",
"pulish:artifacts": "cd packages/artifacts && npm publish && cd -",
"generate:agents": "yarn workspace @cherry-studio/database agents",
"generate:icons": "electron-icon-builder --input=./build/logo.png --output=build"
"generate:icons": "electron-icon-builder --input=./build/logo.png --output=build",
"analyze:renderer": "VISUALIZER_RENDERER=true yarn build",
"analyze:main": "VISUALIZER_MAIN=true yarn build"
},
"dependencies": {
"@electron-toolkit/preload": "^3.0.0",
"@electron-toolkit/utils": "^3.0.0",
"archiver": "^7.0.1",
"@electron/notarize": "^2.5.0",
"@llm-tools/embedjs": "patch:@llm-tools/embedjs@npm%3A0.1.25#~/.yarn/patches/@llm-tools-embedjs-npm-0.1.25-ec5645cf36.patch",
"@llm-tools/embedjs-libsql": "patch:@llm-tools/embedjs-libsql@npm%3A0.1.25#~/.yarn/patches/@llm-tools-embedjs-libsql-npm-0.1.25-fad000d74c.patch",
"@llm-tools/embedjs-loader-csv": "^0.1.25",
"@llm-tools/embedjs-loader-markdown": "^0.1.25",
"@llm-tools/embedjs-loader-msoffice": "^0.1.25",
"@llm-tools/embedjs-loader-pdf": "^0.1.25",
"@llm-tools/embedjs-loader-sitemap": "^0.1.25",
"@llm-tools/embedjs-loader-web": "^0.1.25",
"@llm-tools/embedjs-loader-xml": "^0.1.25",
"@llm-tools/embedjs-openai": "^0.1.25",
"@types/react-infinite-scroll-component": "^5.0.0",
"adm-zip": "^0.5.16",
"apache-arrow": "^18.1.0",
"docx": "^9.0.2",
"electron-log": "^5.1.5",
"electron-store": "^8.2.0",
@@ -48,7 +72,7 @@
"html2canvas": "^1.4.1",
"markdown-it": "^14.1.0",
"officeparser": "^4.1.1",
"unzipper": "^0.12.3",
"tokenx": "^0.4.1",
"webdav": "4.11.4"
},
"devDependencies": {
@@ -56,31 +80,32 @@
"@electron-toolkit/eslint-config-prettier": "^2.0.0",
"@electron-toolkit/eslint-config-ts": "^1.0.1",
"@electron-toolkit/tsconfig": "^1.0.1",
"@google/generative-ai": "^0.16.0",
"@google/generative-ai": "^0.21.0",
"@hello-pangea/dnd": "^16.6.0",
"@kangfenmao/keyv-storage": "^0.1.0",
"@reduxjs/toolkit": "^2.2.5",
"@types/adm-zip": "^0",
"@types/fs-extra": "^11",
"@types/lodash": "^4.17.5",
"@types/markdown-it": "^14",
"@types/node": "^18.19.9",
"@types/react": "^18.2.48",
"@types/react-dom": "^18.2.18",
"@types/react-infinite-scroll-component": "^5.0.0",
"@types/tinycolor2": "^1",
"@types/unzipper": "^0",
"@vitejs/plugin-react": "^4.2.1",
"antd": "^5.18.3",
"axios": "^1.7.3",
"antd": "^5.22.5",
"axios": "^1.7.9",
"browser-image-compression": "^2.0.2",
"dayjs": "^1.11.11",
"dexie": "^4.0.8",
"dexie-react-hooks": "^1.1.7",
"dotenv-cli": "^7.4.2",
"electron": "^28.3.3",
"electron-builder": "^24.9.1",
"electron": "31.7.6",
"electron-builder": "^24.13.3",
"electron-devtools-installer": "^3.2.0",
"electron-icon-builder": "^2.0.1",
"electron-vite": "^2.0.0",
"electron-vite": "^2.3.0",
"emittery": "^1.0.3",
"emoji-picker-element": "^1.22.1",
"eslint": "^8.56.0",
@@ -88,16 +113,16 @@
"eslint-plugin-react-hooks": "^4.6.2",
"eslint-plugin-simple-import-sort": "^12.1.1",
"eslint-plugin-unused-imports": "^4.0.0",
"gpt-tokens": "^1.3.10",
"i18next": "^23.11.5",
"localforage": "^1.10.0",
"lodash": "^4.17.21",
"mime": "^4.0.4",
"openai": "patch:openai@npm%3A4.71.1#~/.yarn/patches/openai-npm-4.71.1-b5940d6401.patch",
"openai": "patch:openai@npm%3A4.76.2#~/.yarn/patches/openai-npm-4.76.2-8ff1374617.patch",
"prettier": "^3.2.4",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-hotkeys-hook": "^4.6.1",
"react-i18next": "^14.1.2",
"react-infinite-scroll-component": "^6.1.0",
"react-markdown": "^9.0.1",
"react-redux": "^9.1.2",
"react-router": "6",
@@ -110,6 +135,7 @@
"rehype-raw": "^7.0.0",
"remark-gfm": "^4.0.0",
"remark-math": "^6.0.0",
"rollup-plugin-visualizer": "^5.12.0",
"sass": "^1.77.2",
"shiki": "^1.22.2",
"styled-components": "^6.1.11",
@@ -123,7 +149,8 @@
"react-dom": "^17.0.0 || ^18.0.0"
},
"resolutions": {
"@electron/notarize@npm:2.2.1": "patch:@electron/notarize@npm%3A2.3.2#~/.yarn/patches/@electron-notarize-npm-2.3.2-535908a4bd.patch"
"pdf-parse@npm:1.1.1": "patch:pdf-parse@npm%3A1.1.1#~/.yarn/patches/pdf-parse-npm-1.1.1-04a6109b2a.patch",
"@llm-tools/embedjs-utils@npm:0.1.25": "patch:@llm-tools/embedjs-utils@npm%3A0.1.25#~/.yarn/patches/@llm-tools-embedjs-utils-npm-0.1.25-fd8fe8a193.patch"
},
"packageManager": "yarn@4.5.0"
}

View File

@@ -0,0 +1,115 @@
export const imageExts = ['.jpg', '.jpeg', '.png', '.gif', '.bmp', '.webp']
export const videoExts = ['.mp4', '.avi', '.mov', '.wmv', '.flv', '.mkv']
export const audioExts = ['.mp3', '.wav', '.ogg', '.flac', '.aac']
export const documentExts = ['.pdf', '.docx', '.pptx', '.xlsx', '.odt', '.odp', '.ods']
export const textExts = [
'.txt', // 普通文本文件
'.md', // Markdown 文件
'.mdx', // Markdown 文件
'.html', // HTML 文件
'.htm', // HTML 文件的另一种扩展名
'.xml', // XML 文件
'.json', // JSON 文件
'.yaml', // YAML 文件
'.yml', // YAML 文件的另一种扩展名
'.csv', // 逗号分隔值文件
'.tsv', // 制表符分隔值文件
'.ini', // 配置文件
'.log', // 日志文件
'.rtf', // 富文本格式文件
'.tex', // LaTeX 文件
'.srt', // 字幕文件
'.xhtml', // XHTML 文件
'.nfo', // 信息文件(主要用于场景发布)
'.conf', // 配置文件
'.config', // 配置文件
'.env', // 环境变量文件
'.rst', // reStructuredText 文件
'.php', // PHP 脚本文件,包含嵌入的 HTML
'.js', // JavaScript 文件(部分是文本,部分可能包含代码)
'.ts', // TypeScript 文件
'.jsp', // JavaServer Pages 文件
'.aspx', // ASP.NET 文件
'.bat', // Windows 批处理文件
'.sh', // Unix/Linux Shell 脚本文件
'.py', // Python 脚本文件
'.rb', // Ruby 脚本文件
'.pl', // Perl 脚本文件
'.sql', // SQL 脚本文件
'.css', // Cascading Style Sheets 文件
'.less', // Less CSS 预处理器文件
'.scss', // Sass CSS 预处理器文件
'.sass', // Sass 文件
'.styl', // Stylus CSS 预处理器文件
'.coffee', // CoffeeScript 文件
'.ino', // Arduino 代码文件
'.asm', // Assembly 语言文件
'.go', // Go 语言文件
'.scala', // Scala 语言文件
'.swift', // Swift 语言文件
'.kt', // Kotlin 语言文件
'.rs', // Rust 语言文件
'.lua', // Lua 语言文件
'.groovy', // Groovy 语言文件
'.dart', // Dart 语言文件
'.hs', // Haskell 语言文件
'.clj', // Clojure 语言文件
'.cljs', // ClojureScript 语言文件
'.elm', // Elm 语言文件
'.erl', // Erlang 语言文件
'.ex', // Elixir 语言文件
'.exs', // Elixir 脚本文件
'.pug', // Pug (formerly Jade) 模板文件
'.haml', // Haml 模板文件
'.slim', // Slim 模板文件
'.tpl', // 模板文件(通用)
'.ejs', // Embedded JavaScript 模板文件
'.hbs', // Handlebars 模板文件
'.mustache', // Mustache 模板文件
'.jade', // Jade 模板文件 (已重命名为 Pug)
'.twig', // Twig 模板文件
'.blade', // Blade 模板文件 (Laravel)
'.vue', // Vue.js 单文件组件
'.jsx', // React JSX 文件
'.tsx', // React TSX 文件
'.graphql', // GraphQL 查询语言文件
'.gql', // GraphQL 查询语言文件
'.proto', // Protocol Buffers 文件
'.thrift', // Thrift 文件
'.toml', // TOML 配置文件
'.edn', // Clojure 数据表示文件
'.cake', // CakePHP 配置文件
'.ctp', // CakePHP 视图文件
'.cfm', // ColdFusion 标记语言文件
'.cfc', // ColdFusion 组件文件
'.m', // Objective-C 源文件
'.mm', // Objective-C++ 源文件
'.gradle', // Gradle 构建文件
'.groovy', // Gradle 构建文件
'.kts', // Kotlin Script 文件
'.java' // Java 代码文件
]
export const ZOOM_SHORTCUTS = [
{
key: 'zoom_in',
shortcut: ['CommandOrControl', '='],
editable: false,
enabled: true,
system: true
},
{
key: 'zoom_out',
shortcut: ['CommandOrControl', '-'],
editable: false,
enabled: true,
system: true
},
{
key: 'zoom_reset',
shortcut: ['CommandOrControl', '0'],
editable: false,
enabled: true,
system: true
}
]

View File

@@ -0,0 +1,202 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Github Releases Timeline</title>
<link href="https://unpkg.com/tailwindcss@^2/dist/tailwind.min.css" rel="stylesheet" />
<script src="https://unpkg.com/vue@3/dist/vue.global.prod.js"></script>
<script src="https://cdn.jsdelivr.net/npm/markdown-it@13.0.1/dist/markdown-it.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/@tailwindcss/typography@0.5.10/dist/typography.min.css"></script>
</head>
<body id="app">
<div :class="isDark ? 'dark-bg' : 'bg'" class="min-h-screen">
<div class="max-w-3xl mx-auto py-12 px-4">
<h1 class="text-3xl font-bold mb-8" :class="isDark ? 'text-white' : 'text-gray-900'">Release Timeline</h1>
<!-- Loading状态 -->
<div v-if="loading" class="text-center py-8">
<div class="inline-block animate-spin rounded-full h-8 w-8 border-4"
:class="isDark ? 'border-gray-700 border-t-blue-500' : 'border-gray-300 border-t-blue-500'"></div>
</div>
<!-- Error 状态 -->
<div v-else-if="error" class="text-red-500 text-center py-8">{{ error }}</div>
<!-- Release 列表 -->
<div v-else class="space-y-8">
<div v-for="release in releases" :key="release.id" class="relative pl-8"
:class="isDark ? 'border-l-2 border-gray-700' : 'border-l-2 border-gray-200'">
<div class="absolute -left-2 top-0 w-4 h-4 rounded-full bg-green-500"></div>
<div class="rounded-lg shadow-sm p-6 transition-shadow"
:class="isDark ? 'bg-black hover:shadow-md hover:shadow-black' : 'bg-white hover:shadow-md'">
<div class="flex items-start justify-between mb-4">
<div>
<h2 class="text-xl font-semibold" :class="isDark ? 'text-white' : 'text-gray-900'">
{{ release.name || release.tag_name }}
</h2>
<p class="text-sm mt-1" :class="isDark ? 'text-gray-400' : 'text-gray-500'">
{{ formatDate(release.published_at) }}
</p>
</div>
<span class="inline-flex items-center px-3 py-1 rounded-full text-sm font-medium"
:class="isDark ? 'bg-green-900 text-green-200' : 'bg-green-100 text-green-800'">
{{ release.tag_name }}
</span>
</div>
<div class="prose" :class="isDark ? 'text-gray-300 dark-prose' : 'text-gray-600'"
v-html="renderMarkdown(release.body)"></div>
</div>
</div>
</div>
</div>
</div>
<script>
const md = window.markdownit({
breaks: true,
linkify: true
})
const { createApp } = Vue
createApp({
data() {
return {
releases: [],
loading: true,
error: null,
isDark: false
}
},
methods: {
async fetchReleases() {
try {
this.loading = true
this.error = null
const response = await fetch('https://api.github.com/repos/kangfenmao/cherry-studio/releases')
if (!response.ok) {
throw new Error('Failed to fetch releases')
}
this.releases = await response.json()
} catch (err) {
this.error = 'Error loading releases: ' + err.message
} finally {
this.loading = false
}
},
formatDate(dateString) {
return new Date(dateString).toLocaleDateString('en-US', {
year: 'numeric',
month: 'long',
day: 'numeric'
})
},
renderMarkdown(content) {
if (!content) return ''
return md.render(content)
},
initTheme() {
// 从 URL 参数获取主题设置
const url = new URL(window.location.href)
const theme = url.searchParams.get('theme')
this.isDark = theme === 'dark'
}
},
mounted() {
this.initTheme()
this.fetchReleases()
}
}).mount('#app')
</script>
<style>
/* 基础的 Markdown 样式 */
.prose {
line-height: 1.6;
}
.prose h1 {
font-size: 1.5em;
margin: 1em 0;
}
.prose h2 {
font-size: 1.3em;
margin: 0.8em 0;
}
.prose h3 {
font-size: 1.1em;
margin: 0.6em 0;
}
.prose ul {
list-style-type: disc;
margin-left: 1.5em;
margin-bottom: 1em;
}
.prose ol {
list-style-type: decimal;
margin-left: 1.5em;
margin-bottom: 1em;
}
.prose code {
padding: 0.2em 0.4em;
border-radius: 0.2em;
font-size: 0.9em;
}
.dark .prose code {
background-color: #1f2937;
}
.prose code {
background-color: #f3f4f6;
}
.prose pre code {
display: block;
padding: 1em;
overflow-x: auto;
}
.prose a {
color: #3b82f6;
text-decoration: underline;
}
.dark .prose a {
color: #60a5fa;
}
.prose blockquote {
border-left: 4px solid #e5e7eb;
padding-left: 1em;
margin: 1em 0;
}
.dark .prose blockquote {
border-left-color: #374151;
color: #9ca3af;
}
.dark .prose {
color: #e5e7eb;
}
.dark-bg {
background-color: #151515;
}
.bg {
background-color: #f2f2f2;
}
</style>
</body>
</html>

45
scripts/after-pack.js Normal file
View File

@@ -0,0 +1,45 @@
const { Arch } = require('electron-builder')
const { default: removeLocales } = require('./remove-locales')
const fs = require('fs')
const path = require('path')
exports.default = async function (context) {
await removeLocales(context)
const platform = context.packager.platform.name
const arch = context.arch
if (platform === 'mac') {
const node_modules_path = path.join(
context.appOutDir,
'Cherry Studio.app',
'Contents',
'Resources',
'app.asar.unpacked',
'node_modules'
)
removeDifferentArchNodeFiles(node_modules_path, '@libsql', arch === Arch.arm64 ? ['darwin-arm64'] : ['darwin-x64'])
}
if (platform === 'linux') {
const node_modules_path = path.join(context.appOutDir, 'resources', 'app.asar.unpacked', 'node_modules')
const _arch = arch === Arch.arm64 ? ['linux-arm64-gnu', 'linux-arm64-musl'] : ['linux-x64-gnu', 'linux-x64-musl']
removeDifferentArchNodeFiles(node_modules_path, '@libsql', _arch)
}
if (platform === 'windows') {
const node_modules_path = path.join(context.appOutDir, 'resources', 'app.asar.unpacked', 'node_modules')
removeDifferentArchNodeFiles(node_modules_path, '@libsql', ['win32-x64-msvc'])
}
}
function removeDifferentArchNodeFiles(nodeModulesPath, packageName, arch) {
const modulePath = path.join(nodeModulesPath, packageName)
const dirs = fs.readdirSync(modulePath)
dirs
.filter((dir) => !arch.includes(dir))
.forEach((dir) => {
fs.rmSync(path.join(modulePath, dir), { recursive: true, force: true })
console.log(`Removed dir: ${dir}`, arch)
})
}

40
scripts/build-npm.js Normal file
View File

@@ -0,0 +1,40 @@
const { downloadNpmPackage } = require('./utils')
async function downloadNpm(platform) {
if (!platform || platform === 'mac') {
downloadNpmPackage(
'@libsql/darwin-arm64',
'https://registry.npmjs.org/@libsql/darwin-arm64/-/darwin-arm64-0.4.7.tgz'
)
downloadNpmPackage('@libsql/darwin-x64', 'https://registry.npmjs.org/@libsql/darwin-x64/-/darwin-x64-0.4.7.tgz')
}
if (!platform || platform === 'linux') {
downloadNpmPackage(
'@libsql/linux-arm64-gnu',
'https://registry.npmjs.org/@libsql/linux-arm64-gnu/-/linux-arm64-gnu-0.4.7.tgz'
)
downloadNpmPackage(
'@libsql/linux-arm64-musl',
'https://registry.npmjs.org/@libsql/linux-arm64-musl/-/linux-arm64-musl-0.4.7.tgz'
)
downloadNpmPackage(
'@libsql/linux-x64-gnu',
'https://registry.npmjs.org/@libsql/linux-x64-gnu/-/linux-x64-gnu-0.4.7.tgz'
)
downloadNpmPackage(
'@libsql/linux-x64-musl',
'https://registry.npmjs.org/@libsql/linux-x64-musl/-/linux-x64-musl-0.4.7.tgz'
)
}
if (!platform || platform === 'windows') {
downloadNpmPackage(
'@libsql/win32-x64-msvc',
'https://registry.npmjs.org/@libsql/win32-x64-msvc/-/win32-x64-msvc-0.4.7.tgz'
)
}
}
const platformArg = process.argv[2]
downloadNpm(platformArg)

View File

@@ -0,0 +1,529 @@
// 配置信息
const config = {
R2_CUSTOM_DOMAIN: 'cherrystudio.ocool.online',
R2_BUCKET_NAME: 'cherrystudio',
// 缓存键名
CACHE_KEY: 'cherry-studio-latest-release',
VERSION_DB: 'versions.json',
LOG_FILE: 'logs.json',
MAX_LOGS: 1000 // 最多保存多少条日志
}
// Worker 入口函数
const worker = {
// 定时器触发配置
scheduled: {
cron: '*/1 * * * *' // 每分钟执行一次
},
// 定时器执行函数 - 只负责检查和更新
async scheduled(event, env, ctx) {
try {
await initDataFiles(env)
console.log('开始定时检查新版本...')
// 使用新的 checkNewRelease 函数
await checkNewRelease(env)
} catch (error) {
console.error('定时任务执行失败:', error)
}
},
// HTTP 请求处理函数 - 只负责返回数据
async fetch(request, env, ctx) {
if (!env || !env.R2_BUCKET) {
return new Response(
JSON.stringify({
error: 'R2 存储桶未正确配置'
}),
{
status: 500,
headers: { 'Content-Type': 'application/json' }
}
)
}
const url = new URL(request.url)
const filename = url.pathname.slice(1)
try {
// 处理文件下载请求
if (filename) {
return await handleDownload(env, filename)
}
// 只返回缓存的版本信息
return await getCachedRelease(env)
} catch (error) {
return new Response(
JSON.stringify({
error: error.message,
stack: error.stack
}),
{
status: 500,
headers: { 'Content-Type': 'application/json' }
}
)
}
}
}
export default worker
/**
* 添加日志记录函数
*/
async function addLog(env, type, event, details = null) {
try {
const logFile = await env.R2_BUCKET.get(config.LOG_FILE)
let logs = { logs: [] }
if (logFile) {
logs = JSON.parse(await logFile.text())
}
logs.logs.unshift({
timestamp: new Date().toISOString(),
type,
event,
details
})
// 保持日志数量在限制内
if (logs.logs.length > config.MAX_LOGS) {
logs.logs = logs.logs.slice(0, config.MAX_LOGS)
}
await env.R2_BUCKET.put(config.LOG_FILE, JSON.stringify(logs, null, 2))
} catch (error) {
console.error('写入日志失败:', error)
}
}
/**
* 获取最新版本信息
*/
async function getLatestRelease(env) {
try {
const cached = await env.R2_BUCKET.get(config.CACHE_KEY)
if (!cached) {
// 如果缓存不存在,先检查版本数据库
const versionDB = await env.R2_BUCKET.get(config.VERSION_DB)
if (versionDB) {
const versions = JSON.parse(await versionDB.text())
if (versions.latestVersion) {
// 从版本数据库重建缓存
const latestVersion = versions.versions[versions.latestVersion]
const cacheData = {
version: latestVersion.version,
publishedAt: latestVersion.publishedAt,
changelog: latestVersion.changelog,
downloads: latestVersion.files
.filter((file) => file.uploaded)
.map((file) => ({
name: file.name,
url: `https://${config.R2_CUSTOM_DOMAIN}/${file.name}`,
size: formatFileSize(file.size)
}))
}
// 更新缓存
await env.R2_BUCKET.put(config.CACHE_KEY, JSON.stringify(cacheData))
return new Response(JSON.stringify(cacheData), {
headers: {
'Content-Type': 'application/json',
'Access-Control-Allow-Origin': '*'
}
})
}
}
// 如果版本数据库也没有数据,才执行检查更新
const data = await checkNewRelease(env)
return new Response(JSON.stringify(data), {
headers: {
'Content-Type': 'application/json',
'Access-Control-Allow-Origin': '*'
}
})
}
const data = await cached.text()
return new Response(data, {
headers: {
'Content-Type': 'application/json',
'Access-Control-Allow-Origin': '*'
}
})
} catch (error) {
await addLog(env, 'ERROR', '获取版本信息失败', error.message)
return new Response(
JSON.stringify({
error: '获取版本信息失败: ' + error.message,
detail: '请稍<E8AFB7><E7A88D><EFBFBD>再试'
}),
{
status: 500,
headers: {
'Content-Type': 'application/json',
'Access-Control-Allow-Origin': '*'
}
}
)
}
}
// 修改下载处理函数,直接接收 env
async function handleDownload(env, filename) {
try {
const object = await env.R2_BUCKET.get(filename)
if (!object) {
return new Response('文件未找到', { status: 404 })
}
// 设置响应头
const headers = new Headers()
object.writeHttpMetadata(headers)
headers.set('etag', object.httpEtag)
headers.set('Content-Disposition', `attachment; filename="${filename}"`)
return new Response(object.body, {
headers
})
} catch (error) {
console.error('下载文件时发生错误:', error)
return new Response('获取文件失败', { status: 500 })
}
}
/**
* 根据文件扩展名获取对应的 Content-Type
*/
function getContentType(filename) {
const ext = filename.split('.').pop().toLowerCase()
const types = {
exe: 'application/x-msdownload', // Windows 可执行文件
dmg: 'application/x-apple-diskimage', // macOS 安装包
zip: 'application/zip', // 压缩包
AppImage: 'application/x-executable', // Linux 可执行文件
blockmap: 'application/octet-stream' // 更新文件
}
return types[ext] || 'application/octet-stream'
}
/**
* 格式化文件大小
* 将字节转换为人类可读的格式B, KB, MB, GB
*/
function formatFileSize(bytes) {
const units = ['B', 'KB', 'MB', 'GB']
let size = bytes
let unitIndex = 0
while (size >= 1024 && unitIndex < units.length - 1) {
size /= 1024
unitIndex++
}
return `${size.toFixed(2)} ${units[unitIndex]}`
}
/**
* 版本号比较函数
* 用于对版本号进行排序
*/
function compareVersions(a, b) {
const partsA = a.replace('v', '').split('.')
const partsB = b.replace('v', '').split('.')
for (let i = 0; i < Math.max(partsA.length, partsB.length); i++) {
const numA = parseInt(partsA[i] || 0)
const numB = parseInt(partsB[i] || 0)
if (numA !== numB) {
return numA - numB
}
}
return 0
}
/**
* 初始化数据文件
*/
async function initDataFiles(env) {
try {
// 检查并初始化版本数据库
const versionDB = await env.R2_BUCKET.get(config.VERSION_DB)
if (!versionDB) {
const initialVersions = {
versions: {},
latestVersion: null,
lastChecked: new Date().toISOString()
}
await env.R2_BUCKET.put(config.VERSION_DB, JSON.stringify(initialVersions, null, 2))
await addLog(env, 'INFO', 'versions.json 初始化成功')
}
// 检查并初始化日志文件
const logFile = await env.R2_BUCKET.get(config.LOG_FILE)
if (!logFile) {
const initialLogs = {
logs: [
{
timestamp: new Date().toISOString(),
type: 'INFO',
event: '系统初始化'
}
]
}
await env.R2_BUCKET.put(config.LOG_FILE, JSON.stringify(initialLogs, null, 2))
console.log('logs.json 初始化成功')
}
} catch (error) {
console.error('初始化数据文件失败:', error)
}
}
// 新增:只获取缓存的版本信息
async function getCachedRelease(env) {
try {
const cached = await env.R2_BUCKET.get(config.CACHE_KEY)
if (!cached) {
// 如果缓存不存在,从版本数据库获取
const versionDB = await env.R2_BUCKET.get(config.VERSION_DB)
if (versionDB) {
const versions = JSON.parse(await versionDB.text())
if (versions.latestVersion) {
const latestVersion = versions.versions[versions.latestVersion]
const cacheData = {
version: latestVersion.version,
publishedAt: latestVersion.publishedAt,
changelog: latestVersion.changelog,
downloads: latestVersion.files
.filter((file) => file.uploaded)
.map((file) => ({
name: file.name,
url: `https://${config.R2_CUSTOM_DOMAIN}/${file.name}`,
size: formatFileSize(file.size)
}))
}
// 重建缓存
await env.R2_BUCKET.put(config.CACHE_KEY, JSON.stringify(cacheData))
return new Response(JSON.stringify(cacheData), {
headers: {
'Content-Type': 'application/json',
'Access-Control-Allow-Origin': '*'
}
})
}
}
// 如果没有任何数据,返回错误
return new Response(
JSON.stringify({
error: '没有可用的版本信息'
}),
{
status: 404,
headers: {
'Content-Type': 'application/json',
'Access-Control-Allow-Origin': '*'
}
}
)
}
// 返回缓存数据
return new Response(await cached.text(), {
headers: {
'Content-Type': 'application/json',
'Access-Control-Allow-Origin': '*'
}
})
} catch (error) {
await addLog(env, 'ERROR', '获取缓存版本信息失败', error.message)
throw error
}
}
// 新增:只检查新版本并更新
async function checkNewRelease(env) {
try {
// 获取 GitHub 最新版本
const githubResponse = await fetch('https://api.github.com/repos/kangfenmao/cherry-studio/releases/latest', {
headers: { 'User-Agent': 'CloudflareWorker' }
})
if (!githubResponse.ok) {
throw new Error('GitHub API 请求失败')
}
const releaseData = await githubResponse.json()
const version = releaseData.tag_name
// 获取版本数据库
const versionDB = await env.R2_BUCKET.get(config.VERSION_DB)
let versions = { versions: {}, latestVersion: null, lastChecked: new Date().toISOString() }
if (versionDB) {
versions = JSON.parse(await versionDB.text())
}
// 移除版本检查,改为记录是否有文件更新的标志
let hasUpdates = false
if (versions.latestVersion !== version) {
await addLog(env, 'INFO', `发现新版本: ${version}`)
hasUpdates = true
} else {
await addLog(env, 'INFO', `版本 ${version} 文件完整性检查开始`)
}
// 准备新版本记录
const versionRecord = {
version,
publishedAt: releaseData.published_at,
uploadedAt: null,
files: releaseData.assets.map((asset) => ({
name: asset.name,
size: asset.size,
uploaded: false
})),
changelog: releaseData.body
}
// 检查并上传文件
for (const asset of releaseData.assets) {
try {
const existingFile = await env.R2_BUCKET.get(asset.name)
// 检查文件是否存在且大小是否一致
if (!existingFile || existingFile.size !== asset.size) {
hasUpdates = true
const response = await fetch(asset.browser_download_url)
if (!response.ok) {
throw new Error(`下载失败: HTTP ${response.status}`)
}
const file = await response.arrayBuffer()
await env.R2_BUCKET.put(asset.name, file, {
httpMetadata: { contentType: getContentType(asset.name) }
})
// 更新文件状态
const fileIndex = versionRecord.files.findIndex((f) => f.name === asset.name)
if (fileIndex !== -1) {
versionRecord.files[fileIndex].uploaded = true
}
await addLog(env, 'INFO', `文件${existingFile ? '更新' : '上传'}成功: ${asset.name}`)
} else {
// 文件存在且大小相同,标记为已上传
const fileIndex = versionRecord.files.findIndex((f) => f.name === asset.name)
if (fileIndex !== -1) {
versionRecord.files[fileIndex].uploaded = true
}
await addLog(env, 'INFO', `文件完整性验证通过: ${asset.name}`)
}
} catch (error) {
await addLog(env, 'ERROR', `文件处理失败: ${asset.name}`, error.message)
}
}
// 只有在有更新或是新版本时才更新数据库和缓存
if (hasUpdates) {
// 更新版本记录
versionRecord.uploadedAt = new Date().toISOString()
versions.versions[version] = versionRecord
versions.latestVersion = version
// 保存版本数据库
await env.R2_BUCKET.put(config.VERSION_DB, JSON.stringify(versions, null, 2))
// 更新缓存
const cacheData = {
version,
publishedAt: releaseData.published_at,
changelog: releaseData.body,
downloads: versionRecord.files
.filter((file) => file.uploaded)
.map((file) => ({
name: file.name,
url: `https://${config.R2_CUSTOM_DOMAIN}/${file.name}`,
size: formatFileSize(file.size)
}))
}
await env.R2_BUCKET.put(config.CACHE_KEY, JSON.stringify(cacheData))
await addLog(env, 'INFO', hasUpdates ? '更新完成' : '文件完整性检查完成')
// 清理旧版本
const versionList = Object.keys(versions.versions).sort((a, b) => compareVersions(b, a))
if (versionList.length > 2) {
// 获取需要保留的两个最新版本
const keepVersions = versionList.slice(0, 2)
// 获取所有需要删除的版本
const oldVersions = versionList.slice(2)
// 先获取 R2 桶中的所有文件列表
const allFiles = await listAllFiles(env)
// 获取需要保留的文件名列表
const keepFiles = new Set()
for (const keepVersion of keepVersions) {
const versionFiles = versions.versions[keepVersion].files
versionFiles.forEach((file) => keepFiles.add(file.name))
}
// 删除所有旧版本文件
for (const oldVersion of oldVersions) {
const oldFiles = versions.versions[oldVersion].files
for (const file of oldFiles) {
try {
if (file.uploaded) {
await env.R2_BUCKET.delete(file.name)
await addLog(env, 'INFO', `删除旧文件: ${file.name}`)
}
} catch (error) {
await addLog(env, 'ERROR', `删除旧文件失败: ${file.name}`, error.message)
}
}
delete versions.versions[oldVersion]
}
// 清理可能遗留的旧文件
for (const file of allFiles) {
if (!keepFiles.has(file.name)) {
try {
await env.R2_BUCKET.delete(file.name)
await addLog(env, 'INFO', `删除遗留文件: ${file.name}`)
} catch (error) {
await addLog(env, 'ERROR', `删除遗留文件失败: ${file.name}`, error.message)
}
}
}
// 保存更新后的版本数据库
await env.R2_BUCKET.put(config.VERSION_DB, JSON.stringify(versions, null, 2))
}
} else {
await addLog(env, 'INFO', '所有文件完整性检查通过,无需更新')
}
return hasUpdates ? cacheData : null
} catch (error) {
await addLog(env, 'ERROR', '检查新版本失败', error.message)
throw error
}
}
// 新增:获取 R2 桶中的所有文件列表
async function listAllFiles(env) {
const files = []
let cursor
do {
const listed = await env.R2_BUCKET.list({ cursor, include: ['customMetadata'] })
files.push(...listed.objects)
cursor = listed.cursor
} while (cursor)
return files
}

58
scripts/remove-locales.js Normal file
View File

@@ -0,0 +1,58 @@
const fs = require('fs')
const path = require('path')
exports.default = async function (context) {
const platform = context.packager.platform.name
// 根据平台确定 locales 目录位置
let resourceDirs = []
if (platform === 'mac') {
// macOS 的语言文件位置
resourceDirs = [
path.join(context.appOutDir, 'Cherry Studio.app', 'Contents', 'Resources'),
path.join(
context.appOutDir,
'Cherry Studio.app',
'Contents',
'Frameworks',
'Electron Framework.framework',
'Resources'
)
]
} else {
// Windows 和 Linux 的语言文件位置
resourceDirs = [path.join(context.appOutDir, 'locales')]
}
// 处理每个资源目录
for (const resourceDir of resourceDirs) {
if (!fs.existsSync(resourceDir)) {
console.log(`Resource directory not found: ${resourceDir}, skipping...`)
continue
}
// 读取所有文件和目录
const items = fs.readdirSync(resourceDir)
// 遍历并删除不需要的语言文件
for (const item of items) {
if (platform === 'mac') {
// 在 macOS 上检查 .lproj 目录
if (item.endsWith('.lproj') && !item.match(/^(en|zh|ru)/)) {
const dirPath = path.join(resourceDir, item)
fs.rmSync(dirPath, { recursive: true, force: true })
console.log(`Removed locale directory: ${item} from ${resourceDir}`)
}
} else {
// 其他平台处理 .pak 文件
if (!item.match(/^(en|zh|ru)/)) {
const filePath = path.join(resourceDir, item)
fs.unlinkSync(filePath)
console.log(`Removed locale file: ${item} from ${resourceDir}`)
}
}
}
}
console.log('Locale cleanup completed!')
}

58
scripts/replace-spaces.js Normal file
View File

@@ -0,0 +1,58 @@
// replaceSpaces.js
const fs = require('fs')
const path = require('path')
const directory = 'dist'
// 处理文件名中的空格
function replaceFileNames() {
fs.readdir(directory, (err, files) => {
if (err) throw err
files.forEach((file) => {
const oldPath = path.join(directory, file)
const newPath = path.join(directory, file.replace(/ /g, '-'))
fs.stat(oldPath, (err, stats) => {
if (err) throw err
if (stats.isFile() && oldPath !== newPath) {
fs.rename(oldPath, newPath, (err) => {
if (err) throw err
console.log(`Renamed: ${oldPath} -> ${newPath}`)
})
}
})
})
})
}
function replaceYmlContent() {
fs.readdir(directory, (err, files) => {
if (err) throw err
files.forEach((file) => {
if (path.extname(file).toLowerCase() === '.yml') {
const filePath = path.join(directory, file)
fs.readFile(filePath, 'utf8', (err, data) => {
if (err) throw err
// 替换内容
const newContent = data.replace(/Cherry Studio-/g, 'Cherry-Studio-')
// 写回文件
fs.writeFile(filePath, newContent, 'utf8', (err) => {
if (err) throw err
console.log(`Updated content in: ${filePath}`)
})
})
}
})
})
}
// 执行两个操作
replaceFileNames()
replaceYmlContent()

View File

@@ -1,26 +0,0 @@
// replaceSpaces.js
const fs = require('fs')
const path = require('path')
const directory = 'dist'
fs.readdir(directory, (err, files) => {
if (err) throw err
files.forEach((file) => {
const oldPath = path.join(directory, file)
const newPath = path.join(directory, file.replace(/ /g, '-'))
fs.stat(oldPath, (err, stats) => {
if (err) throw err
if (stats.isFile() && oldPath !== newPath) {
fs.rename(oldPath, newPath, (err) => {
if (err) throw err
console.log(`Renamed: ${oldPath} -> ${newPath}`)
})
}
})
})
})

39
scripts/utils.js Normal file
View File

@@ -0,0 +1,39 @@
const fs = require('fs')
const path = require('path')
const os = require('os')
function downloadNpmPackage(packageName, url) {
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'npm-download-'))
const targetDir = path.join('./node_modules/', packageName)
const filename = packageName.replace('/', '-') + '.tgz'
// Skip if directory already exists
if (fs.existsSync(targetDir)) {
console.log(`${targetDir} already exists, skipping download...`)
return
}
try {
console.log(`Downloading ${packageName}...`, url)
const { execSync } = require('child_process')
execSync(`curl --fail -o ${filename} ${url}`)
console.log(`Extracting ${filename}...`)
execSync(`tar -xvf ${filename}`)
execSync(`rm -rf ${filename}`)
execSync(`mv package ${targetDir}`)
} catch (error) {
console.error(`Error processing ${packageName}: ${error.message}`)
if (fs.existsSync(filename)) {
fs.unlinkSync(filename)
}
throw error
}
fs.rmSync(tempDir, { recursive: true, force: true })
}
module.exports = {
downloadNpmPackage
}

View File

@@ -1,95 +1,3 @@
export const isMac = process.platform === 'darwin'
export const isWin = process.platform === 'win32'
export const isLinux = process.platform === 'linux'
export const imageExts = ['.jpg', '.jpeg', '.png', '.gif', '.bmp', '.webp']
export const videoExts = ['.mp4', '.avi', '.mov', '.wmv', '.flv', '.mkv']
export const audioExts = ['.mp3', '.wav', '.ogg', '.flac', '.aac']
export const documentExts = ['.pdf', '.docx', '.pptx', '.xlsx', '.odt', '.odp', '.ods']
export const textExts = [
'.txt', // 普通文本文件
'.md', // Markdown 文件
'.mdx', // Markdown 文件
'.html', // HTML 文件
'.htm', // HTML 文件的另一种扩展名
'.xml', // XML 文件
'.json', // JSON 文件
'.yaml', // YAML 文件
'.yml', // YAML 文件的另一种扩展名
'.csv', // 逗号分隔值文件
'.tsv', // 制表符分隔值文件
'.ini', // 配置文件
'.log', // 日志文件
'.rtf', // 富文本格式文件
'.tex', // LaTeX 文件
'.srt', // 字幕文件
'.xhtml', // XHTML 文件
'.nfo', // 信息文件(主要用于场景发布)
'.conf', // 配置文件
'.config', // 配置文件
'.env', // 环境变量文件
'.rst', // reStructuredText 文件
'.php', // PHP 脚本文件,包含嵌入的 HTML
'.js', // JavaScript 文件(部分是文本,部分可能包含代码)
'.ts', // TypeScript 文件
'.jsp', // JavaServer Pages 文件
'.aspx', // ASP.NET 文件
'.bat', // Windows 批处理文件
'.sh', // Unix/Linux Shell 脚本文件
'.py', // Python 脚本文件
'.rb', // Ruby 脚本文件
'.pl', // Perl 脚本文件
'.sql', // SQL 脚本文件
'.css', // Cascading Style Sheets 文件
'.less', // Less CSS 预处理器文件
'.scss', // Sass CSS 预处理器文件
'.sass', // Sass 文件
'.styl', // Stylus CSS 预处理器文件
'.coffee', // CoffeeScript 文件
'.ino', // Arduino 代码文件
'.asm', // Assembly 语言文件
'.go', // Go 语言文件
'.scala', // Scala 语言文件
'.swift', // Swift 语言文件
'.kt', // Kotlin 语言文件
'.rs', // Rust 语言文件
'.lua', // Lua 语言文件
'.groovy', // Groovy 语言文件
'.dart', // Dart 语言文件
'.hs', // Haskell 语言文件
'.clj', // Clojure 语言文件
'.cljs', // ClojureScript 语言文件
'.elm', // Elm 语言文件
'.erl', // Erlang 语言文件
'.ex', // Elixir 语言文件
'.exs', // Elixir 脚本文件
'.pug', // Pug (formerly Jade) 模板文件
'.haml', // Haml 模板文件
'.slim', // Slim 模板文件
'.tpl', // 模板文件(通用)
'.ejs', // Embedded JavaScript 模板文件
'.hbs', // Handlebars 模板文件
'.mustache', // Mustache 模板文件
'.jade', // Jade 模板文件 (已重命名为 Pug)
'.twig', // Twig 模板文件
'.blade', // Blade 模板文件 (Laravel)
'.vue', // Vue.js 单文件组件
'.jsx', // React JSX 文件
'.tsx', // React TSX 文件
'.graphql', // GraphQL 查询语言文件
'.gql', // GraphQL 查询语言文件
'.proto', // Protocol Buffers 文件
'.thrift', // Thrift 文件
'.toml', // TOML 配置文件
'.edn', // Clojure 数据表示文件
'.cake', // CakePHP 配置文件
'.ctp', // CakePHP 视图文件
'.cfm', // ColdFusion 标记语言文件
'.cfc', // ColdFusion 组件文件
'.m', // Objective-C 源文件
'.mm', // Objective-C++ 源文件
'.gradle', // Gradle 构建文件
'.groovy', // Gradle 构建文件
'.kts', // Kotlin Script 文件
'.java' // Java 代码文件
]

View File

@@ -3,7 +3,7 @@ import { app, BrowserWindow } from 'electron'
import installExtension, { REDUX_DEVTOOLS } from 'electron-devtools-installer'
import { registerIpc } from './ipc'
import { registerZoomShortcut } from './services/ShortcutService'
import { registerShortcuts } from './services/ShortcutService'
import { TrayService } from './services/TrayService'
import { windowService } from './services/WindowService'
import { updateUserDataPath } from './utils/upgrade'
@@ -35,7 +35,7 @@ if (!app.requestSingleInstanceLock()) {
}
})
registerZoomShortcut(mainWindow)
registerShortcuts(mainWindow)
registerIpc(mainWindow, app)

View File

@@ -1,7 +1,11 @@
import fs from 'node:fs'
import path from 'node:path'
import vm from 'node:vm'
import { ThemeMode } from '@types'
import { BrowserWindow, ipcMain, session, shell } from 'electron'
import { Shortcut, ThemeMode } from '@types'
import axios from 'axios'
import { BrowserWindow, ipcMain, ProxyConfig, session, shell } from 'electron'
import log from 'electron-log'
import { titleBarOverlayDark, titleBarOverlayLight } from './config'
import AppUpdater from './services/AppUpdater'
@@ -9,6 +13,8 @@ import BackupManager from './services/BackupManager'
import { configManager } from './services/ConfigManager'
import { ExportService } from './services/ExportService'
import FileStorage from './services/FileStorage'
import KnowledgeService from './services/KnowledgeService'
import { registerShortcuts, unregisterAllShortcuts } from './services/ShortcutService'
import { windowService } from './services/WindowService'
import { compress, decompress } from './utils/zip'
@@ -23,10 +29,17 @@ export function registerIpc(mainWindow: BrowserWindow, app: Electron.App) {
version: app.getVersion(),
isPackaged: app.isPackaged,
appPath: app.getAppPath(),
filesPath: path.join(app.getPath('userData'), 'Data', 'Files')
filesPath: path.join(app.getPath('userData'), 'Data', 'Files'),
appDataPath: app.getPath('userData'),
logsPath: log.transports.file.getFile().path
}))
ipcMain.handle('app:proxy', (_, proxy: string) => session.defaultSession.setProxy(proxy ? { proxyRules: proxy } : {}))
ipcMain.handle('app:proxy', async (_, proxy: string) => {
const sessions = [session.defaultSession, session.fromPartition('persist:webview')]
const proxyConfig: ProxyConfig = proxy === 'system' ? { mode: 'system' } : proxy ? { proxyRules: proxy } : {}
await Promise.all(sessions.map((session) => session.setProxy(proxyConfig)))
})
ipcMain.handle('app:reload', () => mainWindow.reload())
ipcMain.handle('open:website', (_, url: string) => shell.openExternal(url))
@@ -47,11 +60,34 @@ export function registerIpc(mainWindow: BrowserWindow, app: Electron.App) {
mainWindow.setTitleBarOverlay(theme === 'dark' ? titleBarOverlayDark : titleBarOverlayLight)
})
// clear cache
ipcMain.handle('app:clear-cache', async () => {
const sessions = [session.defaultSession, session.fromPartition('persist:webview')]
try {
await Promise.all(
sessions.map(async (session) => {
await session.clearCache()
await session.clearStorageData({
storages: ['cookies', 'filesystem', 'shadercache', 'websql', 'serviceworkers', 'cachestorage']
})
})
)
await fileManager.clearTemp()
await fs.writeFileSync(log.transports.file.getFile().path, '')
return { success: true }
} catch (error: any) {
log.error('Failed to clear cache:', error)
return { success: false, error: error.message }
}
})
// check for update
ipcMain.handle('app:check-for-update', async () => {
const update = await autoUpdater.checkForUpdates()
return {
currentVersion: autoUpdater.currentVersion,
update: await autoUpdater.checkForUpdates()
updateInfo: update?.updateInfo
}
})
@@ -67,6 +103,7 @@ export function registerIpc(mainWindow: BrowserWindow, app: Electron.App) {
// file
ipcMain.handle('file:open', fileManager.open)
ipcMain.handle('file:openPath', fileManager.openPath)
ipcMain.handle('file:save', fileManager.save)
ipcMain.handle('file:select', fileManager.selectFile)
ipcMain.handle('file:upload', fileManager.uploadFile)
@@ -96,4 +133,33 @@ export function registerIpc(mainWindow: BrowserWindow, app: Electron.App) {
// export
ipcMain.handle('export:word', exportService.exportToWord)
// open path
ipcMain.handle('open:path', async (_, path: string) => {
await shell.openPath(path)
})
// shortcuts
ipcMain.handle('shortcuts:update', (_, shortcuts: Shortcut[]) => {
configManager.setShortcuts(shortcuts)
// Refresh shortcuts registration
if (mainWindow) {
unregisterAllShortcuts()
registerShortcuts(mainWindow)
}
})
// knowledge base
ipcMain.handle('knowledge-base:create', KnowledgeService.create)
ipcMain.handle('knowledge-base:reset', KnowledgeService.reset)
ipcMain.handle('knowledge-base:delete', KnowledgeService.delete)
ipcMain.handle('knowledge-base:add', KnowledgeService.add)
ipcMain.handle('knowledge-base:remove', KnowledgeService.remove)
ipcMain.handle('knowledge-base:search', KnowledgeService.search)
// vm
ipcMain.handle('run-js', (_, code: string) => {
const context = vm.createContext(Object.assign({ fetch: fetch, URL: URL, axios: axios }, global))
return vm.runInContext(code, context)
})
}

View File

@@ -1,15 +1,18 @@
import { BrowserWindow, dialog } from 'electron'
import { app, BrowserWindow, dialog } from 'electron'
import logger from 'electron-log'
import { AppUpdater as _AppUpdater, autoUpdater, UpdateInfo } from 'electron-updater'
import icon from '../../../build/icon.png?asset'
export default class AppUpdater {
autoUpdater: _AppUpdater = autoUpdater
constructor(mainWindow: BrowserWindow) {
logger.transports.file.level = 'debug'
logger.transports.file.level = 'info'
autoUpdater.logger = logger
autoUpdater.forceDevUpdateConfig = true
autoUpdater.autoDownload = false
autoUpdater.forceDevUpdateConfig = !app.isPackaged
autoUpdater.autoDownload = true
// 检测下载错误
autoUpdater.on('error', (error) => {
@@ -18,40 +21,8 @@ export default class AppUpdater {
})
autoUpdater.on('update-available', (releaseInfo: UpdateInfo) => {
autoUpdater.logger?.info('检测到新版本,确认是否下载')
logger.info('检测到新版本', releaseInfo)
mainWindow.webContents.send('update-available', releaseInfo)
const releaseNotes = releaseInfo.releaseNotes
let releaseContent = ''
if (releaseNotes) {
if (typeof releaseNotes === 'string') {
releaseContent = <string>releaseNotes
} else if (releaseNotes instanceof Array) {
releaseNotes.forEach((releaseNote) => {
releaseContent += `${releaseNote}\n`
})
}
} else {
releaseContent = '暂无更新说明'
}
// 弹框确认是否下载更新releaseContent是更新日志
dialog
.showMessageBox({
type: 'info',
title: '应用有新的更新',
detail: releaseContent,
message: '发现新版本,是否现在更新?',
buttons: ['下次再说', '更新']
})
.then(({ response }) => {
if (response === 1) {
logger.info('用户选择更新,准备下载更新')
mainWindow.webContents.send('download-update')
autoUpdater.downloadUpdate()
}
})
})
// 检测到不需要更新时
@@ -61,23 +32,53 @@ export default class AppUpdater {
// 更新下载进度
autoUpdater.on('download-progress', (progress) => {
logger.info('下载进度', progress)
mainWindow.webContents.send('download-progress', progress)
})
// 当需要更新的内容下载完成后
autoUpdater.on('update-downloaded', () => {
logger.info('下载完成,准备更新')
autoUpdater.on('update-downloaded', (releaseInfo: UpdateInfo) => {
mainWindow.webContents.send('update-downloaded')
logger.info('下载完成,询问用户是否更新', releaseInfo)
dialog
.showMessageBox({
type: 'info',
title: '安装更新',
message: '更新下载完毕,应用将重启并进行安装'
icon,
message: `新版本 ${releaseInfo.version} 已准备就绪`,
detail: this.formatReleaseNotes(releaseInfo.releaseNotes),
buttons: ['稍后安装', '立即安装'],
defaultId: 1,
cancelId: 0
})
.then(() => {
setImmediate(() => autoUpdater.quitAndInstall())
.then(({ response }) => {
if (response === 1) {
app.isQuitting = true
setImmediate(() => autoUpdater.quitAndInstall())
} else {
mainWindow.webContents.send('update-downloaded-cancelled')
}
})
})
this.autoUpdater = autoUpdater
}
private formatReleaseNotes(releaseNotes: string | ReleaseNoteInfo[] | null | undefined): string {
if (!releaseNotes) {
return '暂无更新说明'
}
if (typeof releaseNotes === 'string') {
return releaseNotes
}
return releaseNotes.map((note) => note.note).join('\n')
}
}
interface ReleaseNoteInfo {
readonly version: string
readonly note: string | null
}

View File

@@ -1,10 +1,9 @@
import { WebDavConfig } from '@types'
import archiver from 'archiver'
import AdmZip from 'adm-zip'
import { app } from 'electron'
import Logger from 'electron-log'
import * as fs from 'fs-extra'
import * as path from 'path'
import * as unzipper from 'unzipper'
import WebDav from './WebDav'
@@ -26,7 +25,6 @@ class BackupManager {
destinationPath: string = this.backupDir
): Promise<string> {
try {
// 创建临时目录
await fs.ensureDir(this.tempDir)
// 将 data 写入临时文件
@@ -38,21 +36,16 @@ class BackupManager {
const tempDataDir = path.join(this.tempDir, 'Data')
await fs.copy(sourcePath, tempDataDir)
// 创建 zip 文件
const output = fs.createWriteStream(path.join(destinationPath, fileName))
const archive = archiver('zip', { zlib: { level: 9 } })
archive.pipe(output)
archive.directory(this.tempDir, false)
await archive.finalize()
// 使用 adm-zip 创建压缩文件
const zip = new AdmZip()
zip.addLocalFolder(this.tempDir)
const backupedFilePath = path.join(destinationPath, fileName)
zip.writeZip(backupedFilePath)
// 清理临时目录
await fs.remove(this.tempDir)
Logger.log('Backup completed successfully')
const backupedFilePath = path.join(destinationPath, fileName)
return backupedFilePath
} catch (error) {
Logger.error('Backup failed:', error)
@@ -61,31 +54,43 @@ class BackupManager {
}
async restore(_: Electron.IpcMainInvokeEvent, backupPath: string): Promise<string> {
// 创建临时目录
await fs.ensureDir(this.tempDir)
try {
// 创建临时目录
await fs.ensureDir(this.tempDir)
// 解压备份文件到临时目录
await fs
.createReadStream(backupPath)
.pipe(unzipper.Extract({ path: this.tempDir }))
.promise()
Logger.log('[backup] step 1: unzip backup file', this.tempDir)
// 读取 data.json
const dataPath = path.join(this.tempDir, 'data.json')
const data = await fs.readFile(dataPath, 'utf-8')
// 使用 adm-zip 解压
const zip = new AdmZip(backupPath)
zip.extractAllTo(this.tempDir, true) // true 表示覆盖已存在的文件
// 恢复 Data 目录
const sourcePath = path.join(this.tempDir, 'Data')
const destPath = path.join(app.getPath('userData'), 'Data')
await fs.remove(destPath)
await fs.copy(sourcePath, destPath)
Logger.log('[backup] step 2: read data.json')
// 清理临时目录
await fs.remove(this.tempDir)
// 读取 data.json
const dataPath = path.join(this.tempDir, 'data.json')
const data = await fs.readFile(dataPath, 'utf-8')
Logger.log('Restore completed successfully')
Logger.log('[backup] step 3: restore Data directory')
return data
// 恢复 Data 目录
const sourcePath = path.join(this.tempDir, 'Data')
const destPath = path.join(app.getPath('userData'), 'Data')
await fs.remove(destPath)
await fs.copy(sourcePath, destPath)
Logger.log('[backup] step 4: clean up temp directory')
// 清理临时目录
await fs.remove(this.tempDir)
Logger.log('[backup] step 5: Restore completed successfully')
return data
} catch (error) {
Logger.error('[backup] Restore failed:', error)
await fs.remove(this.tempDir).catch(() => {})
throw error
}
}
async backupToWebdav(_: Electron.IpcMainInvokeEvent, data: string, webdavConfig: WebDavConfig) {

View File

@@ -1,7 +1,10 @@
import { LanguageVarious, ThemeMode } from '@types'
import { ZOOM_SHORTCUTS } from '@shared/config/constant'
import { LanguageVarious, Shortcut, ThemeMode } from '@types'
import { app } from 'electron'
import Store from 'electron-store'
import { locales } from '../utils/locales'
export class ConfigManager {
private store: Store
private subscribers: Map<string, Array<(newValue: any) => void>> = new Map()
@@ -11,7 +14,8 @@ export class ConfigManager {
}
getLanguage(): LanguageVarious {
return this.store.get('language', app.getLocale()) as LanguageVarious
const locale = Object.keys(locales).includes(app.getLocale()) ? app.getLocale() : 'en-US'
return this.store.get('language', locale) as LanguageVarious
}
setLanguage(theme: LanguageVarious) {
@@ -35,6 +39,15 @@ export class ConfigManager {
this.notifySubscribers('tray', value)
}
getZoomFactor(): number {
return this.store.get('zoomFactor', 1) as number
}
setZoomFactor(factor: number) {
this.store.set('zoomFactor', factor)
this.notifySubscribers('zoomFactor', factor)
}
subscribe<T>(key: string, callback: (newValue: T) => void) {
if (!this.subscribers.has(key)) {
this.subscribers.set(key, [])
@@ -58,6 +71,18 @@ export class ConfigManager {
subscribers.forEach((subscriber) => subscriber(newValue))
}
}
getShortcuts() {
return this.store.get('shortcuts', ZOOM_SHORTCUTS) as Shortcut[] | []
}
setShortcuts(shortcuts: Shortcut[]) {
this.store.set(
'shortcuts',
shortcuts.filter((shortcut) => shortcut.system)
)
this.notifySubscribers('shortcuts', shortcuts)
}
}
export const configManager = new ConfigManager()

View File

@@ -1,5 +1,5 @@
import { documentExts, imageExts } from '@main/constant'
import { getFileType } from '@main/utils/file'
import { documentExts, imageExts } from '@shared/config/constant'
import { FileType } from '@types'
import * as crypto from 'crypto'
import {
@@ -8,7 +8,8 @@ import {
OpenDialogOptions,
OpenDialogReturnValue,
SaveDialogOptions,
SaveDialogReturnValue
SaveDialogReturnValue,
shell
} from 'electron'
import logger from 'electron-log'
import * as fs from 'fs'
@@ -267,6 +268,11 @@ class FileStorage {
await this.initStorageDir()
}
public clearTemp = async (): Promise<void> => {
await fs.promises.rmdir(this.tempDir, { recursive: true })
await fs.promises.mkdir(this.tempDir, { recursive: true })
}
public open = async (
_: Electron.IpcMainInvokeEvent,
options: OpenDialogOptions
@@ -293,6 +299,10 @@ class FileStorage {
}
}
public openPath = async (_: Electron.IpcMainInvokeEvent, path: string): Promise<void> => {
shell.openPath(path).catch((err) => logger.error('[IPC - Error] Failed to open file:', err))
}
public save = async (
_: Electron.IpcMainInvokeEvent,
fileName: string,

View File

@@ -0,0 +1,154 @@
import * as fs from 'node:fs'
import path from 'node:path'
import { LocalPathLoader, RAGApplication, RAGApplicationBuilder, TextLoader } from '@llm-tools/embedjs'
import type { AddLoaderReturn, ExtractChunkData } from '@llm-tools/embedjs-interfaces'
import { LibSqlDb } from '@llm-tools/embedjs-libsql'
import { MarkdownLoader } from '@llm-tools/embedjs-loader-markdown'
import { DocxLoader, ExcelLoader, PptLoader } from '@llm-tools/embedjs-loader-msoffice'
import { PdfLoader } from '@llm-tools/embedjs-loader-pdf'
import { SitemapLoader } from '@llm-tools/embedjs-loader-sitemap'
import { WebLoader } from '@llm-tools/embedjs-loader-web'
import { AzureOpenAiEmbeddings, OpenAiEmbeddings } from '@llm-tools/embedjs-openai'
import { getInstanceName } from '@main/utils'
import { FileType, KnowledgeBaseParams, KnowledgeItem } from '@types'
import { app } from 'electron'
class KnowledgeService {
private storageDir = path.join(app.getPath('userData'), 'Data', 'KnowledgeBase')
constructor() {
this.initStorageDir()
}
private initStorageDir = (): void => {
if (!fs.existsSync(this.storageDir)) {
fs.mkdirSync(this.storageDir, { recursive: true })
}
}
private getRagApplication = async ({
id,
model,
apiKey,
apiVersion,
baseURL,
dimensions
}: KnowledgeBaseParams): Promise<RAGApplication> => {
return new RAGApplicationBuilder()
.setModel('NO_MODEL')
.setEmbeddingModel(
apiVersion
? new AzureOpenAiEmbeddings({
azureOpenAIApiKey: apiKey,
azureOpenAIApiVersion: apiVersion,
azureOpenAIApiDeploymentName: model,
azureOpenAIApiInstanceName: getInstanceName(baseURL),
dimensions,
batchSize: 15
})
: new OpenAiEmbeddings({
model,
apiKey,
configuration: { baseURL },
dimensions,
batchSize: 15
})
)
.setVectorDatabase(new LibSqlDb({ path: path.join(this.storageDir, id) }))
.build()
}
public create = async (_: Electron.IpcMainInvokeEvent, base: KnowledgeBaseParams): Promise<void> => {
this.getRagApplication(base)
}
public reset = async (_: Electron.IpcMainInvokeEvent, { base }: { base: KnowledgeBaseParams }): Promise<void> => {
const ragApplication = await this.getRagApplication(base)
await ragApplication.reset()
}
public delete = async (_: Electron.IpcMainInvokeEvent, id: string): Promise<void> => {
const dbPath = path.join(this.storageDir, id)
if (fs.existsSync(dbPath)) {
fs.rmSync(dbPath, { recursive: true })
}
}
public add = async (
_: Electron.IpcMainInvokeEvent,
{ base, item, forceReload = false }: { base: KnowledgeBaseParams; item: KnowledgeItem; forceReload: boolean }
): Promise<AddLoaderReturn> => {
const ragApplication = await this.getRagApplication(base)
if (item.type === 'directory') {
const directory = item.content as string
return await ragApplication.addLoader(new LocalPathLoader({ path: directory }), forceReload)
}
if (item.type === 'url') {
const content = item.content as string
if (content.startsWith('http')) {
return await ragApplication.addLoader(new WebLoader({ urlOrContent: content }), forceReload)
}
}
if (item.type === 'sitemap') {
const content = item.content as string
return await ragApplication.addLoader(new SitemapLoader({ url: content }), forceReload)
}
if (item.type === 'note') {
const content = item.content as string
return await ragApplication.addLoader(new TextLoader({ text: content }), forceReload)
}
if (item.type === 'file') {
const file = item.content as FileType
if (file.ext === '.pdf') {
return await ragApplication.addLoader(new PdfLoader({ filePathOrUrl: file.path }) as any, forceReload)
}
if (file.ext === '.docx') {
return await ragApplication.addLoader(new DocxLoader({ filePathOrUrl: file.path }) as any, forceReload)
}
if (file.ext === '.pptx') {
return await ragApplication.addLoader(new PptLoader({ filePathOrUrl: file.path }) as any, forceReload)
}
if (file.ext === '.xlsx') {
return await ragApplication.addLoader(new ExcelLoader({ filePathOrUrl: file.path }) as any, forceReload)
}
if (['.md', '.mdx'].includes(file.ext)) {
return await ragApplication.addLoader(new MarkdownLoader({ filePathOrUrl: file.path }) as any, forceReload)
}
const fileContent = fs.readFileSync(file.path, 'utf-8')
return await ragApplication.addLoader(new TextLoader({ text: fileContent }), forceReload)
}
return { entriesAdded: 0, uniqueId: '', loaderType: '' }
}
public remove = async (
_: Electron.IpcMainInvokeEvent,
{ uniqueId, base }: { uniqueId: string; base: KnowledgeBaseParams }
): Promise<void> => {
const ragApplication = await this.getRagApplication(base)
await ragApplication.deleteLoader(uniqueId)
}
public search = async (
_: Electron.IpcMainInvokeEvent,
{ search, base }: { search: string; base: KnowledgeBaseParams }
): Promise<ExtractChunkData[]> => {
const ragApplication = await this.getRagApplication(base)
return await ragApplication.search(search)
}
}
export default new KnowledgeService()

View File

@@ -1,66 +1,131 @@
import { Shortcut } from '@types'
import { BrowserWindow, globalShortcut } from 'electron'
import Logger from 'electron-log'
export function registerZoomShortcut(mainWindow: BrowserWindow) {
const registerShortcuts = () => {
// 注册放大快捷键 (Ctrl+Plus 或 Cmd+Plus)
globalShortcut.register('CommandOrControl+=', () => {
if (mainWindow) {
const currentZoom = mainWindow.webContents.getZoomFactor()
const newZoom = currentZoom + 0.1
// Prevent zoom factor from exceeding reasonable limits
if (newZoom <= 5.0) {
mainWindow.webContents.setZoomFactor(newZoom)
import { configManager } from './ConfigManager'
let showAppAccelerator: string | null = null
function getShortcutHandler(shortcut: Shortcut) {
switch (shortcut.key) {
case 'zoom_in':
return (window: BrowserWindow) => handleZoom(0.1)(window)
case 'zoom_out':
return (window: BrowserWindow) => handleZoom(-0.1)(window)
case 'zoom_reset':
return (window: BrowserWindow) => {
window.webContents.setZoomFactor(1)
configManager.setZoomFactor(1)
}
case 'show_app':
return (window: BrowserWindow) => {
if (window.isVisible()) {
window.hide()
} else {
window.show()
window.focus()
}
}
})
// 注册缩小快捷键 (Ctrl+Minus 或 Cmd+Minus)
globalShortcut.register('CommandOrControl+-', () => {
if (mainWindow) {
const currentZoom = mainWindow.webContents.getZoomFactor()
const newZoom = currentZoom - 0.1
// Prevent zoom factor from going below 0.1
if (newZoom >= 0.1) {
mainWindow.webContents.setZoomFactor(newZoom)
}
}
})
// 注册重置缩放快捷键 (Ctrl+0 或 Cmd+0)
globalShortcut.register('CommandOrControl+0', () => {
if (mainWindow) {
mainWindow.webContents.setZoomFactor(1)
}
})
}
const unregisterShortcuts = () => {
globalShortcut.unregister('CommandOrControl+=')
globalShortcut.unregister('CommandOrControl+-')
globalShortcut.unregister('CommandOrControl+0')
}
// Add check for window destruction
if (mainWindow.isDestroyed()) {
return
}
// When window gains focus, register shortcuts
mainWindow.on('focus', () => {
if (!mainWindow.isDestroyed()) {
registerShortcuts()
}
})
// When window loses focus, unregister shortcuts
mainWindow.on('blur', () => {
if (!mainWindow.isDestroyed()) {
unregisterShortcuts()
}
})
// Initial registration (if window is already focused)
if (!mainWindow.isDestroyed() && mainWindow.isFocused()) {
registerShortcuts()
default:
return null
}
}
function formatShortcutKey(shortcut: string[]): string {
return shortcut.join('+')
}
function handleZoom(delta: number) {
return (window: BrowserWindow) => {
const currentZoom = window.webContents.getZoomFactor()
const newZoom = currentZoom + delta
if (newZoom >= 0.1 && newZoom <= 5.0) {
window.webContents.setZoomFactor(newZoom)
configManager.setZoomFactor(newZoom)
}
}
}
export function registerShortcuts(window: BrowserWindow) {
window.webContents.setZoomFactor(configManager.getZoomFactor())
const register = () => {
if (window.isDestroyed()) return
const shortcuts = configManager.getShortcuts()
if (!shortcuts) return
shortcuts.forEach((shortcut) => {
try {
if (shortcut.shortcut.length === 0) {
return
}
const handler = getShortcutHandler(shortcut)
if (!handler) {
return
}
const accelerator = formatShortcutKey(shortcut.shortcut)
if (shortcut.key === 'show_app') {
showAppAccelerator = accelerator
}
if (shortcut.key.includes('zoom')) {
switch (shortcut.key) {
case 'zoom_in':
globalShortcut.register('CommandOrControl+=', () => shortcut.enabled && handler(window))
globalShortcut.register('CommandOrControl+numadd', () => shortcut.enabled && handler(window))
return
case 'zoom_out':
globalShortcut.register('CommandOrControl+-', () => shortcut.enabled && handler(window))
globalShortcut.register('CommandOrControl+numsub', () => shortcut.enabled && handler(window))
return
case 'zoom_reset':
globalShortcut.register('CommandOrControl+0', () => shortcut.enabled && handler(window))
return
}
}
if (shortcut.enabled) {
globalShortcut.register(accelerator, () => handler(window))
}
} catch (error) {
Logger.error(`[ShortcutService] Failed to register shortcut ${shortcut.key}`)
}
})
}
const unregister = () => {
if (window.isDestroyed()) return
try {
globalShortcut.unregisterAll()
if (showAppAccelerator) {
const handler = getShortcutHandler({ key: 'show_app' } as Shortcut)
handler && globalShortcut.register(showAppAccelerator, () => handler(window))
}
} catch (error) {
Logger.error('[ShortcutService] Failed to unregister shortcuts')
}
}
window.on('focus', () => register())
window.on('blur', () => unregister())
if (!window.isDestroyed() && window.isFocused()) {
register()
}
}
export function unregisterAllShortcuts() {
try {
showAppAccelerator = null
globalShortcut.unregisterAll()
} catch (error) {
Logger.error('[ShortcutService] Failed to unregister all shortcuts')
}
}

View File

@@ -1,8 +1,9 @@
import { is } from '@electron-toolkit/utils'
import { isLinux, isWin } from '@main/constant'
import { app, BrowserWindow, Menu, MenuItem, shell } from 'electron'
import Logger from 'electron-log'
import windowStateKeeper from 'electron-window-state'
import { join } from 'path'
import path, { join } from 'path'
import icon from '../../../build/icon.png?asset'
import { titleBarOverlayDark, titleBarOverlayLight } from '../config'
@@ -32,6 +33,7 @@ export class WindowService {
const theme = configManager.getTheme()
const isMac = process.platform === 'darwin'
const isLinux = process.platform === 'linux'
this.mainWindow = new BrowserWindow({
x: mainWindowState.x,
@@ -43,9 +45,9 @@ export class WindowService {
show: true,
autoHideMenuBar: true,
transparent: isMac,
vibrancy: 'fullscreen-ui',
vibrancy: 'under-window',
visualEffectState: 'active',
titleBarStyle: 'hidden',
titleBarStyle: isLinux ? 'default' : 'hidden',
titleBarOverlay: theme === 'dark' ? titleBarOverlayDark : titleBarOverlayLight,
backgroundColor: isMac ? undefined : theme === 'dark' ? '#181818' : '#FFFFFF',
trafficLightPosition: { x: 8, y: 12 },
@@ -123,12 +125,26 @@ export class WindowService {
private setupWebContentsHandlers(mainWindow: BrowserWindow) {
mainWindow.webContents.on('will-navigate', (event, url) => {
if (url.includes('localhost:5173')) {
return
}
event.preventDefault()
shell.openExternal(url)
})
mainWindow.webContents.setWindowOpenHandler((details) => {
shell.openExternal(details.url)
const { url } = details
if (url.includes('http://file/')) {
const fileName = url.replace('http://file/', '')
const storageDir = path.join(app.getPath('userData'), 'Data', 'Files')
const filePath = storageDir + '/' + fileName
shell.openPath(filePath).catch((err) => Logger.error('Failed to open file:', err))
} else {
shell.openExternal(details.url)
}
return { action: 'deny' }
})

View File

@@ -1,6 +1,5 @@
import { audioExts, documentExts, imageExts, textExts, videoExts } from '@main/constant'
import { FileTypes } from '../../renderer/src/types'
import { audioExts, documentExts, imageExts, textExts, videoExts } from '@shared/config/constant'
import { FileTypes } from '@types'
export function getFileType(ext: string): FileTypes {
ext = ext.toLowerCase()

View File

@@ -14,3 +14,11 @@ export function getDataPath() {
}
return dataPath
}
export function getInstanceName(baseURL: string) {
try {
return new URL(baseURL).host.split('.')[0]
} catch (error) {
return ''
}
}

View File

@@ -1,11 +1,15 @@
import EnUs from '../../renderer/src/i18n/locales/en-us.json'
import JaJP from '../../renderer/src/i18n/locales/ja-jp.json'
import RuRu from '../../renderer/src/i18n/locales/ru-ru.json'
import ZhCn from '../../renderer/src/i18n/locales/zh-cn.json'
import ZhTw from '../../renderer/src/i18n/locales/zh-tw.json'
const locales = {
'en-US': EnUs,
'zh-CN': ZhCn,
'zh-TW': ZhTw
'zh-TW': ZhTw,
'ja-JP': JaJP,
'ru-RU': RuRu
}
export { locales }

View File

@@ -1,21 +1,18 @@
import { ElectronAPI } from '@electron-toolkit/preload'
import { AddLoaderReturn, ExtractChunkData } from '@llm-tools/embedjs-interfaces'
import { FileType } from '@renderer/types'
import { WebDavConfig } from '@renderer/types'
import { LanguageVarious } from '@renderer/types'
import { AppInfo, KnowledgeBaseParams, KnowledgeItem, LanguageVarious } from '@renderer/types'
import type { OpenDialogOptions } from 'electron'
import type { UpdateInfo } from 'electron-updater'
import { Readable } from 'stream'
declare global {
interface Window {
electron: ElectronAPI
api: {
getAppInfo: () => Promise<{
version: string
isPackaged: boolean
appPath: string
filesPath: string
}>
checkForUpdate: () => void
getAppInfo: () => Promise<AppInfo>
checkForUpdate: () => Promise<{ currentVersion: string; updateInfo: UpdateInfo | null }>
openWebsite: (url: string) => void
setProxy: (proxy: string | undefined) => void
setLanguage: (theme: LanguageVarious) => void
@@ -23,6 +20,7 @@ declare global {
setTheme: (theme: 'light' | 'dark') => void
minApp: (options: { url: string; windowOptions?: Electron.BrowserWindowConstructorOptions }) => void
reload: () => void
clearCache: () => Promise<{ success: boolean; error?: string }>
zip: {
compress: (text: string) => Promise<Buffer>
decompress: (text: Buffer) => Promise<string>
@@ -44,6 +42,7 @@ declare global {
create: (fileName: string) => Promise<string>
write: (filePath: string, data: Uint8Array | string) => Promise<void>
open: (options?: OpenDialogOptions) => Promise<{ fileName: string; filePath: string; content: Buffer } | null>
openPath: (path: string) => Promise<void>
save: (
path: string,
content: string | NodeJS.ArrayBufferView,
@@ -57,6 +56,29 @@ declare global {
export: {
toWord: (markdown: string, fileName: string) => Promise<void>
}
openPath: (path: string) => Promise<void>
shortcuts: {
update: (shortcuts: Shortcut[]) => Promise<void>
}
knowledgeBase: {
create: ({ id, model, apiKey, baseURL }: KnowledgeBaseParams) => Promise<void>
reset: ({ base }: { base: KnowledgeBaseParams }) => Promise<void>
delete: (id: string) => Promise<void>
add: ({
base,
item,
forceReload = false
}: {
base: KnowledgeBaseParams
item: KnowledgeItem
forceReload?: boolean
}) => Promise<AddLoaderReturn>
remove: ({ uniqueId, base }: { uniqueId: string; base: KnowledgeBaseParams }) => Promise<void>
search: ({ search, base }: { search: string; base: KnowledgeBaseParams }) => Promise<ExtractChunkData[]>
}
vm: {
run: (code: string) => Promise<any>
}
}
}
}

View File

@@ -1,5 +1,5 @@
import { electronAPI } from '@electron-toolkit/preload'
import { WebDavConfig } from '@types'
import { KnowledgeBaseParams, KnowledgeItem, Shortcut, WebDavConfig } from '@types'
import { contextBridge, ipcRenderer, OpenDialogOptions } from 'electron'
// Custom APIs for renderer
@@ -13,6 +13,7 @@ const api = {
setTheme: (theme: 'light' | 'dark') => ipcRenderer.invoke('app:set-theme', theme),
openWebsite: (url: string) => ipcRenderer.invoke('open:website', url),
minApp: (url: string) => ipcRenderer.invoke('minapp', url),
clearCache: () => ipcRenderer.invoke('app:clear-cache'),
zip: {
compress: (text: string) => ipcRenderer.invoke('zip:compress', text),
decompress: (text: Buffer) => ipcRenderer.invoke('zip:decompress', text)
@@ -35,6 +36,7 @@ const api = {
create: (fileName: string) => ipcRenderer.invoke('file:create', fileName),
write: (filePath: string, data: Uint8Array | string) => ipcRenderer.invoke('file:write', filePath, data),
open: (options?: { decompress: boolean }) => ipcRenderer.invoke('file:open', options),
openPath: (path: string) => ipcRenderer.invoke('file:openPath', path),
save: (path: string, content: string, options?: { compress: boolean }) =>
ipcRenderer.invoke('file:save', path, content, options),
selectFolder: () => ipcRenderer.invoke('file:selectFolder'),
@@ -45,6 +47,32 @@ const api = {
},
export: {
toWord: (markdown: string, fileName: string) => ipcRenderer.invoke('export:word', markdown, fileName)
},
openPath: (path: string) => ipcRenderer.invoke('open:path', path),
shortcuts: {
update: (shortcuts: Shortcut[]) => ipcRenderer.invoke('shortcuts:update', shortcuts)
},
knowledgeBase: {
create: ({ id, model, apiKey, baseURL }: KnowledgeBaseParams) =>
ipcRenderer.invoke('knowledge-base:create', { id, model, apiKey, baseURL }),
reset: ({ base }: { base: KnowledgeBaseParams }) => ipcRenderer.invoke('knowledge-base:reset', { base }),
delete: (id: string) => ipcRenderer.invoke('knowledge-base:delete', id),
add: ({
base,
item,
forceReload = false
}: {
base: KnowledgeBaseParams
item: KnowledgeItem
forceReload?: boolean
}) => ipcRenderer.invoke('knowledge-base:add', { base, item, forceReload }),
remove: ({ uniqueId, base }: { uniqueId: string; base: KnowledgeBaseParams }) =>
ipcRenderer.invoke('knowledge-base:remove', { uniqueId, base }),
search: ({ search, base }: { search: string; base: KnowledgeBaseParams }) =>
ipcRenderer.invoke('knowledge-base:search', { search, base })
},
vm: {
run: (code: string) => ipcRenderer.invoke('run-js', code)
}
}

View File

@@ -13,8 +13,8 @@ import { ThemeProvider } from './context/ThemeProvider'
import AgentsPage from './pages/agents/AgentsPage'
import AppsPage from './pages/apps/AppsPage'
import FilesPage from './pages/files/FilesPage'
import HistoryPage from './pages/history/HistoryPage'
import HomePage from './pages/home/HomePage'
import KnowledgePage from './pages/knowledge/KnowledgePage'
import PaintingsPage from './pages/paintings/PaintingsPage'
import SettingsPage from './pages/settings/SettingsPage'
import TranslatePage from './pages/translate/TranslatePage'
@@ -31,12 +31,12 @@ function App(): JSX.Element {
<Sidebar />
<Routes>
<Route path="/" element={<HomePage />} />
<Route path="/files" element={<FilesPage />} />
<Route path="/agents" element={<AgentsPage />} />
<Route path="/paintings" element={<PaintingsPage />} />
<Route path="/translate" element={<TranslatePage />} />
<Route path="/files" element={<FilesPage />} />
<Route path="/knowledge" element={<KnowledgePage />} />
<Route path="/apps" element={<AppsPage />} />
<Route path="/messages/*" element={<HistoryPage />} />
<Route path="/settings/*" element={<SettingsPage />} />
</Routes>
</HashRouter>

View File

@@ -1,6 +1,6 @@
@font-face {
font-family: 'iconfont'; /* Project id 4563475 */
src: url('iconfont.woff2?t=1725606177995') format('woff2');
font-family: 'iconfont'; /* Project id 4753420 */
src: url('iconfont.woff2?t=1733224456443') format('woff2');
}
.iconfont {
@@ -11,6 +11,14 @@
-moz-osx-font-smoothing: grayscale;
}
.icon-at1:before {
content: '\e7df';
}
.icon-at:before {
content: '\e630';
}
.icon-a-darkmode:before {
content: '\e6cd';
}
@@ -27,10 +35,6 @@
content: '\e942';
}
.icon-grid-row-2copy:before {
content: '\e681';
}
.icon-inbox:before {
content: '\e869';
}
@@ -71,10 +75,6 @@
content: '\e944';
}
.icon-a-addchat:before {
content: '\e658';
}
.icon-appstore:before {
content: '\e792';
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 195 KiB

After

Width:  |  Height:  |  Size: 44 KiB

View File

@@ -1,55 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg id="_图层_2" data-name="图层_2" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 198.45 66.73">
<defs>
<style>
.cls-1 {
fill: #ea5e5d;
}
.cls-2 {
fill: #23af69;
}
.cls-3 {
fill: #ea5756;
}
</style>
</defs>
<g id="_图层_1-2" data-name="图层_1">
<g>
<g>
<g>
<path class="cls-1" d="M16.72,51.21c-4.45,0-8.64-1.78-11.81-5.01-3.17-3.23-4.91-7.51-4.91-12.04s1.74-8.81,4.91-12.04,7.36-5.01,11.81-5.01,8.71,1.82,11.82,4.99c2.32,2.36,2.32,6.2,0,8.56-2.32,2.36-6.08,2.36-8.4,0-.9-.92-2.15-1.45-3.43-1.45-2.63,0-4.85,2.26-4.85,4.94s2.22,4.94,4.85,4.94c1.28,0,2.52-.53,3.43-1.45,2.32-2.36,6.08-2.36,8.4,0,2.32,2.36,2.32,6.2,0,8.56-3.11,3.17-7.42,4.99-11.82,4.99Z"/>
<path class="cls-1" d="M32.05,66.73c-4.45,0-8.64-1.78-11.81-5.01s-4.91-7.51-4.91-12.04,1.79-8.88,4.9-12.06c2.32-2.36,6.08-2.36,8.4,0,2.32,2.36,2.32,6.2,0,8.56-.9.92-1.42,2.19-1.42,3.49,0,2.68,2.22,4.94,4.85,4.94s4.85-2.26,4.85-4.94c0-.95-.23-2.31-1.32-3.43-3.13-3.19-4.92-7.6-4.92-12.09s1.74-8.81,4.91-12.04,7.36-5.01,11.81-5.01,8.64,1.78,11.81,5.01,4.91,7.51,4.91,12.04-1.79,8.88-4.9,12.06c-2.32,2.36-6.08,2.36-8.4,0-2.32-2.36-2.32-6.2,0-8.56.9-.92,1.42-2.19,1.42-3.49,0-2.68-2.22-4.94-4.85-4.94s-4.85,2.26-4.85,4.94c0,1.31.53,2.6,1.45,3.53,3.1,3.16,4.8,7.42,4.8,11.99s-1.74,8.81-4.91,12.04c-3.17,3.23-7.36,5.01-11.81,5.01Z"/>
</g>
<path class="cls-2" d="M32.05,19.09l-9.72-9.12c-1.5-1.4-1.57-3.75-.17-5.25,1.4-1.49,3.75-1.57,5.25-.17l3.89,3.65,5.53-6.83c1.29-1.59,3.63-1.84,5.22-.55,1.59,1.29,1.84,3.63.55,5.22l-10.56,13.05Z"/>
</g>
<g>
<path class="cls-3" d="M93.93,24.6l.55-.39c.69-.4,1.17-.61,1.46-.61.63,0,1.3.57,2.03,1.7.44.71.67,1.27.67,1.7s-.14.78-.41,1.06c-.27.28-.59.54-.96.76-.36.22-.71.43-1.05.64-.33.2-1.02.47-2.05.79-1.03.32-2.03.49-2.99.49s-1.93-.13-2.91-.38c-.98-.25-1.99-.68-3.03-1.27-1.04-.6-1.98-1.32-2.81-2.18-.83-.86-1.51-1.96-2.05-3.31-.54-1.35-.8-2.81-.8-4.38s.26-3.01.79-4.29c.53-1.28,1.2-2.35,2.02-3.19.82-.84,1.75-1.54,2.81-2.11,1.98-1.09,3.97-1.64,5.98-1.64.95,0,1.92.15,2.9.44.98.29,1.72.59,2.23.9l.73.42c.36.22.65.4.85.55.53.42.79.91.79,1.44s-.21,1.1-.64,1.68c-.79,1.09-1.5,1.64-2.12,1.64-.36,0-.88-.22-1.55-.67-.85-.69-1.98-1.03-3.4-1.03-1.31,0-2.61.46-3.88,1.36-.61.44-1.11,1.07-1.52,1.88-.4.81-.61,1.72-.61,2.75s.2,1.94.61,2.75c.4.81.92,1.45,1.55,1.91,1.23.89,2.52,1.34,3.85,1.34.63,0,1.22-.08,1.77-.24.56-.16.96-.32,1.2-.49Z"/>
<path class="cls-3" d="M114.38,9.07c.16-.3.43-.52.82-.64.38-.12.87-.18,1.46-.18s1.05.05,1.4.15c.34.1.61.22.79.36.18.14.32.34.42.61.1.34.15.87.15,1.58v16.84c0,.47-.02.81-.05,1.05-.03.23-.13.5-.29.8-.28.55-1.07.82-2.37.82-1.42,0-2.25-.37-2.49-1.12-.12-.34-.18-.87-.18-1.58v-6.16h-8.04v6.19c0,.47-.02.81-.05,1.05-.03.23-.13.5-.29.8-.28.55-1.07.82-2.37.82-1.42,0-2.25-.37-2.49-1.12-.12-.34-.18-.87-.18-1.58V10.92c0-.46.02-.81.05-1.05.03-.23.13-.5.29-.8.28-.55,1.07-.82,2.37-.82,1.42,0,2.25.37,2.52,1.12.1.34.15.87.15,1.58v6.19h8.04v-6.22c0-.46.02-.81.05-1.05.03-.23.13-.5.29-.8Z"/>
<path class="cls-3" d="M127.21,25.1h9.34c.47,0,.81.02,1.05.05.23.03.5.13.8.29.55.28.82,1.07.82,2.37,0,1.42-.37,2.25-1.12,2.49-.34.12-.87.18-1.58.18h-12.01c-1.42,0-2.25-.38-2.49-1.15-.12-.32-.18-.84-.18-1.55V10.9c0-1.03.19-1.73.58-2.11.38-.37,1.11-.56,2.18-.56h11.95c.47,0,.81.02,1.05.05.23.03.5.13.8.29.55.28.82,1.07.82,2.37,0,1.42-.37,2.25-1.12,2.49-.34.12-.87.18-1.58.18h-9.31v3.06h6.01c.46,0,.81.02,1.05.05.23.03.5.13.8.29.55.28.82,1.07.82,2.37,0,1.42-.38,2.25-1.15,2.49-.34.12-.87.18-1.58.18h-5.95v3.06Z"/>
<path class="cls-3" d="M196.96,8.79c.99.69,1.49,1.35,1.49,2,0,.38-.23.92-.7,1.61l-6.55,9.8v5.79c0,.47-.02.81-.05,1.05-.03.23-.13.5-.29.8-.16.3-.43.52-.82.64-.38.12-.9.18-1.55.18s-1.16-.06-1.55-.18c-.38-.12-.66-.34-.82-.65-.16-.31-.26-.59-.29-.82-.03-.23-.05-.59-.05-1.08v-5.73l-6.55-9.8c-.47-.69-.7-1.22-.7-1.61,0-.65.44-1.27,1.33-1.87.89-.6,1.53-.9,1.91-.9s.69.08.91.24c.34.22.71.64,1.09,1.24l4.7,7.52,4.7-7.52c.38-.61.72-1.01,1-1.2s.61-.29.99-.29.97.25,1.77.76Z"/>
<g>
<path class="cls-3" d="M81.93,56.63c-.53-.65-.79-1.23-.79-1.74s.43-1.2,1.3-2.05c.51-.49,1.04-.73,1.61-.73s1.36.51,2.37,1.52c.28.34.69.67,1.21.99.53.31,1.01.47,1.46.47,1.88,0,2.82-.77,2.82-2.31,0-.46-.26-.85-.77-1.17-.52-.31-1.16-.54-1.93-.68-.77-.14-1.6-.37-2.49-.68-.89-.31-1.72-.68-2.49-1.11-.77-.42-1.41-1.1-1.93-2.02-.52-.92-.77-2.03-.77-3.32,0-1.78.66-3.33,1.99-4.66s3.13-1.99,5.42-1.99c1.21,0,2.32.16,3.32.47,1,.31,1.69.63,2.08.96l.76.58c.63.59.94,1.08.94,1.49s-.24.96-.73,1.67c-.69,1.01-1.4,1.52-2.12,1.52-.42,0-.95-.2-1.58-.61-.06-.04-.18-.14-.35-.3-.17-.16-.33-.29-.47-.39-.42-.26-.97-.39-1.62-.39s-1.2.16-1.64.47c-.43.31-.65.75-.65,1.3s.26,1.01.77,1.35c.52.34,1.16.58,1.93.7.77.12,1.61.31,2.52.56.91.25,1.75.56,2.52.93.77.36,1.41,1,1.93,1.9.52.9.77,2.01.77,3.32s-.26,2.47-.79,3.47c-.53,1-1.21,1.77-2.06,2.32-1.64,1.07-3.39,1.61-5.25,1.61-.95,0-1.85-.12-2.7-.35-.85-.23-1.54-.52-2.06-.86-1.07-.65-1.82-1.27-2.24-1.88l-.27-.33Z"/>
<path class="cls-3" d="M100.74,37.49h16.87c.65,0,1.12.08,1.43.23.3.15.51.39.61.71.1.32.15.75.15,1.27s-.05.95-.15,1.26c-.1.31-.27.53-.52.65-.36.18-.88.27-1.55.27h-5.79v15.26c0,.47-.02.81-.05,1.03s-.12.48-.27.77c-.15.29-.42.5-.8.62-.38.12-.89.18-1.52.18s-1.13-.06-1.5-.18c-.37-.12-.64-.33-.79-.62-.15-.29-.24-.56-.27-.79-.03-.23-.05-.58-.05-1.05v-15.23h-5.82c-.65,0-1.12-.08-1.43-.23-.3-.15-.51-.39-.61-.71-.1-.32-.15-.75-.15-1.27s.05-.95.15-1.26c.1-.31.27-.53.52-.65.36-.18.88-.27,1.55-.27Z"/>
<path class="cls-3" d="M135.99,38.34c.2-.32.5-.55.88-.67.38-.12.86-.18,1.44-.18s1.04.05,1.38.15c.34.1.61.22.79.36.18.14.31.35.39.64.12.34.18.87.18,1.58v9.16c0,2.67-.83,5.1-2.49,7.28-.81,1.03-1.85,1.87-3.12,2.5s-2.68.96-4.23.96-2.95-.32-4.22-.97c-1.26-.65-2.29-1.5-3.08-2.55-1.64-2.14-2.46-4.57-2.46-7.28v-9.13c0-.49.02-.84.05-1.08.03-.23.13-.5.29-.8.16-.3.43-.52.82-.64.38-.12.9-.18,1.55-.18s1.16.06,1.55.18c.38.12.65.33.79.64.24.47.36,1.1.36,1.91v9.1c0,1.23.3,2.41.91,3.52.3.57.76,1.02,1.37,1.36.61.34,1.32.52,2.15.52,1.48,0,2.58-.55,3.31-1.64.73-1.09,1.09-2.36,1.09-3.79v-9.28c0-.79.1-1.34.3-1.67Z"/>
<path class="cls-3" d="M146.18,37.49l5.61.03c2.93,0,5.51,1.06,7.74,3.17,2.22,2.11,3.34,4.71,3.34,7.8s-1.09,5.73-3.26,7.93c-2.17,2.2-4.81,3.31-7.9,3.31h-5.55c-1.23,0-2-.25-2.31-.76-.24-.42-.36-1.07-.36-1.94v-16.87c0-.49.02-.84.05-1.06s.13-.49.29-.79c.28-.55,1.07-.82,2.37-.82ZM151.79,54.35c1.46,0,2.77-.54,3.94-1.62,1.17-1.08,1.76-2.44,1.76-4.08s-.57-3.01-1.71-4.11c-1.14-1.1-2.48-1.65-4.02-1.65h-2.91v11.47h2.94Z"/>
<path class="cls-3" d="M164.84,40.19c0-.46.02-.81.05-1.05.03-.23.13-.5.29-.8.28-.55,1.07-.82,2.37-.82,1.42,0,2.25.37,2.52,1.12.1.34.15.87.15,1.58v16.87c0,.49-.02.84-.05,1.06s-.13.49-.29.79c-.28.55-1.07.82-2.37.82-1.42,0-2.25-.38-2.49-1.15-.12-.32-.18-.84-.18-1.55v-16.87Z"/>
<path class="cls-3" d="M183.07,37.24c2.99,0,5.59,1.08,7.8,3.25,2.2,2.16,3.31,4.85,3.31,8.05s-1.05,5.94-3.16,8.19c-2.1,2.26-4.69,3.38-7.77,3.38s-5.69-1.11-7.84-3.34c-2.15-2.22-3.23-4.87-3.23-7.95,0-1.68.3-3.25.91-4.72.61-1.47,1.42-2.7,2.43-3.69,1.01-.99,2.17-1.77,3.49-2.34,1.31-.57,2.67-.85,4.07-.85ZM177.55,48.68c0,1.8.58,3.26,1.74,4.38,1.16,1.12,2.46,1.68,3.9,1.68s2.73-.55,3.88-1.64c1.15-1.09,1.73-2.56,1.73-4.4s-.58-3.32-1.74-4.43c-1.16-1.11-2.46-1.67-3.9-1.67s-2.73.56-3.88,1.68c-1.15,1.12-1.73,2.58-1.73,4.38Z"/>
</g>
<g>
<path class="cls-3" d="M176.92,11.06c-.03-.23-.13-.5-.29-.8-.28-.55-1.07-.82-2.37-.82h-6.55c-1.78,0-3.51.65-5.19,1.94-.81.63-1.48,1.48-2,2.55-.53,1.07-.79,2.27-.79,3.58,0,2.29.76,4.17,2.28,5.64-.44,1.07-1.13,2.66-2.06,4.76-.3.73-.45,1.25-.45,1.58,0,.77.63,1.42,1.88,1.94.65.28,1.17.43,1.56.43s.72-.1.97-.29c.25-.19.44-.39.56-.59.2-.38.99-2.21,2.37-5.49l.94.06h3.82v3.43c0,.47.02.81.05,1.05.03.23.13.5.29.8.28.55,1.07.82,2.37.82,1.42,0,2.25-.37,2.49-1.12.12-.34.18-.87.18-1.58V12.11c0-.46-.02-.81-.05-1.05ZM172.81,19.44c-.09.14-.48.77-1.24.91-.2.04-.37.03-.48.02-.02.14-.04.26-.06.38-.16.83-.38,1.05-.57,1.07-.29.05-.51-.35-.93-.9-.23.01-.46.02-.69.02-.51,0-1.01-.03-1.49-.09-.25-.03-.5-.07-.74-.11-1.18-.32-2.03-1.27-2.03-2.4v-1.37c0-1.13.86-2.08,2.03-2.4.24-.04.49-.08.74-.11.48-.06.98-.09,1.49-.09s1.01.03,1.49.09c.25.03.5.07.74.11.6.16,1.12.49,1.49.93.34.41.55.92.55,1.47v1.37c0,.23-.01.66-.29,1.1Z"/>
<circle class="cls-2" cx="167.24" cy="17.67" r=".49"/>
<circle class="cls-2" cx="168.88" cy="17.71" r=".49"/>
<circle class="cls-2" cx="170.59" cy="17.71" r=".49"/>
</g>
<g>
<path class="cls-3" d="M141.01,8.24c.03-.23.13-.5.29-.8.28-.55,1.07-.82,2.37-.82h6.55c1.78,0,3.51.65,5.19,1.94.81.63,1.48,1.48,2,2.55.53,1.07.79,2.27.79,3.58,0,2.29-.76,4.17-2.28,5.64.44,1.07,1.13,2.66,2.06,4.76.3.73.45,1.25.45,1.58,0,.77-.63,1.42-1.88,1.94-.65.28-1.17.43-1.56.43s-.72-.1-.97-.29c-.25-.19-.44-.39-.56-.59-.2-.38-.99-2.21-2.37-5.49l-.94.06h-3.82v3.43c0,.47-.02.81-.05,1.05-.03.23-.13.5-.29.8-.28.55-1.07.82-2.37.82-1.42,0-2.25-.37-2.49-1.12-.12-.34-.18-.87-.18-1.58V9.28c0-.46.02-.81.05-1.05ZM145.12,16.62c.09.14.48.77,1.24.91.2.04.37.03.48.02.02.14.04.26.06.38.16.83.38,1.05.57,1.07.29.05.51-.35.93-.9.23.01.46.02.69.02.51,0,1.01-.03,1.49-.09.25-.03.5-.07.74-.11,1.18-.32,2.03-1.27,2.03-2.4v-1.37c0-1.13-.86-2.08-2.03-2.4-.24-.04-.49-.08-.74-.11-.48-.06-.98-.09-1.49-.09s-1.01.03-1.49.09c-.25.03-.5.07-.74.11-.6.16-1.12.49-1.49.93-.34.41-.55.92-.55,1.47v1.37c0,.23.01.66.29,1.1Z"/>
<circle class="cls-2" cx="150.69" cy="14.84" r=".49"/>
<circle class="cls-2" cx="149.05" cy="14.89" r=".49"/>
<circle class="cls-2" cx="147.35" cy="14.89" r=".49"/>
</g>
</g>
</g>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 9.5 KiB

View File

@@ -1,55 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg id="_图层_2" data-name="图层_2" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 84.39 115.44">
<defs>
<style>
.cls-1 {
fill: #ea5e5d;
}
.cls-2 {
fill: #23af69;
}
.cls-3 {
fill: #ea5756;
}
</style>
</defs>
<g id="_图层_1-2" data-name="图层_1">
<g>
<g>
<g>
<path class="cls-1" d="M25.31,51.21c-4.45,0-8.64-1.78-11.81-5.01-3.17-3.23-4.91-7.51-4.91-12.04s1.74-8.81,4.91-12.04,7.36-5.01,11.81-5.01,8.71,1.82,11.82,4.99c2.32,2.36,2.32,6.2,0,8.56-2.32,2.36-6.08,2.36-8.4,0-.9-.92-2.15-1.45-3.43-1.45-2.63,0-4.85,2.26-4.85,4.94s2.22,4.94,4.85,4.94c1.28,0,2.52-.53,3.43-1.45,2.32-2.36,6.08-2.36,8.4,0,2.32,2.36,2.32,6.2,0,8.56-3.11,3.17-7.42,4.99-11.82,4.99Z"/>
<path class="cls-1" d="M40.64,66.73c-4.45,0-8.64-1.78-11.81-5.01s-4.91-7.51-4.91-12.04,1.79-8.88,4.9-12.06c2.32-2.36,6.08-2.36,8.4,0,2.32,2.36,2.32,6.2,0,8.56-.9.92-1.42,2.19-1.42,3.49,0,2.68,2.22,4.94,4.85,4.94s4.85-2.26,4.85-4.94c0-.95-.23-2.31-1.32-3.43-3.13-3.19-4.92-7.6-4.92-12.09s1.74-8.81,4.91-12.04c3.17-3.23,7.36-5.01,11.81-5.01s8.64,1.78,11.81,5.01,4.91,7.51,4.91,12.04-1.79,8.88-4.9,12.06c-2.32,2.36-6.08,2.36-8.4,0-2.32-2.36-2.32-6.2,0-8.56.9-.92,1.42-2.19,1.42-3.49,0-2.68-2.22-4.94-4.85-4.94s-4.85,2.26-4.85,4.94c0,1.31.53,2.6,1.45,3.53,3.1,3.16,4.8,7.42,4.8,11.99s-1.74,8.81-4.91,12.04c-3.17,3.23-7.36,5.01-11.81,5.01Z"/>
</g>
<path class="cls-2" d="M40.64,19.09l-9.72-9.12c-1.5-1.4-1.57-3.75-.17-5.25,1.4-1.49,3.75-1.57,5.25-.17l3.89,3.65,5.53-6.83c1.29-1.59,3.63-1.84,5.22-.55,1.59,1.29,1.84,3.63.55,5.22l-10.56,13.05Z"/>
</g>
<g>
<path class="cls-3" d="M10.19,90.22l.39-.28c.49-.29.83-.43,1.03-.43.45,0,.93.4,1.44,1.21.32.5.47.9.47,1.21s-.1.55-.29.75c-.19.2-.42.38-.68.54-.26.16-.51.31-.74.45-.24.14-.72.33-1.45.56-.73.23-1.44.34-2.12.34s-1.37-.09-2.07-.27c-.7-.18-1.41-.48-2.15-.9-.74-.42-1.4-.94-1.99-1.55-.59-.61-1.07-1.39-1.45-2.35-.38-.96-.57-1.99-.57-3.11s.19-2.14.56-3.05c.37-.91.85-1.67,1.43-2.26.58-.6,1.25-1.09,1.99-1.5,1.41-.78,2.82-1.16,4.24-1.16.67,0,1.36.1,2.06.31.7.21,1.22.42,1.58.64l.52.3c.26.16.46.29.6.39.37.3.56.64.56,1.02s-.15.78-.45,1.2c-.56.78-1.06,1.16-1.51,1.16-.26,0-.62-.16-1.1-.47-.6-.49-1.41-.73-2.41-.73-.93,0-1.85.32-2.76.97-.43.32-.79.76-1.08,1.34-.29.57-.43,1.22-.43,1.95s.14,1.37.43,1.95c.29.57.65,1.03,1.1,1.36.88.63,1.79.95,2.74.95.45,0,.87-.06,1.26-.17.39-.11.68-.23.85-.34Z"/>
<path class="cls-3" d="M24.7,79.2c.11-.22.31-.37.58-.45.27-.09.62-.13,1.03-.13s.75.04.99.11c.24.07.43.16.56.26.13.1.23.24.3.43.07.24.11.62.11,1.12v11.95c0,.33-.01.58-.03.74-.02.17-.09.36-.2.57-.2.39-.76.58-1.68.58-1.01,0-1.59-.27-1.77-.8-.09-.24-.13-.62-.13-1.12v-4.37h-5.71v4.39c0,.33-.01.58-.03.74-.02.17-.09.36-.2.57-.2.39-.76.58-1.68.58-1.01,0-1.59-.27-1.77-.8-.09-.24-.13-.62-.13-1.12v-11.95c0-.33.01-.58.03-.74.02-.17.09-.36.2-.57.2-.39.76-.58,1.68-.58,1.01,0,1.6.27,1.79.8.07.24.11.62.11,1.12v4.39h5.71v-4.42c0-.33.01-.58.03-.74.02-.17.09-.36.2-.57Z"/>
<path class="cls-3" d="M33.82,90.58h6.63c.33,0,.58.01.74.03.17.02.36.09.57.2.39.2.58.76.58,1.68,0,1.01-.27,1.59-.8,1.77-.24.09-.62.13-1.12.13h-8.53c-1.01,0-1.59-.27-1.77-.82-.09-.23-.13-.6-.13-1.1v-11.98c0-.73.14-1.23.41-1.5.27-.27.79-.4,1.55-.4h8.49c.33,0,.58.01.74.03.17.02.36.09.57.2.39.2.58.76.58,1.68,0,1.01-.27,1.59-.8,1.77-.24.09-.62.13-1.12.13h-6.61v2.18h4.26c.33,0,.58.01.74.03.17.02.36.09.57.2.39.2.58.76.58,1.68,0,1.01-.27,1.59-.82,1.77-.24.09-.62.13-1.12.13h-4.22v2.18Z"/>
<path class="cls-3" d="M83.34,79c.7.49,1.06.96,1.06,1.42,0,.27-.17.65-.5,1.14l-4.65,6.96v4.11c0,.33-.01.58-.03.74-.02.17-.09.36-.2.57-.11.22-.31.37-.58.45-.27.09-.64.13-1.1.13s-.83-.04-1.1-.13c-.27-.09-.47-.24-.58-.46-.11-.22-.18-.42-.2-.58-.02-.17-.03-.42-.03-.76v-4.07l-4.65-6.96c-.33-.49-.5-.87-.5-1.14,0-.46.32-.9.95-1.32.63-.42,1.08-.64,1.36-.64s.49.06.65.17c.24.16.5.45.78.88l3.34,5.34,3.34-5.34c.27-.43.51-.71.71-.85s.43-.2.7-.2.69.18,1.26.54Z"/>
<g>
<path class="cls-3" d="M1.66,112.96c-.37-.46-.56-.87-.56-1.24s.31-.85.93-1.45c.36-.34.74-.52,1.14-.52s.96.36,1.68,1.08c.2.24.49.48.86.7.37.22.72.33,1.03.33,1.34,0,2-.55,2-1.64,0-.33-.18-.61-.55-.83-.37-.22-.82-.38-1.37-.48-.55-.1-1.13-.26-1.77-.48-.63-.22-1.22-.48-1.77-.79-.55-.3-1-.78-1.37-1.43-.37-.65-.55-1.44-.55-2.36,0-1.26.47-2.37,1.41-3.31s2.22-1.41,3.84-1.41c.86,0,1.65.11,2.36.33.71.22,1.2.45,1.48.68l.54.41c.45.42.67.77.67,1.06s-.17.68-.52,1.18c-.49.72-.99,1.08-1.51,1.08-.3,0-.67-.14-1.12-.43-.04-.03-.13-.1-.25-.22-.12-.11-.23-.21-.33-.28-.3-.19-.69-.28-1.15-.28s-.85.11-1.16.33c-.31.22-.46.53-.46.93s.18.71.55.96c.37.24.82.41,1.37.5.55.09,1.14.22,1.79.4.65.18,1.24.4,1.79.66.55.26,1,.71,1.37,1.35.37.64.55,1.42.55,2.36s-.19,1.76-.56,2.47c-.37.71-.86,1.26-1.46,1.65-1.16.76-2.4,1.14-3.73,1.14-.68,0-1.31-.08-1.92-.25-.6-.17-1.09-.37-1.46-.61-.76-.46-1.29-.9-1.59-1.34l-.19-.24Z"/>
<path class="cls-3" d="M15.02,99.37h11.98c.46,0,.8.05,1.01.16.22.11.36.28.43.51.07.23.11.53.11.9s-.04.67-.11.89c-.07.22-.19.38-.37.46-.26.13-.62.19-1.1.19h-4.11v10.83c0,.33-.01.57-.03.73s-.09.34-.19.55c-.11.21-.3.36-.57.44-.27.09-.63.13-1.08.13s-.8-.04-1.07-.13c-.27-.09-.45-.23-.56-.44-.11-.21-.17-.4-.19-.56-.02-.17-.03-.41-.03-.74v-10.81h-4.14c-.46,0-.8-.05-1.01-.16-.22-.11-.36-.28-.43-.51-.07-.23-.11-.53-.11-.9s.04-.67.11-.89c.07-.22.19-.38.37-.46.26-.13.62-.19,1.1-.19Z"/>
<path class="cls-3" d="M40.05,99.98c.14-.23.35-.39.62-.47.27-.09.61-.13,1.02-.13s.74.04.98.11c.24.07.43.16.56.26.13.1.22.25.28.45.09.24.13.62.13,1.12v6.5c0,1.9-.59,3.62-1.77,5.17-.57.73-1.31,1.32-2.22,1.78s-1.91.68-3,.68-2.1-.23-2.99-.69c-.9-.46-1.63-1.06-2.19-1.81-1.16-1.52-1.74-3.25-1.74-5.17v-6.48c0-.34.01-.6.03-.76.02-.17.09-.36.2-.57.11-.22.31-.37.58-.45.27-.09.64-.13,1.1-.13s.83.04,1.1.13c.27.09.46.24.56.45.17.33.26.78.26,1.36v6.46c0,.88.22,1.71.65,2.5.22.4.54.72.97.97.43.24.94.37,1.53.37,1.05,0,1.83-.39,2.35-1.16.52-.78.78-1.67.78-2.69v-6.59c0-.56.07-.95.22-1.18Z"/>
<path class="cls-3" d="M47.28,99.37l3.98.02c2.08,0,3.91.75,5.49,2.25,1.58,1.5,2.37,3.35,2.37,5.54s-.77,4.07-2.32,5.63c-1.54,1.57-3.41,2.35-5.61,2.35h-3.94c-.88,0-1.42-.18-1.64-.54-.17-.3-.26-.76-.26-1.38v-11.98c0-.34.01-.6.03-.75s.09-.34.2-.56c.2-.39.76-.58,1.68-.58ZM51.27,111.35c1.03,0,1.97-.38,2.8-1.15.83-.77,1.25-1.73,1.25-2.9s-.41-2.14-1.22-2.92c-.81-.78-1.76-1.17-2.85-1.17h-2.07v8.14h2.09Z"/>
<path class="cls-3" d="M60.53,101.29c0-.33.01-.58.03-.74.02-.17.09-.36.2-.57.2-.39.76-.58,1.68-.58,1.01,0,1.6.27,1.79.8.07.24.11.62.11,1.12v11.98c0,.34-.01.6-.03.75s-.09.34-.2.56c-.2.39-.76.58-1.68.58-1.01,0-1.59-.27-1.77-.82-.09-.23-.13-.6-.13-1.1v-11.98Z"/>
<path class="cls-3" d="M73.47,99.2c2.13,0,3.97.77,5.54,2.3,1.57,1.54,2.35,3.44,2.35,5.72s-.75,4.21-2.24,5.82c-1.49,1.6-3.33,2.4-5.51,2.4s-4.04-.79-5.57-2.37c-1.53-1.58-2.29-3.46-2.29-5.64,0-1.19.22-2.31.65-3.35.43-1.04,1.01-1.91,1.72-2.62.72-.7,1.54-1.26,2.48-1.66.93-.4,1.9-.6,2.89-.6ZM69.55,107.32c0,1.28.41,2.32,1.24,3.11.83.8,1.75,1.2,2.77,1.2s1.94-.39,2.76-1.16c.82-.78,1.23-1.82,1.23-3.12s-.41-2.35-1.24-3.14c-.83-.79-1.75-1.18-2.77-1.18s-1.94.4-2.76,1.2c-.82.8-1.23,1.83-1.23,3.11Z"/>
</g>
<g>
<path class="cls-3" d="M69.11,80.61c-.02-.17-.09-.36-.2-.57-.2-.39-.76-.58-1.68-.58h-4.65c-1.26,0-2.49.46-3.68,1.38-.57.45-1.05,1.05-1.42,1.81-.37.76-.56,1.61-.56,2.54,0,1.62.54,2.96,1.62,4.01-.32.76-.8,1.89-1.46,3.38-.22.52-.32.89-.32,1.12,0,.55.45,1.01,1.34,1.38.46.2.83.3,1.11.3s.51-.07.69-.2c.18-.14.31-.28.4-.42.14-.27.7-1.57,1.68-3.9l.67.04h2.71v2.43c0,.33.01.58.03.74.02.17.09.36.2.57.2.39.76.58,1.68.58,1.01,0,1.59-.27,1.77-.8.09-.24.13-.62.13-1.12v-11.95c0-.33-.01-.58-.03-.74ZM66.19,86.56c-.06.1-.34.54-.88.65-.14.03-.26.02-.34.02-.01.1-.03.19-.04.27-.11.59-.27.74-.4.76-.21.03-.36-.25-.66-.64-.16,0-.32.01-.49.01-.36,0-.72-.02-1.06-.06-.18-.02-.35-.05-.52-.08-.84-.22-1.44-.9-1.44-1.7v-.97c0-.8.61-1.48,1.44-1.7.17-.03.34-.06.52-.08.34-.04.69-.06,1.06-.06s.72.02,1.06.06c.18.02.35.05.52.08.43.12.8.35,1.06.66.24.29.39.65.39,1.05v.97c0,.16,0,.47-.21.78Z"/>
<circle class="cls-2" cx="62.23" cy="85.3" r=".35"/>
<circle class="cls-2" cx="63.4" cy="85.33" r=".35"/>
<circle class="cls-2" cx="64.61" cy="85.33" r=".35"/>
</g>
<g>
<path class="cls-3" d="M43.62,78.61c.02-.17.09-.36.2-.57.2-.39.76-.58,1.68-.58h4.65c1.26,0,2.49.46,3.68,1.38.57.45,1.05,1.05,1.42,1.81.37.76.56,1.61.56,2.54,0,1.62-.54,2.96-1.62,4.01.32.76.8,1.89,1.46,3.38.22.52.32.89.32,1.12,0,.55-.45,1.01-1.34,1.38-.46.2-.83.3-1.11.3s-.51-.07-.69-.2c-.18-.14-.31-.28-.4-.42-.14-.27-.7-1.57-1.68-3.9l-.67.04h-2.71v2.43c0,.33-.01.58-.03.74-.02.17-.09.36-.2.57-.2.39-.76.58-1.68.58-1.01,0-1.59-.27-1.77-.8-.09-.24-.13-.62-.13-1.12v-11.95c0-.33.01-.58.03-.74ZM46.53,84.56c.06.1.34.54.88.65.14.03.26.02.34.02.01.1.03.19.04.27.11.59.27.74.4.76.21.03.36-.25.66-.64.16,0,.32.01.49.01.36,0,.72-.02,1.06-.06.18-.02.35-.05.52-.08.84-.22,1.44-.9,1.44-1.7v-.97c0-.8-.61-1.48-1.44-1.7-.17-.03-.34-.06-.52-.08-.34-.04-.69-.06-1.06-.06s-.72.02-1.06.06c-.18.02-.35.05-.52.08-.43.12-.8.35-1.06.66-.24.29-.39.65-.39,1.05v.97c0,.16,0,.47.21.78Z"/>
<circle class="cls-2" cx="50.49" cy="83.3" r=".35"/>
<circle class="cls-2" cx="49.32" cy="83.33" r=".35"/>
<circle class="cls-2" cx="48.11" cy="83.33" r=".35"/>
</g>
</g>
</g>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 9.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.1 KiB

View File

@@ -42,35 +42,6 @@
-webkit-app-region: no-drag;
}
.segmented-tab {
.ant-segmented-item {
overflow: hidden;
}
.ant-segmented-item-selected {
background-color: var(--color-background-mute);
}
.ant-segmented-item-label {
align-items: center;
display: flex;
flex-direction: row;
justify-content: center;
font-size: 13px;
}
.iconfont {
font-size: 13px;
margin-left: -2px;
}
.anticon-setting {
font-size: 12px;
}
.icon-business-smart-assistant {
margin-right: -2px;
}
.ant-segmented-item-icon + * {
margin-left: 4px;
}
}
.message-attachments {
.ant-upload-list-item:hover {
background-color: initial !important;

View File

@@ -10,8 +10,8 @@
--color-white-mute: rgba(255, 255, 255, 0.94);
--color-black: #151515;
--color-black-soft: #202020;
--color-black-mute: #262626;
--color-black-soft: #222222;
--color-black-mute: #333333;
--color-gray-1: #515c67;
--color-gray-2: #414853;
@@ -32,14 +32,17 @@
--color-text: var(--color-text-1);
--color-icon: #ffffff99;
--color-icon-white: #ffffff;
--color-border: #ffffff24;
--color-border-soft: #ffffff20;
--color-border: #ffffff22;
--color-border-soft: #ffffff11;
--color-border-mute: #ffffff11;
--color-error: #f44336;
--color-link: #1677ff;
--color-code-background: #323232;
--color-hover: rgba(40, 40, 40, 1);
--color-active: rgba(55, 55, 55, 1);
--color-frame-border: #333;
--color-group-background: var(--color-background-soft);
--color-reference-background: #0b0e12;
--navbar-background-mac: rgba(30, 30, 30, 0.6);
--navbar-background: rgba(30, 30, 30);
@@ -88,13 +91,16 @@ body[theme-mode='light'] {
--color-icon: #00000099;
--color-icon-white: #000000;
--color-border: #00000028;
--color-border-soft: #00000028;
--color-border-mute: #00000011;
--color-border-soft: #00000020;
--color-border-mute: #00000010;
--color-error: #f44336;
--color-link: #1677ff;
--color-code-background: #e3e3e3;
--color-hover: var(--color-white-mute);
--color-active: var(--color-white-soft);
--color-frame-border: #ddd;
--color-group-background: var(--color-white);
--color-reference-background: #f1f7ff;
--navbar-background-mac: rgba(255, 255, 255, 0.6);
--navbar-background: rgba(255, 255, 255);
@@ -165,19 +171,10 @@ body,
border-top: 0.5px solid var(--color-border);
}
body[os='mac'] {
#content-container {
border-top-left-radius: 10px;
border-bottom-left-radius: 10px;
border-left: 0.5px solid var(--color-border);
box-shadow: 0 0 15px 1px rgba(0, 0, 0, 0.05);
}
}
body[os='windows'] {
#app-sidebar {
border-right: 0.5px solid var(--color-border);
}
#content-container {
border-top-left-radius: 12px;
border-left: 0.5px solid var(--color-border);
box-shadow: -2px 0px 20px -4px rgba(0, 0, 0, 0.08);
}
.loader {

View File

@@ -66,6 +66,10 @@
}
}
ul {
list-style: initial;
}
ul,
ol {
padding-left: 1.5em;
@@ -225,11 +229,24 @@
.footnotes {
margin-top: 1em;
margin-bottom: 1em;
padding-top: 1em;
border-top: 1px solid var(--color-border);
background-color: var(--color-reference-background);
border-radius: 8px;
padding: 8px 12px;
h4 {
margin-bottom: 5px;
font-size: 12px;
}
ol {
padding-left: 1em;
margin: 0;
li:last-child {
margin-bottom: 0;
}
}
li {

View File

@@ -1,21 +1,21 @@
:root {
--color-scrollbar-thumb: rgba(255, 255, 255, 0.15);
--color-scrollbar-thumb-hover: rgba(255, 255, 255, 0.2);
--color-scrollbar-thumb-right: rgba(255, 255, 255, 0.25);
--color-scrollbar-thumb-right-hover: rgba(255, 255, 255, 0.35);
--color-scrollbar-thumb-right: rgba(255, 255, 255, 0.18);
--color-scrollbar-thumb-right-hover: rgba(255, 255, 255, 0.25);
}
body[theme-mode='light'] {
--color-scrollbar-thumb: rgba(0, 0, 0, 0.15);
--color-scrollbar-thumb-hover: rgba(0, 0, 0, 0.2);
--color-scrollbar-thumb-right: rgba(0, 0, 0, 0.25);
--color-scrollbar-thumb-right-hover: rgba(0, 0, 0, 0.35);
--color-scrollbar-thumb-right: rgba(0, 0, 0, 0.18);
--color-scrollbar-thumb-right-hover: rgba(0, 0, 0, 0.25);
}
/* 全局初始化滚动条样式 */
::-webkit-scrollbar {
width: 5px;
height: 5px;
width: 6px;
height: 6px;
}
::-webkit-scrollbar-track {

View File

@@ -1,210 +0,0 @@
import { PlusOutlined, QuestionCircleOutlined } from '@ant-design/icons'
import { HStack } from '@renderer/components/Layout'
import { DEFAULT_CONTEXTCOUNT, DEFAULT_TEMPERATURE } from '@renderer/config/constant'
import { SettingRow } from '@renderer/pages/settings'
import { Assistant, AssistantSettings } from '@renderer/types'
import { Button, Col, Divider, Row, Slider, Switch, Tooltip } from 'antd'
import { FC, useState } from 'react'
import { useTranslation } from 'react-i18next'
import styled from 'styled-components'
import ModelAvatar from '../Avatar/ModelAvatar'
import SelectModelPopup from '../Popups/SelectModelPopup'
interface Props {
assistant: Assistant
updateAssistant: (assistant: Assistant) => void
updateAssistantSettings: (settings: Partial<AssistantSettings>) => void
}
const AssistantModelSettings: FC<Props> = ({ assistant, updateAssistant, updateAssistantSettings }) => {
const [temperature, setTemperature] = useState(assistant?.settings?.temperature ?? DEFAULT_TEMPERATURE)
const [contextCount, setContextCount] = useState(assistant?.settings?.contextCount ?? DEFAULT_CONTEXTCOUNT)
const [enableMaxTokens, setEnableMaxTokens] = useState(assistant?.settings?.enableMaxTokens ?? false)
const [maxTokens, setMaxTokens] = useState(assistant?.settings?.maxTokens ?? 0)
const [autoResetModel, setAutoResetModel] = useState(assistant?.settings?.autoResetModel ?? false)
const [streamOutput, setStreamOutput] = useState(assistant?.settings?.streamOutput ?? true)
const [defaultModel, setDefaultModel] = useState(assistant?.defaultModel)
const { t } = useTranslation()
const onTemperatureChange = (value) => {
if (!isNaN(value as number)) {
updateAssistantSettings({ temperature: value })
}
}
const onContextCountChange = (value) => {
if (!isNaN(value as number)) {
updateAssistantSettings({ contextCount: value })
}
}
const onMaxTokensChange = (value) => {
if (!isNaN(value as number)) {
updateAssistantSettings({ maxTokens: value })
}
}
const onReset = () => {
setTemperature(DEFAULT_TEMPERATURE)
setContextCount(DEFAULT_CONTEXTCOUNT)
setEnableMaxTokens(false)
setMaxTokens(0)
setStreamOutput(true)
updateAssistantSettings({
temperature: DEFAULT_TEMPERATURE,
contextCount: DEFAULT_CONTEXTCOUNT,
enableMaxTokens: false,
maxTokens: 0,
streamOutput: true
})
}
const onSelectModel = async () => {
const selectedModel = await SelectModelPopup.show({ model: assistant?.model })
if (selectedModel) {
setDefaultModel(selectedModel)
updateAssistant({
...assistant,
defaultModel: selectedModel
})
}
}
return (
<Container>
<Row align="middle" style={{ marginBottom: 10 }}>
<Label style={{ marginBottom: 10 }}>{t('assistants.settings.default_model')}</Label>
<Col span={24}>
<HStack alignItems="center">
<Button
icon={defaultModel ? <ModelAvatar model={defaultModel} size={20} /> : <PlusOutlined />}
onClick={onSelectModel}>
{defaultModel ? defaultModel.name : t('agents.edit.model.select.title')}
</Button>
</HStack>
</Col>
</Row>
<Divider style={{ margin: '10px 0' }} />
<SettingRow style={{ minHeight: 30 }}>
<Label>
{t('assistants.settings.auto_reset_model')}{' '}
<Tooltip title={t('assistants.settings.auto_reset_model.tip')}>
<QuestionIcon />
</Tooltip>
</Label>
<Switch
value={autoResetModel}
onChange={(checked) => {
setAutoResetModel(checked)
updateAssistantSettings({ autoResetModel: checked })
}}
/>
</SettingRow>
<Divider style={{ margin: '10px 0' }} />
<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={24}>
<Slider
min={0}
max={2}
onChange={setTemperature}
onChangeComplete={onTemperatureChange}
value={typeof temperature === 'number' ? temperature : 0}
step={0.1}
/>
</Col>
</Row>
<Row align="middle">
<Label>
{t('chat.settings.context_count')}{' '}
<Tooltip title={t('chat.settings.context_count.tip')}>
<QuestionIcon />
</Tooltip>
</Label>
</Row>
<Row align="middle" gutter={10}>
<Col span={24}>
<Slider
min={0}
max={20}
onChange={setContextCount}
onChangeComplete={onContextCountChange}
value={typeof contextCount === 'number' ? contextCount : 0}
step={1}
/>
</Col>
</Row>
<Row align="middle" justify="space-between">
<HStack alignItems="center">
<Label>{t('chat.settings.max_tokens')}</Label>
<Tooltip title={t('chat.settings.max_tokens.tip')}>
<QuestionIcon />
</Tooltip>
</HStack>
<Switch
checked={enableMaxTokens}
onChange={(enabled) => {
setEnableMaxTokens(enabled)
updateAssistantSettings({ enableMaxTokens: enabled })
}}
/>
</Row>
<Row align="middle" gutter={10}>
<Col span={24}>
<Slider
disabled={!enableMaxTokens}
min={0}
max={32000}
onChange={setMaxTokens}
onChangeComplete={onMaxTokensChange}
value={typeof maxTokens === 'number' ? maxTokens : 0}
step={100}
/>
</Col>
</Row>
<SettingRow>
<Label>{t('model.stream_output')}</Label>
<Switch
checked={streamOutput}
onChange={(checked) => {
setStreamOutput(checked)
updateAssistantSettings({ streamOutput: checked })
}}
/>
</SettingRow>
<Divider style={{ margin: '15px 0' }} />
<HStack justifyContent="flex-end">
<Button onClick={onReset} style={{ width: 80 }} danger type="primary">
{t('chat.settings.reset')}
</Button>
</HStack>
</Container>
)
}
const Container = styled.div`
display: flex;
flex: 1;
flex-direction: column;
overflow: hidden;
padding: 5px;
`
const Label = styled.p`
margin-right: 5px;
font-weight: 500;
`
const QuestionIcon = styled(QuestionCircleOutlined)`
font-size: 12px;
cursor: pointer;
color: var(--color-text-3);
`
export default AssistantModelSettings

View File

@@ -0,0 +1,15 @@
import { GlobalOutlined } from '@ant-design/icons'
import React, { FC } from 'react'
import styled from 'styled-components'
const WebSearchIcon: FC<React.DetailedHTMLProps<React.HTMLAttributes<HTMLElement>, HTMLElement>> = (props) => {
return <Icon {...(props as any)} />
}
const Icon = styled(GlobalOutlined)`
color: var(--color-link);
font-size: 12px;
margin-left: 4px;
`
export default WebSearchIcon

View File

@@ -0,0 +1,35 @@
// src/renderer/src/components/IndicatorLight.tsx
import React from 'react'
import styled from 'styled-components'
interface IndicatorLightProps {
color: string
}
const Light = styled.div<{ color: string }>`
width: 8px;
height: 8px;
border-radius: 50%;
background-color: ${({ color }) => color};
box-shadow: 0 0 6px ${({ color }) => color};
animation: pulse 2s infinite;
@keyframes pulse {
0% {
opacity: 1;
}
50% {
opacity: 0.6;
}
100% {
opacity: 1;
}
}
`
const IndicatorLight: React.FC<IndicatorLightProps> = ({ color }) => {
const actualColor = color === 'green' ? '#22c55e' : color
return <Light color={actualColor} />
}
export default IndicatorLight

View File

@@ -0,0 +1,84 @@
import { ReactNode } from 'react'
import styled from 'styled-components'
interface ListItemProps {
active?: boolean
icon?: ReactNode
title: string
subtitle?: string
onClick?: () => void
}
const ListItem = ({ active, icon, title, subtitle, onClick }: ListItemProps) => {
const borderRadius = subtitle ? '10px' : '16px'
return (
<ListItemContainer className={active ? 'active' : ''} onClick={onClick} style={{ borderRadius }}>
<ListItemContent>
{icon && <IconWrapper>{icon}</IconWrapper>}
<TextContainer>
<TitleText>{title}</TitleText>
{subtitle && <SubtitleText>{subtitle}</SubtitleText>}
</TextContainer>
</ListItemContent>
</ListItemContainer>
)
}
const ListItemContainer = styled.div`
padding: 7px 12px;
border-radius: 16px;
font-size: 13px;
display: flex;
flex-direction: column;
justify-content: space-between;
position: relative;
font-family: Ubuntu;
cursor: pointer;
border: 1px solid transparent;
&:hover {
background-color: var(--color-background-soft);
}
&.active {
background-color: var(--color-background-soft);
border: 1px solid var(--color-border-soft);
}
`
const ListItemContent = styled.div`
display: flex;
align-items: center;
gap: 8px;
overflow: hidden;
font-size: 13px;
`
const IconWrapper = styled.span`
margin-right: 8px;
`
const TextContainer = styled.div`
display: flex;
flex-direction: column;
overflow: hidden;
`
const TitleText = styled.div`
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
`
const SubtitleText = styled.div`
font-size: 10px;
color: var(--color-text-soft);
margin-top: 2px;
display: -webkit-box;
-webkit-line-clamp: 1;
-webkit-box-orient: vertical;
overflow: hidden;
color: var(--color-text-3);
`
export default ListItem

View File

@@ -114,7 +114,15 @@ const PopupContainer: React.FC<Props> = ({ app, resolve }) => {
<BeatLoader color="var(--color-text-2)" size="10" style={{ marginTop: 15 }} />
</EmptyView>
)}
{opened && <webview src={app.url} ref={webviewRef} style={WebviewStyle} allowpopups={'true' as any} />}
{opened && (
<webview
src={app.url}
ref={webviewRef}
style={WebviewStyle}
allowpopups={'true' as any}
partition="persist:webview"
/>
)}
</Drawer>
)
}

View File

@@ -32,9 +32,22 @@ const PopupContainer: React.FC<Props> = ({ resolve }) => {
const agents = useMemo(() => {
const allAgents = [...userAgents, ...systemAgents] as Agent[]
const list = [defaultAssistant, ...allAgents.filter((agent) => !assistants.map((a) => a.id).includes(agent.id))]
return searchText
const filtered = searchText
? list.filter((agent) => agent.name.toLowerCase().includes(searchText.trim().toLocaleLowerCase()))
: list
if (searchText.trim()) {
const newAgent: Agent = {
id: 'new',
name: searchText.trim(),
prompt: '',
topics: [],
type: 'assistant',
emoji: '⭐️'
}
return [newAgent, ...filtered]
}
return filtered
}, [assistants, defaultAssistant, searchText, userAgents])
const onCreateAssistant = async (agent: Agent) => {
@@ -72,7 +85,14 @@ const PopupContainer: React.FC<Props> = ({ resolve }) => {
onCancel={onCancel}
afterClose={onClose}
transitionName="ant-move-up"
styles={{ content: { borderRadius: 20, padding: 0, overflow: 'hidden', paddingBottom: 20 } }}
styles={{
content: {
borderRadius: 20,
padding: 0,
overflow: 'hidden',
paddingBottom: 20
}
}}
closeIcon={null}
footer={null}>
<HStack style={{ padding: '0 12px', marginTop: 5 }}>
@@ -105,6 +125,7 @@ const PopupContainer: React.FC<Props> = ({ resolve }) => {
</HStack>
{agent.id === 'default' && <Tag color="green">{t('agents.tag.system')}</Tag>}
{agent.type === 'agent' && <Tag color="orange">{t('agents.tag.agent')}</Tag>}
{agent.id === 'new' && <Tag color="green">{t('agents.tag.new')}</Tag>}
</AgentItem>
))}
</Container>
@@ -148,7 +169,7 @@ const SearchIcon = styled.div`
flex-direction: row;
justify-content: center;
align-items: center;
background-color: var(--color-background-soft);
background-color: var(--color-background-mute);
margin-right: 2px;
`

View File

@@ -0,0 +1,65 @@
import { Center } from '@renderer/components/Layout'
import { getAllMinApps } from '@renderer/config/minapps'
import App from '@renderer/pages/apps/App'
import { Popover } from 'antd'
import { Empty } from 'antd'
import { isEmpty } from 'lodash'
import { FC, useState } from 'react'
import { useHotkeys } from 'react-hotkeys-hook'
import styled from 'styled-components'
import Scrollbar from '../Scrollbar'
interface Props {
children: React.ReactNode
}
const AppStorePopover: FC<Props> = ({ children }) => {
const [open, setOpen] = useState(false)
const apps = getAllMinApps()
useHotkeys('esc', () => {
setOpen(false)
})
const handleClose = () => {
setOpen(false)
}
const content = (
<PopoverContent>
<AppsContainer>
{apps.map((app) => (
<App key={app.id} app={app} onClick={handleClose} size={50} />
))}
{isEmpty(apps) && (
<Center>
<Empty />
</Center>
)}
</AppsContainer>
</PopoverContent>
)
return (
<Popover
open={open}
onOpenChange={setOpen}
content={content}
trigger="click"
placement="bottomRight"
overlayInnerStyle={{ padding: 25 }}>
{children}
</Popover>
)
}
const PopoverContent = styled(Scrollbar)``
const AppsContainer = styled.div`
display: grid;
grid-template-columns: repeat(6, minmax(90px, 1fr));
gap: 25px;
`
export default AppStorePopover

View File

@@ -1,4 +1,5 @@
import { Input, InputProps, Modal } from 'antd'
import { Input, Modal } from 'antd'
import { TextAreaProps } from 'antd/es/input'
import { useState } from 'react'
import { Box } from '../Layout'
@@ -9,11 +10,11 @@ interface PromptPopupShowParams {
message: string
defaultValue?: string
inputPlaceholder?: string
inputProps?: InputProps
inputProps?: TextAreaProps
}
interface Props extends PromptPopupShowParams {
resolve: (value: string) => void
resolve: (value: any) => void
}
const PromptPopupContainer: React.FC<Props> = ({
@@ -29,36 +30,42 @@ const PromptPopupContainer: React.FC<Props> = ({
const onOk = () => {
setOpen(false)
resolve(value)
}
const handleCancel = () => {
const onCancel = () => {
setOpen(false)
}
const onClose = () => {
resolve(value)
resolve(null)
}
PromptPopup.hide = onCancel
return (
<Modal title={title} open={open} onOk={onOk} onCancel={handleCancel} afterClose={onClose} centered>
<Modal title={title} open={open} onOk={onOk} onCancel={onCancel} afterClose={onClose} centered>
<Box mb={8}>{message}</Box>
<Input
<Input.TextArea
placeholder={inputPlaceholder}
value={value}
onChange={(e) => setValue(e.target.value)}
allowClear
autoFocus
onPressEnter={onOk}
rows={1}
{...inputProps}
/>
</Modal>
)
}
const TopViewKey = 'PromptPopup'
export default class PromptPopup {
static topviewId = 0
static hide() {
TopView.hide('PromptPopup')
TopView.hide(TopViewKey)
}
static show(props: PromptPopupShowParams) {
return new Promise<string>((resolve) => {
@@ -67,7 +74,7 @@ export default class PromptPopup {
{...props}
resolve={(v) => {
resolve(v)
this.hide()
TopView.hide(TopViewKey)
}}
/>,
'PromptPopup'

View File

@@ -0,0 +1,69 @@
import HistoryPage from '@renderer/pages/history/HistoryPage'
import { Modal } from 'antd'
import { useState } from 'react'
import { TopView } from '../TopView'
interface Props {
resolve: (data: any) => void
}
const PopupContainer: React.FC<Props> = ({ resolve }) => {
const [open, setOpen] = useState(true)
const onOk = () => {
setOpen(false)
}
const onCancel = () => {
setOpen(false)
}
const onClose = () => {
resolve({})
}
SearchPopup.hide = onCancel
return (
<Modal
open={open}
onOk={onOk}
onCancel={onCancel}
afterClose={onClose}
title={null}
width="920px"
transitionName="ant-move-down"
styles={{
content: {
padding: 0,
border: `1px solid var(--color-frame-border)`
},
body: { height: '85vh' }
}}
centered
footer={null}>
<HistoryPage />
</Modal>
)
}
export default class SearchPopup {
static topviewId = 0
static hide() {
TopView.hide('SearchPopup')
}
static show() {
return new Promise<any>((resolve) => {
TopView.show(
<PopupContainer
resolve={(v) => {
resolve(v)
TopView.hide('SearchPopup')
}}
/>,
'SearchPopup'
)
})
}
}

View File

@@ -1,17 +1,18 @@
import { PushpinOutlined, SearchOutlined } from '@ant-design/icons'
import VisionIcon from '@renderer/components/Icons/VisionIcon'
import { TopView } from '@renderer/components/TopView'
import { getModelLogo, isVisionModel } from '@renderer/config/models'
import { getModelLogo, isEmbeddingModel, isVisionModel, isWebSearchModel } from '@renderer/config/models'
import db from '@renderer/databases'
import { useProviders } from '@renderer/hooks/useProvider'
import { getModelUniqId } from '@renderer/services/ModelService'
import { Model } from '@renderer/types'
import { Avatar, Divider, Empty, Input, InputRef, Menu, MenuProps, Modal } from 'antd'
import { first, reverse, sortBy } from 'lodash'
import { first, sortBy } from 'lodash'
import { useEffect, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next'
import styled from 'styled-components'
import WebSearchIcon from '../Icons/WebSearchIcon'
import { HStack } from '../Layout'
import Scrollbar from '../Scrollbar'
@@ -47,7 +48,7 @@ const PopupContainer: React.FC<PopupContainerProps> = ({ model, resolve }) => {
await db.settings.put({ id: 'pinned:models', value: validPinnedModels })
}
setPinnedModels(validPinnedModels)
setPinnedModels(sortBy(validPinnedModels, ['group', 'name']))
}
loadPinnedModels()
}, [providers])
@@ -58,13 +59,14 @@ const PopupContainer: React.FC<PopupContainerProps> = ({ model, resolve }) => {
: [...pinnedModels, modelId]
await db.settings.put({ id: 'pinned:models', value: newPinnedModels })
setPinnedModels(newPinnedModels)
setPinnedModels(sortBy(newPinnedModels, ['group', 'name']))
}
const filteredItems: MenuItem[] = providers
.filter((p) => p.models && p.models.length > 0)
.map((p) => {
const filteredModels = reverse(sortBy(p.models, 'name'))
const filteredModels = sortBy(p.models, ['group', 'name'])
.filter((m) => !isEmbeddingModel(m))
.filter((m) =>
[m.name + m.provider + t('provider.' + p.id)].join('').toLowerCase().includes(searchText.toLowerCase())
)
@@ -73,7 +75,7 @@ const PopupContainer: React.FC<PopupContainerProps> = ({ model, resolve }) => {
label: (
<ModelItem>
<span>
{m?.name} {isVisionModel(m) && <VisionIcon />}
{m?.name} {isVisionModel(m) && <VisionIcon />} {isWebSearchModel(m) && <WebSearchIcon />}
</span>
<PinIcon
onClick={(e) => {
@@ -113,7 +115,7 @@ const PopupContainer: React.FC<PopupContainerProps> = ({ model, resolve }) => {
.flatMap((p) => p.models || [])
.filter((m) => pinnedModels.includes(getModelUniqId(m)))
.map((m) => ({
key: getModelUniqId(m),
key: getModelUniqId(m) + '_pinned',
label: (
<ModelItem>
{m?.name} {isVisionModel(m) && <VisionIcon />}
@@ -141,7 +143,7 @@ const PopupContainer: React.FC<PopupContainerProps> = ({ model, resolve }) => {
if (pinnedItems.length > 0) {
filteredItems.unshift({
key: 'pinned',
label: t('model.pinned'),
label: t('models.pinned'),
type: 'group',
children: pinnedItems
} as MenuItem)
@@ -168,7 +170,15 @@ const PopupContainer: React.FC<PopupContainerProps> = ({ model, resolve }) => {
onCancel={onCancel}
afterClose={onClose}
transitionName="ant-move-down"
styles={{ content: { borderRadius: 20, padding: 0, overflow: 'hidden', paddingBottom: 20 } }}
styles={{
content: {
borderRadius: 20,
padding: 0,
overflow: 'hidden',
paddingBottom: 20,
border: '1px solid var(--color-border)'
}
}}
closeIcon={null}
footer={null}>
<HStack style={{ padding: '0 12px', marginTop: 5 }}>
@@ -179,7 +189,7 @@ const PopupContainer: React.FC<PopupContainerProps> = ({ model, resolve }) => {
</SearchIcon>
}
ref={inputRef}
placeholder={t('model.search')}
placeholder={t('models.search')}
value={searchText}
onChange={(e) => setSearchText(e.target.value)}
allowClear

View File

@@ -27,17 +27,28 @@ const PopupContainer: React.FC<Props> = ({ title, resolve }) => {
resolve({})
}
TemplatePopup.hide = onCancel
return (
<Modal title={title} open={open} onOk={onOk} onCancel={onCancel} afterClose={onClose}>
<Modal
title={title}
open={open}
onOk={onOk}
onCancel={onCancel}
afterClose={onClose}
transitionName="ant-move-down"
centered>
<Box mb={8}>Name</Box>
</Modal>
)
}
const TopViewKey = 'TemplatePopup'
export default class TemplatePopup {
static topviewId = 0
static hide() {
TopView.hide('TemplatePopup')
TopView.hide(TopViewKey)
}
static show(props: ShowParams) {
return new Promise<any>((resolve) => {
@@ -46,10 +57,10 @@ export default class TemplatePopup {
{...props}
resolve={(v) => {
resolve(v)
this.hide()
TopView.hide(TopViewKey)
}}
/>,
'TemplatePopup'
TopViewKey
)
})
}

View File

@@ -4,6 +4,7 @@ import { TextAreaProps } from 'antd/lib/input'
import { TextAreaRef } from 'antd/lib/input/TextArea'
import { useEffect, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next'
import styled from 'styled-components'
import { TopView } from '../TopView'
@@ -11,13 +12,14 @@ interface ShowParams {
text: string
textareaProps?: TextAreaProps
modalProps?: ModalProps
children?: (props: { onOk?: () => void; onCancel?: () => void }) => React.ReactNode
}
interface Props extends ShowParams {
resolve: (data: any) => void
}
const PopupContainer: React.FC<Props> = ({ text, textareaProps, modalProps, resolve }) => {
const PopupContainer: React.FC<Props> = ({ text, textareaProps, modalProps, resolve, children }) => {
const [open, setOpen] = useState(true)
const { t } = useTranslation()
const [textValue, setTextValue] = useState(text)
@@ -49,6 +51,8 @@ const PopupContainer: React.FC<Props> = ({ text, textareaProps, modalProps, reso
setTimeout(resizeTextArea, 0)
}, [])
TextEditPopup.hide = onCancel
return (
<Modal
title={t('common.edit')}
@@ -71,14 +75,21 @@ const PopupContainer: React.FC<Props> = ({ text, textareaProps, modalProps, reso
onInput={resizeTextArea}
onChange={(e) => setTextValue(e.target.value)}
/>
<ChildrenContainer>{children && children({ onOk, onCancel })}</ChildrenContainer>
</Modal>
)
}
const TopViewKey = 'TextEditPopup'
const ChildrenContainer = styled.div`
position: relative;
`
export default class TextEditPopup {
static topviewId = 0
static hide() {
TopView.hide('TextEditPopup')
TopView.hide(TopViewKey)
}
static show(props: ShowParams) {
return new Promise<any>((resolve) => {
@@ -87,10 +98,10 @@ export default class TextEditPopup {
{...props}
resolve={(v) => {
resolve(v)
this.hide()
TopView.hide(TopViewKey)
}}
/>,
'TextEditPopup'
TopViewKey
)
})
}

View File

@@ -1,27 +1,41 @@
import { TranslationOutlined } from '@ant-design/icons'
import { LoadingOutlined, TranslationOutlined } from '@ant-design/icons'
import { useDefaultModel } from '@renderer/hooks/useAssistant'
import { fetchTranslate } from '@renderer/services/ApiService'
import { getDefaultTopic, getDefaultTranslateAssistant } from '@renderer/services/AssistantService'
import { getUserMessage } from '@renderer/services/MessagesService'
import { Button } from 'antd'
import { FC, useState } from 'react'
import { Button, Tooltip } from 'antd'
import { FC, useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next'
import styled from 'styled-components'
interface Props {
text?: string
onTranslated: (translatedText: string) => void
disabled?: boolean
style?: React.CSSProperties
isLoading?: boolean
}
const TranslateButton: FC<Props> = ({ text, onTranslated, disabled, style }) => {
const TranslateButton: FC<Props> = ({ text, onTranslated, disabled, style, isLoading }) => {
const { t } = useTranslation()
const { translateModel } = useDefaultModel()
const [isTranslating, setIsTranslating] = useState(false)
const translateConfirm = () => {
return window?.modal?.confirm({
title: t('translate.confirm.title'),
content: t('translate.confirm.content'),
centered: true
})
}
const handleTranslate = async () => {
if (!text?.trim()) return
if (!(await translateConfirm())) {
return
}
if (!translateModel) {
window.message.error({
content: t('translate.error.not_configured'),
@@ -39,7 +53,8 @@ const TranslateButton: FC<Props> = ({ text, onTranslated, disabled, style }) =>
const message = getUserMessage({
assistant,
topic: getDefaultTopic('default'),
type: 'text'
type: 'text',
content: text
})
const translatedText = await fetchTranslate({ message, assistant })
@@ -55,16 +70,53 @@ const TranslateButton: FC<Props> = ({ text, onTranslated, disabled, style }) =>
}
}
useEffect(() => {
setIsTranslating(isLoading ?? false)
}, [isLoading])
return (
<Button
icon={<TranslationOutlined style={{ fontSize: 14 }} />}
onClick={handleTranslate}
disabled={disabled || isTranslating}
loading={isTranslating}
style={style}
size="small"
/>
<Tooltip placement="top" title={t('chat.input.translate')} arrow>
<ToolbarButton onClick={handleTranslate} disabled={disabled || isTranslating} style={style} type="text">
{isTranslating ? <LoadingOutlined spin /> : <TranslationOutlined />}
</ToolbarButton>
</Tooltip>
)
}
const ToolbarButton = styled(Button)`
min-width: 30px;
height: 30px;
font-size: 17px;
border-radius: 50%;
transition: all 0.3s ease;
color: var(--color-icon);
display: flex;
flex-direction: row;
justify-content: center;
align-items: center;
padding: 0;
&.anticon,
&.iconfont {
transition: all 0.3s ease;
color: var(--color-icon);
}
&:hover {
background-color: var(--color-background-soft);
.anticon,
.iconfont {
color: var(--color-text-1);
}
}
&.active {
background-color: var(--color-primary) !important;
.anticon,
.iconfont {
color: var(--color-white-soft);
}
&:hover {
background-color: var(--color-primary);
}
}
`
export default TranslateButton

View File

@@ -38,7 +38,6 @@ const NavbarContainer = styled.div`
max-height: var(--navbar-height);
margin-left: ${isMac ? 'calc(var(--sidebar-width) * -1)' : 0};
padding-left: ${isMac ? 'var(--sidebar-width)' : 0};
transition: background-color 0.3s ease;
-webkit-app-region: drag;
`

View File

@@ -22,7 +22,7 @@ const Sidebar: FC = () => {
const { generating } = useRuntime()
const { t } = useTranslation()
const navigate = useNavigate()
const { windowStyle } = useSettings()
const { windowStyle, showMinappIcon, showFilesIcon } = useSettings()
const { theme, toggleTheme } = useTheme()
const isRoute = (path: string): string => (pathname === path ? 'active' : '')
@@ -51,59 +51,63 @@ const Sidebar: FC = () => {
<AvatarImg src={avatar || UserAvatar} draggable={false} className="nodrag" onClick={onEditUser} />
<MainMenus>
<Menus onClick={MinApp.onClose}>
<Tooltip title={t('assistants.title')} mouseEnterDelay={0.5} placement="right">
<Tooltip title={t('assistants.title')} mouseEnterDelay={0.8} placement="right">
<StyledLink onClick={() => to('/')}>
<Icon className={isRoute('/')}>
<i className="iconfont icon-chat" />
</Icon>
</StyledLink>
</Tooltip>
<Tooltip title={t('agents.title')} mouseEnterDelay={0.5} placement="right">
<Tooltip title={t('agents.title')} mouseEnterDelay={0.8} placement="right">
<StyledLink onClick={() => to('/agents')}>
<Icon className={isRoutes('/agents')}>
<i className="iconfont icon-business-smart-assistant" />
</Icon>
</StyledLink>
</Tooltip>
<Tooltip title={t('paintings.title')} mouseEnterDelay={0.5} placement="right">
<Tooltip title={t('paintings.title')} mouseEnterDelay={0.8} placement="right">
<StyledLink onClick={() => to('/paintings')}>
<Icon className={isRoute('/paintings')}>
<PictureOutlined style={{ fontSize: 16 }} />
</Icon>
</StyledLink>
</Tooltip>
<Tooltip title={t('translate.title')} mouseEnterDelay={0.5} placement="right">
<Tooltip title={t('translate.title')} mouseEnterDelay={0.8} placement="right">
<StyledLink onClick={() => to('/translate')}>
<Icon className={isRoute('/translate')}>
<TranslationOutlined />
</Icon>
</StyledLink>
</Tooltip>
<Tooltip title={t('minapp.title')} mouseEnterDelay={0.5} placement="right">
<StyledLink onClick={() => to('/apps')}>
<Icon className={isRoute('/apps')}>
<i className="iconfont icon-appstore" />
</Icon>
</StyledLink>
</Tooltip>
<Tooltip title={t('files.title')} mouseEnterDelay={0.5} placement="right">
<StyledLink onClick={() => to('/files')}>
<Icon className={isRoute('/files')}>
<FolderOutlined />
</Icon>
</StyledLink>
</Tooltip>
<Tooltip title={t('history.title')} mouseEnterDelay={0.5} placement="right">
<StyledLink onClick={() => to('/messages')}>
<Icon className={isRoutes('/messages')}>
{showMinappIcon && (
<Tooltip title={t('minapp.title')} mouseEnterDelay={0.8} placement="right">
<StyledLink onClick={() => to('/apps')}>
<Icon className={isRoute('/apps')}>
<i className="iconfont icon-appstore" />
</Icon>
</StyledLink>
</Tooltip>
)}
<Tooltip title={t('knowledge_base.title')} mouseEnterDelay={0.5} placement="right">
<StyledLink onClick={() => to('/knowledge')}>
<Icon className={isRoute('/knowledge')}>
<FileSearchOutlined />
</Icon>
</StyledLink>
</Tooltip>
{showFilesIcon && (
<Tooltip title={t('files.title')} mouseEnterDelay={0.8} placement="right">
<StyledLink onClick={() => to('/files')}>
<Icon className={isRoute('/files')}>
<FolderOutlined />
</Icon>
</StyledLink>
</Tooltip>
)}
</Menus>
</MainMenus>
<Menus onClick={MinApp.onClose}>
<Tooltip title={t('settings.theme.title')} mouseEnterDelay={0.5} placement="right">
<Tooltip title={t('settings.theme.title')} mouseEnterDelay={0.8} placement="right">
<Icon onClick={() => toggleTheme()}>
{theme === 'dark' ? (
<i className="iconfont icon-theme icon-dark1" />
@@ -112,7 +116,7 @@ const Sidebar: FC = () => {
)}
</Icon>
</Tooltip>
<Tooltip title={t('settings.title')} mouseEnterDelay={0.5} placement="right">
<Tooltip title={t('settings.title')} mouseEnterDelay={0.8} placement="right">
<StyledLink onClick={() => to(isLocalAi ? '/settings/assistant' : '/settings/provider')}>
<Icon className={pathname.startsWith('/settings') ? 'active' : ''}>
<i className="iconfont icon-setting" />
@@ -134,7 +138,6 @@ const Container = styled.div`
height: ${isMac ? 'calc(100vh - var(--navbar-height))' : '100vh'};
-webkit-app-region: drag !important;
margin-top: ${isMac ? 'var(--navbar-height)' : 0};
transition: background-color 0.3s ease;
`
const AvatarImg = styled(Avatar)`
@@ -165,14 +168,12 @@ const Icon = styled.div`
align-items: center;
border-radius: 50%;
margin-bottom: 5px;
transition: background-color 0.2s ease;
-webkit-app-region: none;
transition: all 0.2s ease;
border: 0.5px solid transparent;
.iconfont,
.anticon {
color: var(--color-icon);
font-size: 20px;
transition: color 0.2s ease;
text-decoration: none;
}
.anticon {
@@ -188,6 +189,7 @@ const Icon = styled.div`
}
&.active {
background-color: var(--color-active);
border: 0.5px solid var(--color-border);
.iconfont,
.anticon {
color: var(--color-icon-white);

View File

@@ -3,97 +3,8 @@ export const DEFAULT_CONTEXTCOUNT = 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
export const isMac = platform === 'darwin'
export const isWindows = platform === 'win32' || platform === 'win64'
export const isLinux = platform === 'linux'
export const imageExts = ['.jpg', '.jpeg', '.png', '.gif', '.bmp', '.webp']
export const documentExts = ['.pdf', '.docx', '.pptx', '.xlsx', '.odt', '.odp', '.ods']
export const textExts = [
'.txt', // 普通文本文件
'.md', // Markdown 文件
'.mdx', // Markdown 文件
'.html', // HTML 文件
'.htm', // HTML 文件的另一种扩展名
'.xml', // XML 文件
'.json', // JSON 文件
'.yaml', // YAML 文件
'.yml', // YAML 文件的另一种扩展名
'.csv', // 逗号分隔值文件
'.tsv', // 制表符分隔值文件
'.ini', // 配置文件
'.log', // 日志文件
'.rtf', // 富文本格式文件
'.tex', // LaTeX 文件
'.srt', // 字幕文件
'.xhtml', // XHTML 文件
'.nfo', // 信息文件(主要用于场景发布)
'.conf', // 配置文件
'.config', // 配置文件
'.env', // 环境变量文件
'.rst', // reStructuredText 文件
'.php', // PHP 脚本文件,包含嵌入的 HTML
'.js', // JavaScript 文件(部分是文本,部分可能包含代码)
'.ts', // TypeScript 文件
'.jsp', // JavaServer Pages 文件
'.aspx', // ASP.NET 文件
'.bat', // Windows 批处理文件
'.sh', // Unix/Linux Shell 脚本文件
'.py', // Python 脚本文件
'.rb', // Ruby 脚本文件
'.pl', // Perl 脚本文件
'.sql', // SQL 脚本文件
'.css', // Cascading Style Sheets 文件
'.less', // Less CSS 预处理器文件
'.scss', // Sass CSS 预处理器文件
'.sass', // Sass 文件
'.styl', // Stylus CSS 预处理器文件
'.coffee', // CoffeeScript 文件
'.ino', // Arduino 代码文件
'.asm', // Assembly 语言文件
'.go', // Go 语言文件
'.scala', // Scala 语言文件
'.swift', // Swift 语言文件
'.kt', // Kotlin 语言文件
'.rs', // Rust 语言文件
'.lua', // Lua 语言文件
'.groovy', // Groovy 语言文件
'.dart', // Dart 语言文件
'.hs', // Haskell 语言文件
'.clj', // Clojure 语言文件
'.cljs', // ClojureScript 语言文件
'.elm', // Elm 语言文件
'.erl', // Erlang 语言文件
'.ex', // Elixir 语言文件
'.exs', // Elixir 脚本文件
'.pug', // Pug (formerly Jade) 模板文件
'.haml', // Haml 模板文件
'.slim', // Slim 模板文件
'.tpl', // 模板文件(通用)
'.ejs', // Embedded JavaScript 模板文件
'.hbs', // Handlebars 模板文件
'.mustache', // Mustache 模板文件
'.jade', // Jade 模板文件 (已重命名为 Pug)
'.twig', // Twig 模板文件
'.blade', // Blade 模板文件 (Laravel)
'.vue', // Vue.js 单文件组件
'.jsx', // React JSX 文件
'.tsx', // React TSX 文件
'.graphql', // GraphQL 查询语言文件
'.gql', // GraphQL 查询语言文件
'.proto', // Protocol Buffers 文件
'.thrift', // Thrift 文件
'.toml', // TOML 配置文件
'.edn', // Clojure 数据表示文件
'.cake', // CakePHP 配置文件
'.ctp', // CakePHP 视图文件
'.cfm', // ColdFusion 标记语言文件
'.cfc', // ColdFusion 组件文件
'.m', // Objective-C 源文件
'.mm', // Objective-C++ 源文件
'.gradle', // Gradle 构建文件
'.groovy', // Gradle 构建文件
'.kts', // Kotlin Script 文件
'.java' // Java 代码文件
]

View File

@@ -1,5 +1,3 @@
import AiAssistantAppLogo from '@renderer/assets/images/apps/360-ai.png'
import AiSearchAppLogo from '@renderer/assets/images/apps/ai-search.png'
import BaiduAiAppLogo from '@renderer/assets/images/apps/baidu-ai.png'
import BaicuanAppLogo from '@renderer/assets/images/apps/baixiaoying.webp'
import BoltAppLogo from '@renderer/assets/images/apps/bolt.svg'
@@ -8,14 +6,18 @@ import DoubaoAppLogo from '@renderer/assets/images/apps/doubao.png'
import DuckDuckGoAppLogo from '@renderer/assets/images/apps/duckduckgo.webp'
import FeloAppLogo from '@renderer/assets/images/apps/felo.png'
import GeminiAppLogo from '@renderer/assets/images/apps/gemini.png'
import GensparkLogo from '@renderer/assets/images/apps/genspark.jpg'
import GithubCopilotLogo from '@renderer/assets/images/apps/github-copilot.webp'
import HuggingChatLogo from '@renderer/assets/images/apps/huggingchat.svg'
import KimiAppLogo from '@renderer/assets/images/apps/kimi.jpg'
import MetasoAppLogo from '@renderer/assets/images/apps/metaso.webp'
import NamiAiSearchLogo from '@renderer/assets/images/apps/nm.webp'
import PerplexityAppLogo from '@renderer/assets/images/apps/perplexity.webp'
import PoeAppLogo from '@renderer/assets/images/apps/poe.webp'
import ZhipuProviderLogo from '@renderer/assets/images/apps/qingyan.png'
import SensetimeAppLogo from '@renderer/assets/images/apps/sensetime.png'
import SparkDeskAppLogo from '@renderer/assets/images/apps/sparkdesk.png'
import ThinkAnyLogo from '@renderer/assets/images/apps/thinkany.webp'
import TiangongAiLogo from '@renderer/assets/images/apps/tiangong.png'
import WanZhiAppLogo from '@renderer/assets/images/apps/wanzhi.jpg'
import TencentYuanbaoAppLogo from '@renderer/assets/images/apps/yuanbao.png'
@@ -119,19 +121,6 @@ const _apps: MinAppType[] = [
url: 'https://claude.ai/',
logo: ClaudeAppLogo
},
{
id: '360-ai-so',
name: '360AI搜索',
logo: AiSearchAppLogo,
url: 'https://so.360.com/'
},
{
id: '360-ai-bot',
name: 'AI 助手',
logo: AiAssistantAppLogo,
url: 'https://bot.360.com/',
bodered: true
},
{
id: 'baidu-ai-chat',
name: '文心一言',
@@ -210,6 +199,12 @@ const _apps: MinAppType[] = [
url: 'https://felo.ai/',
bodered: true
},
{
id: 'duckduckgo',
name: 'DuckDuckGo',
logo: DuckDuckGoAppLogo,
url: 'https://duck.ai'
},
{
id: 'bolt',
name: 'bolt',
@@ -218,10 +213,30 @@ const _apps: MinAppType[] = [
bodered: true
},
{
id: 'duckduckgo',
name: 'DuckDuckGo',
logo: DuckDuckGoAppLogo,
url: 'https://duck.ai'
id: 'nm',
name: '纳米AI搜索',
logo: NamiAiSearchLogo,
url: 'https://www.n.cn/',
bodered: true
},
{
id: 'thinkany',
name: 'ThinkAny',
logo: ThinkAnyLogo,
url: 'https://thinkany.ai/',
bodered: true
},
{
id: 'github-copilot',
name: 'GitHub Copilot',
logo: GithubCopilotLogo,
url: 'https://github.com/copilot'
},
{
id: 'genspark',
name: 'Genspark',
logo: GensparkLogo,
url: 'https://www.genspark.ai/'
}
]

View File

@@ -10,8 +10,8 @@ import AisingaporeModelLogo from '@renderer/assets/images/models/aisingapore.png
import AisingaporeModelLogoDark from '@renderer/assets/images/models/aisingapore_dark.png'
import BaichuanModelLogo from '@renderer/assets/images/models/baichuan.png'
import BaichuanModelLogoDark from '@renderer/assets/images/models/baichuan_dark.png'
import BigcodeModelLogo from '@renderer/assets/images/models/bigcode.png'
import BigcodeModelLogoDark from '@renderer/assets/images/models/bigcode_dark.png'
import BigcodeModelLogo from '@renderer/assets/images/models/bigcode.webp'
import BigcodeModelLogoDark from '@renderer/assets/images/models/bigcode_dark.webp'
import ChatGLMModelLogo from '@renderer/assets/images/models/chatglm.png'
import ChatGLMModelLogoDark from '@renderer/assets/images/models/chatglm_dark.png'
import ChatGptModelLogo from '@renderer/assets/images/models/chatgpt.jpeg'
@@ -121,6 +121,7 @@ import WenxinModelLogo from '@renderer/assets/images/models/wenxin.png'
import WenxinModelLogoDark from '@renderer/assets/images/models/wenxin_dark.png'
import YiModelLogo from '@renderer/assets/images/models/yi.png'
import YiModelLogoDark from '@renderer/assets/images/models/yi_dark.png'
import { getProviderByModel } from '@renderer/services/AssistantService'
import { Model } from '@renderer/types'
import OpenAI from 'openai'
@@ -129,13 +130,15 @@ const visionAllowedModels = [
'moondream',
'minicpm',
'gemini-1\\.5',
'gemini-exp',
'claude-3',
'vision',
'glm-4v',
'qwen-vl',
'qwen2-vl',
'internvl2',
'grok',
'grok-vision-beta',
'pixtral',
'gpt-4(?:-[\\w-]+)',
'gpt-4o(?:-[\\w-]+)?',
'chatgpt-4o(?:-[\\w-]+)?'
@@ -143,14 +146,14 @@ const visionAllowedModels = [
const visionExcludedModels = ['gpt-4-\\d+-preview', 'gpt-4-turbo-preview', 'gpt-4-32k', 'gpt-4-\\d+']
const VISION_REGEX = new RegExp(
export const VISION_REGEX = new RegExp(
`\\b(?!(?:${visionExcludedModels.join('|')})\\b)(${visionAllowedModels.join('|')})\\b`,
'i'
)
const TEXT_TO_IMAGE_REGEX = /flux|diffusion|stabilityai|sd-|dall|cogview/i
const EMBEDDING_REGEX = /(?:^text-|embed|rerank|davinci|babbage|bge-|base|retrieval|uae-)/i
const NOT_SUPPORTED_REGEX = /(?:^text-|embed|tts|rerank|whisper|speech|davinci|babbage|bge-|base|retrieval|uae-)/i
export const TEXT_TO_IMAGE_REGEX = /flux|diffusion|stabilityai|sd-|dall|cogview/i
export const EMBEDDING_REGEX = /(?:^text-|embed|rerank|davinci|babbage|bge-|e5-|LLM2Vec|retrieval|uae-|gte-|jina)/i
export const NOT_SUPPORTED_REGEX = /(?:^tts|rerank|whisper|speech)/i
export function getModelLogo(modelId: string) {
const isLight = true
@@ -163,7 +166,7 @@ export function getModelLogo(modelId: string) {
pixtral: isLight ? PixtralModelLogo : PixtralModelLogoDark,
jina: isLight ? JinaModelLogo : JinaModelLogoDark,
abab: isLight ? MinimaxModelLogo : MinimaxModelLogoDark,
'o1-': isLight ? ChatGPTo1ModelLogo : ChatGPTo1ModelLogoDark,
o1: isLight ? ChatGPTo1ModelLogo : ChatGPTo1ModelLogoDark,
'gpt-3': isLight ? ChatGPT35ModelLogo : ChatGPT35ModelLogoDark,
'gpt-4': isLight ? ChatGPT4ModelLogo : ChatGPT4ModelLogoDark,
gpts: isLight ? ChatGPT4ModelLogo : ChatGPT4ModelLogoDark,
@@ -189,6 +192,7 @@ export function getModelLogo(modelId: string) {
palm: isLight ? PalmModelLogo : PalmModelLogoDark,
step: isLight ? StepModelLogo : StepModelLogoDark,
hailuo: isLight ? HailuoModelLogo : HailuoModelLogoDark,
doubao: isLight ? DoubaoModelLogo : DoubaoModelLogoDark,
'ep-202': isLight ? DoubaoModelLogo : DoubaoModelLogoDark,
cohere: isLight ? CohereModelLogo : CohereModelLogoDark,
command: isLight ? CohereModelLogo : CohereModelLogoDark,
@@ -261,22 +265,10 @@ export const SYSTEM_MODELS: Record<string, Model[]> = {
ollama: [],
silicon: [
{
id: 'Qwen/Qwen2.5-72B-Instruct',
id: 'deepseek-ai/DeepSeek-V2.5',
name: 'deepseek-ai/DeepSeek-V2.5',
provider: 'silicon',
name: 'Qwen2.5-72B-Instruct',
group: 'Qwen2.5'
},
{
id: 'Qwen/Qwen2.5-32B-Instruct',
provider: 'silicon',
name: 'Qwen2.5-32B-Instruct',
group: 'Qwen2.5'
},
{
id: 'Qwen/Qwen2.5-14B-Instruct',
provider: 'silicon',
name: 'Qwen2.5-14B-Instruct',
group: 'Qwen2.5'
group: 'deepseek-ai'
},
{
id: 'Qwen/Qwen2.5-7B-Instruct',
@@ -285,79 +277,17 @@ export const SYSTEM_MODELS: Record<string, Model[]> = {
group: 'Qwen2.5'
},
{
id: 'Qwen/Qwen2-7B-Instruct',
id: 'meta-llama/Llama-3.3-70B-Instruct',
name: 'meta-llama/Llama-3.3-70B-Instruct',
provider: 'silicon',
name: 'Qwen2-7B-Instruct',
group: 'Qwen2'
},
{
id: 'Qwen/Qwen2-72B-Instruct',
provider: 'silicon',
name: 'Qwen2-72B-Instruct',
group: 'Qwen2'
},
{
id: 'THUDM/glm-4-9b-chat',
provider: 'silicon',
name: 'GLM-4-9B-Chat',
group: 'GLM'
},
{
id: 'deepseek-ai/DeepSeek-V2-Chat',
provider: 'silicon',
name: 'DeepSeek-V2-Chat',
group: 'DeepSeek'
},
{
id: 'deepseek-ai/DeepSeek-Coder-V2-Instruct',
provider: 'silicon',
name: 'DeepSeek-Coder-V2-Instruct',
group: 'DeepSeek'
group: 'meta-llama'
}
],
openai: [
{
id: 'gpt-4o',
provider: 'openai',
name: ' GPT-4o',
group: 'GPT 4o'
},
{
id: 'gpt-4o-mini',
provider: 'openai',
name: ' GPT-4o-mini',
group: 'GPT 4o'
},
{
id: 'chatgpt-4o-latest',
provider: 'openai',
name: ' GPT-4o-latest',
group: 'GPT 4o'
},
{
id: 'gpt-4-turbo',
provider: 'openai',
name: ' GPT-4 Turbo',
group: 'GPT 4'
},
{
id: 'gpt-4',
provider: 'openai',
name: ' GPT-4',
group: 'GPT 4'
},
{
id: 'o1-mini',
provider: 'openai',
name: ' o1-mini',
group: 'o1'
},
{
id: 'o1-preview',
provider: 'openai',
name: ' o1-preview',
group: 'o1'
}
{ id: 'gpt-4o', provider: 'openai', name: ' GPT-4o', group: 'GPT 4o' },
{ id: 'gpt-4o-mini', provider: 'openai', name: ' GPT-4o-mini', group: 'GPT 4o' },
{ id: 'o1-mini', provider: 'openai', name: ' o1-mini', group: 'o1' },
{ id: 'o1-preview', provider: 'openai', name: ' o1-preview', group: 'o1' }
],
'azure-openai': [
{
@@ -381,10 +311,10 @@ export const SYSTEM_MODELS: Record<string, Model[]> = {
group: 'Gemini 1.5'
},
{
id: 'gemini-1.5-pro-exp-0801',
id: 'gemini-1.5-pro',
name: 'Gemini 1.5 Pro',
provider: 'gemini',
name: 'Gemini 1.5 Pro Experimental 0801',
group: 'Gemini 1.5'
group: 'gemini-1.5'
}
],
anthropic: [
@@ -584,42 +514,10 @@ export const SYSTEM_MODELS: Record<string, Model[]> = {
}
],
yi: [
{
id: 'yi-large',
provider: 'yi',
name: 'Yi-Large',
group: 'Yi'
},
{
id: 'yi-large-turbo',
provider: 'yi',
name: 'Yi-Large-Turbo',
group: 'Yi'
},
{
id: 'yi-large-rag',
provider: 'yi',
name: 'Yi-Large-Rag',
group: 'Yi'
},
{
id: 'yi-medium',
provider: 'yi',
name: 'Yi-Medium',
group: 'Yi'
},
{
id: 'yi-medium-200k',
provider: 'yi',
name: 'Yi-Medium-200k',
group: 'Yi'
},
{
id: 'yi-spark',
provider: 'yi',
name: 'Yi-Spark',
group: 'Yi'
}
{ id: 'yi-lightning', name: 'yi-lightning', provider: 'yi', group: 'yi-lightning', owned_by: '01.ai' },
{ id: 'yi-medium', name: 'yi-medium', provider: 'yi', group: 'yi-medium', owned_by: '01.ai' },
{ id: 'yi-large', name: 'yi-large', provider: 'yi', group: 'yi-large', owned_by: '01.ai' },
{ id: 'yi-vision', name: 'yi-vision', provider: 'yi', group: 'yi-vision', owned_by: '01.ai' }
],
zhipu: [
{
@@ -673,22 +571,11 @@ export const SYSTEM_MODELS: Record<string, Model[]> = {
],
moonshot: [
{
id: 'moonshot-v1-8k',
id: 'moonshot-v1-auto',
name: 'moonshot-v1-auto',
provider: 'moonshot',
name: 'Moonshot V1 8k',
group: 'Moonshot V1'
},
{
id: 'moonshot-v1-32k',
provider: 'moonshot',
name: 'Moonshot V1 32k',
group: 'Moonshot V1'
},
{
id: 'moonshot-v1-128k',
provider: 'moonshot',
name: 'Moonshot V1 128k',
group: 'Moonshot V1'
group: 'moonshot-v1',
owned_by: 'moonshot'
}
],
baichuan: [
@@ -712,24 +599,11 @@ export const SYSTEM_MODELS: Record<string, Model[]> = {
}
],
bailian: [
{
id: 'qwen-turbo',
provider: 'dashscope',
name: 'Qwen Turbo',
group: 'Qwen'
},
{
id: 'qwen-plus',
provider: 'dashscope',
name: 'Qwen Plus',
group: 'Qwen'
},
{
id: 'qwen-max',
provider: 'dashscope',
name: 'Qwen Max',
group: 'Qwen'
}
{ id: 'qwen-vl-plus', name: 'qwen-vl-plus', provider: 'dashscope', group: 'qwen-vl', owned_by: 'system' },
{ id: 'qwen-coder-plus', name: 'qwen-coder-plus', provider: 'dashscope', group: 'qwen-coder', owned_by: 'system' },
{ id: 'qwen-turbo', name: 'qwen-turbo', provider: 'dashscope', group: 'qwen-turbo', owned_by: 'system' },
{ id: 'qwen-plus', name: 'qwen-plus', provider: 'dashscope', group: 'qwen-plus', owned_by: 'system' },
{ id: 'qwen-max', name: 'qwen-max', provider: 'dashscope', group: 'qwen-max', owned_by: 'system' }
],
stepfun: [
{
@@ -804,6 +678,12 @@ export const SYSTEM_MODELS: Record<string, Model[]> = {
provider: 'grok',
name: 'Grok Beta',
group: 'Grok'
},
{
id: 'grok-vision-beta',
provider: 'grok',
name: 'Grok Vision Beta',
group: 'Grok'
}
],
mistral: [
@@ -820,7 +700,56 @@ export const SYSTEM_MODELS: Record<string, Model[]> = {
group: 'Mistral'
}
],
jina: [],
jina: [
{
id: 'jina-clip-v1',
provider: 'jina',
name: 'jina-clip-v1',
group: 'Jina Clip'
},
{
id: 'jina-clip-v2',
provider: 'jina',
name: 'jina-clip-v2',
group: 'Jina Clip'
},
{
id: 'jina-embeddings-v2-base-en',
provider: 'jina',
name: 'jina-embeddings-v2-base-en',
group: 'Jina Embeddings V2'
},
{
id: 'jina-embeddings-v2-base-es',
provider: 'jina',
name: 'jina-embeddings-v2-base-es',
group: 'Jina Embeddings V2'
},
{
id: 'jina-embeddings-v2-base-de',
provider: 'jina',
name: 'jina-embeddings-v2-base-de',
group: 'Jina Embeddings V2'
},
{
id: 'jina-embeddings-v2-base-zh',
provider: 'jina',
name: 'jina-embeddings-v2-base-zh',
group: 'Jina Embeddings V2'
},
{
id: 'jina-embeddings-v2-base-code',
provider: 'jina',
name: 'jina-embeddings-v2-base-code',
group: 'Jina Embeddings V2'
},
{
id: 'jina-embeddings-v3',
provider: 'jina',
name: 'jina-embeddings-v3',
group: 'Jina Embeddings V3'
}
],
aihubmix: [
{
id: 'gpt-4o-mini',
@@ -1037,13 +966,43 @@ export function isTextToImageModel(model: Model): boolean {
}
export function isEmbeddingModel(model: Model): boolean {
return EMBEDDING_REGEX.test(model.id)
if (!model) {
return false
}
if (['anthropic'].includes(model?.provider)) {
return false
}
return EMBEDDING_REGEX.test(model.id) || model.type?.includes('embedding') || false
}
export function isVisionModel(model: Model): boolean {
return VISION_REGEX.test(model.id)
if (!model) {
return false
}
return VISION_REGEX.test(model.id) || model.type?.includes('vision') || false
}
export function isSupportedModel(model: OpenAI.Models.Model): boolean {
if (!model) {
return false
}
return !NOT_SUPPORTED_REGEX.test(model.id)
}
export function isWebSearchModel(model: Model): boolean {
if (!model) {
return false
}
const provider = getProviderByModel(model)
if (!provider) {
return false
}
return (provider.id === 'gemini' || provider?.type === 'gemini') && model?.id === 'gemini-2.0-flash-exp'
}

View File

@@ -45,4 +45,24 @@ export const AGENT_PROMPT = `
`
export const SUMMARIZE_PROMPT =
'你是一名擅长会话的助理,你需要将用户的会话总结为 10 个字以内的标题,不要使用标点符号和其他特殊符号'
'你是一名擅长会话的助理,你需要将用户的会话总结为 10 个字以内的标题,标题语言与用户的首要语言一致,不要使用标点符号和其他特殊符号'
export const TRANSLATE_PROMPT =
'You are a translation expert. Translate from input language to {{target_language}}, provide the translation result directly without any explanation and keep original format. Do not translate if the target language is the same as the source language.'
export const REFERENCE_PROMPT = `请根据参考资料回答问题,并使用脚注格式引用数据来源。请忽略无关的参考资料。
## 脚注格式:
1. **脚注标记**:在正文中使用 [^数字] 的形式标记脚注,例如 [^1]。
2. **脚注内容**:在文档末尾使用 [^数字]: 脚注内容 的形式定义脚注的具体内容
3. **脚注内容**:应该尽量简洁
## 我的问题是:
{question}
## 参考资料:
{references}
`

View File

@@ -11,8 +11,12 @@ import FireworksProviderLogo from '@renderer/assets/images/providers/fireworks.p
import GithubProviderLogo from '@renderer/assets/images/providers/github.png'
import GoogleProviderLogo from '@renderer/assets/images/providers/google.png'
import GraphRagProviderLogo from '@renderer/assets/images/providers/graph-rag.png'
import GrokProviderLogo from '@renderer/assets/images/providers/grok.png'
import GroqProviderLogo from '@renderer/assets/images/providers/groq.png'
import HyperbolicProviderLogo from '@renderer/assets/images/providers/hyperbolic.png'
import JinaProviderLogo from '@renderer/assets/images/providers/jina.png'
import MinimaxProviderLogo from '@renderer/assets/images/providers/minimax.png'
import MistralProviderLogo from '@renderer/assets/images/providers/mistral.png'
import MoonshotProviderLogo from '@renderer/assets/images/providers/moonshot.png'
import NvidiaProviderLogo from '@renderer/assets/images/providers/nvidia.png'
import OcoolAiProviderLogo from '@renderer/assets/images/providers/ocoolai.png'
@@ -24,10 +28,6 @@ import StepProviderLogo from '@renderer/assets/images/providers/step.png'
import TogetherProviderLogo from '@renderer/assets/images/providers/together.png'
import ZeroOneProviderLogo from '@renderer/assets/images/providers/zero-one.png'
import ZhipuProviderLogo from '@renderer/assets/images/providers/zhipu.png'
import GrokProviderLogo from '@renderer/assets/images/providers/grok.png'
import HyperbolicProviderLogo from '@renderer/assets/images/providers/hyperbolic.png'
import MistralProviderLogo from '@renderer/assets/images/providers/mistral.png'
import JinaProviderLogo from '@renderer/assets/images/providers/jina.png'
export function getProviderLogo(providerId: string) {
switch (providerId) {
@@ -326,6 +326,7 @@ export const PROVIDER_CONFIG = {
},
websites: {
official: 'https://app.hyperbolic.xyz',
apiKey: 'https://app.hyperbolic.xyz/settings',
docs: 'https://docs.hyperbolic.xyz',
models: 'https://app.hyperbolic.xyz/models'
}
@@ -336,6 +337,7 @@ export const PROVIDER_CONFIG = {
},
websites: {
official: 'https://mistral.ai',
apiKey: 'https://console.mistral.ai/api-keys/',
docs: 'https://docs.mistral.ai',
models: 'https://docs.mistral.ai/getting-started/models/models_overview'
}
@@ -346,17 +348,18 @@ export const PROVIDER_CONFIG = {
},
websites: {
official: 'https://jina.ai',
apiKey: 'https://jina.ai/',
docs: 'https://jina.ai',
models: 'https://jina.ai'
}
},
aihubmix: {
api: {
url: 'https://aihubmix.com'
url: 'https://aihubmix.com?aff=SJyh'
},
websites: {
official: 'https://aihubmix.com/',
apiKey: 'https://aihubmix.com/token',
official: 'https://aihubmix.com?aff=SJyh',
apiKey: 'https://aihubmix.com?aff=SJyh',
docs: 'https://doc.aihubmix.com/',
models: 'https://aihubmix.com/models'
}

View File

@@ -1,6 +1,11 @@
import { useSettings } from '@renderer/hooks/useSettings'
import { LanguageVarious } from '@renderer/types'
import { ConfigProvider, theme } from 'antd'
import enUS from 'antd/locale/en_US'
import jaJP from 'antd/locale/ja_JP'
import ruRU from 'antd/locale/ru_RU'
import zhCN from 'antd/locale/zh_CN'
import zhTW from 'antd/locale/zh_TW'
import { FC, PropsWithChildren } from 'react'
import { useTheme } from './ThemeProvider'
@@ -18,15 +23,22 @@ const AntdProvider: FC<PropsWithChildren> = ({ children }) => {
components: {
Segmented: {
trackBg: 'transparent',
itemSelectedBg: isDarkTheme ? 'rgba(255, 255, 255, 0.05)' : 'rgba(0, 0, 0, 0.05)',
itemSelectedBg: isDarkTheme ? 'rgba(255, 255, 255, 0.1)' : 'rgba(0, 0, 0, 0.1)',
boxShadowTertiary: undefined,
borderRadiusLG: 12,
borderRadiusSM: 12,
borderRadiusXS: 12
borderRadiusLG: 16,
borderRadiusSM: 16,
borderRadiusXS: 16
},
Menu: {
activeBarBorderWidth: 0,
darkItemBg: 'transparent'
},
Button: {
boxShadow: 'none',
boxShadowSecondary: 'none',
defaultShadow: 'none',
dangerShadow: 'none',
primaryShadow: 'none'
}
},
token: {
@@ -38,12 +50,19 @@ const AntdProvider: FC<PropsWithChildren> = ({ children }) => {
)
}
function getAntdLocale(language: string) {
function getAntdLocale(language: LanguageVarious) {
switch (language) {
case 'zh-CN':
return zhCN
case 'zh-TW':
return zhTW
case 'en-US':
return undefined
return enUS
case 'ru-RU':
return ruRU
case 'ja-JP':
return jaJP
default:
return zhCN
}

View File

@@ -54,13 +54,15 @@ export const SyntaxHighlighterProvider: React.FC<PropsWithChildren> = ({ childre
const codeToHtml = async (code: string, language: string) => {
if (!highlighter) return ''
const escapedCode = code.replace(/[<>]/g, (char) => ({ '<': '&lt;', '>': '&gt;' })[char]!)
try {
if (!highlighter.getLoadedLanguages().includes(language as BundledLanguage)) {
if (language in bundledLanguages) {
if (language in bundledLanguages || language === 'text') {
await highlighter.loadLanguage(language as BundledLanguage)
console.log(`Loaded language: ${language}`)
} else {
return `<pre><code>${code}</code></pre>`
return `<pre style="padding: 10px"><code>${escapedCode}</code></pre>`
}
}
@@ -70,7 +72,7 @@ export const SyntaxHighlighterProvider: React.FC<PropsWithChildren> = ({ childre
})
} catch (error) {
console.warn(`Error highlighting code for language '${language}':`, error)
return `<pre><code>${code}</code></pre>`
return `<pre style="padding: 10px"><code>${escapedCode}</code></pre>`
}
}

View File

@@ -1,27 +1,29 @@
import { FileType, Topic } from '@renderer/types'
import { FileType, KnowledgeItem, Topic } from '@renderer/types'
import { Dexie, type EntityTable } from 'dexie'
import { populateTopics } from './populate'
// Database declaration (move this to its own module also)
export const db = new Dexie('CherryStudio') as Dexie & {
files: EntityTable<FileType, 'id'>
topics: EntityTable<Pick<Topic, 'id' | 'messages'>, 'id'>
settings: EntityTable<{ id: string; value: any }, 'id'>
knowledge_notes: EntityTable<KnowledgeItem, 'id'>
}
db.version(1).stores({
files: 'id, name, origin_name, path, size, ext, type, created_at, count'
})
db.version(2)
.stores({
files: 'id, name, origin_name, path, size, ext, type, created_at, count',
topics: '&id, messages',
settings: '&id, value'
})
.upgrade(populateTopics)
db.version(2).stores({
files: 'id, name, origin_name, path, size, ext, type, created_at, count',
topics: '&id, messages',
settings: '&id, value'
})
db.on('populate', populateTopics)
db.version(3).stores({
files: 'id, name, origin_name, path, size, ext, type, created_at, count',
topics: '&id, messages',
settings: '&id, value',
knowledge_notes: '&id, baseId, type, content, created_at, updated_at'
})
export default db

View File

@@ -1,27 +0,0 @@
import i18n from '@renderer/i18n'
import { Transaction } from 'dexie'
import localforage from 'localforage'
export async function populateTopics(trans: Transaction) {
const indexedKeys = await localforage.keys()
if (indexedKeys.length > 0) {
for (const key of indexedKeys) {
const value: any = await localforage.getItem(key)
if (key.startsWith('topic:')) {
await trans.db.table('topics').add({ id: value.id, messages: value.messages })
}
if (key === 'image://avatar') {
await trans.db.table('settings').add({ id: key, value: await localforage.getItem(key) })
}
}
window.modal.success({
title: i18n.t('message.upgrade.success.title'),
content: i18n.t('message.upgrade.success.content'),
okText: i18n.t('message.upgrade.success.button'),
centered: true,
onOk: () => window.api.reload()
})
}
}

View File

@@ -19,5 +19,6 @@ declare global {
modal: HookAPI
keyv: KeyvStorage
mermaid: any
store: any
}
}

View File

@@ -2,23 +2,36 @@ import { isMac } from '@renderer/config/constant'
import { isLocalAi } from '@renderer/config/env'
import db from '@renderer/databases'
import i18n from '@renderer/i18n'
import { startAutoSync, stopAutoSync } from '@renderer/services/BackupService'
import { useAppDispatch } from '@renderer/store'
import { setAvatar, setFilesPath } from '@renderer/store/runtime'
import { runAsyncFunction } from '@renderer/utils'
import { setAvatar, setFilesPath, setUpdateState } from '@renderer/store/runtime'
import { delay, runAsyncFunction } from '@renderer/utils'
import { useLiveQuery } from 'dexie-react-hooks'
import { useEffect } from 'react'
import { useDefaultModel } from './useAssistant'
import { useRuntime } from './useRuntime'
import { useSettings } from './useSettings'
import useUpdateHandler from './useUpdateHandler'
export function useAppInit() {
const dispatch = useAppDispatch()
const { proxyUrl, language, windowStyle, manualUpdateCheck } = useSettings()
const {
proxyUrl,
language,
windowStyle,
manualUpdateCheck,
proxyMode,
webdavAutoSync,
webdavSyncInterval,
customCss
} = useSettings()
const { minappShow } = useRuntime()
const { setDefaultModel, setTopicNamingModel, setTranslateModel } = useDefaultModel()
const avatar = useLiveQuery(() => db.settings.get('image://avatar'))
useUpdateHandler()
useEffect(() => {
avatar?.value && dispatch(setAvatar(avatar.value))
}, [avatar, dispatch])
@@ -28,15 +41,22 @@ export function useAppInit() {
runAsyncFunction(async () => {
const { isPackaged } = await window.api.getAppInfo()
if (isPackaged && !manualUpdateCheck) {
setTimeout(window.api.checkForUpdate, 3000)
await delay(2)
const { updateInfo } = await window.api.checkForUpdate()
dispatch(setUpdateState({ info: updateInfo }))
}
})
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [])
}, [dispatch, manualUpdateCheck])
useEffect(() => {
proxyUrl && window.api.setProxy(proxyUrl)
}, [proxyUrl])
if (proxyMode === 'system') {
window.api.setProxy('system')
} else if (proxyMode === 'custom') {
proxyUrl && window.api.setProxy(proxyUrl)
} else {
window.api.setProxy('')
}
}, [proxyUrl, proxyMode])
useEffect(() => {
i18n.changeLanguage(language || navigator.language || 'en-US')
@@ -63,4 +83,26 @@ export function useAppInit() {
dispatch(setFilesPath(info.filesPath))
})
}, [dispatch])
useEffect(() => {
webdavAutoSync ? startAutoSync() : stopAutoSync()
}, [webdavAutoSync, webdavSyncInterval])
useEffect(() => {
import('@renderer/queue/KnowledgeQueue')
}, [])
useEffect(() => {
const oldCustomCss = document.getElementById('user-defined-custom-css')
if (oldCustomCss) {
oldCustomCss.remove()
}
if (customCss) {
const style = document.createElement('style')
style.id = 'user-defined-custom-css'
style.textContent = customCss
document.head.appendChild(style)
}
}, [customCss])
}

View File

@@ -1,3 +1,4 @@
import { db } from '@renderer/databases'
import { getDefaultTopic } from '@renderer/services/AssistantService'
import { useAppDispatch, useAppSelector } from '@renderer/store'
import {
@@ -50,8 +51,20 @@ export function useAssistant(id: string) {
dispatch(removeTopic({ assistantId: assistant.id, topic }))
},
moveTopic: (topic: Topic, toAssistant: Assistant) => {
dispatch(addTopic({ assistantId: toAssistant.id, topic: { ...topic } }))
dispatch(addTopic({ assistantId: toAssistant.id, topic: { ...topic, assistantId: toAssistant.id } }))
dispatch(removeTopic({ assistantId: assistant.id, topic }))
// update topic messages in database
db.topics
.where('id')
.equals(topic.id)
.modify((dbTopic) => {
if (dbTopic.messages) {
dbTopic.messages = dbTopic.messages.map((message) => ({
...message,
assistantId: toAssistant.id
}))
}
})
},
updateTopic: (topic: Topic) => dispatch(updateTopic({ assistantId: assistant.id, topic })),
updateTopics: (topics: Topic[]) => dispatch(updateTopics({ assistantId: assistant.id, topics })),

View File

@@ -0,0 +1,279 @@
/* eslint-disable react-hooks/rules-of-hooks */
import { db } from '@renderer/databases/index'
import KnowledgeQueue from '@renderer/queue/KnowledgeQueue'
import FileManager from '@renderer/services/FileManager'
import { getKnowledgeBaseParams } from '@renderer/services/KnowledgeService'
import { RootState } from '@renderer/store'
import {
addBase,
addFiles as addFilesAction,
addItem,
clearAllProcessing,
clearCompletedProcessing,
deleteBase,
removeItem as removeItemAction,
renameBase,
updateBase,
updateBases,
updateItemProcessingStatus,
updateNotes
} from '@renderer/store/knowledge'
import { FileType, KnowledgeBase, ProcessingStatus } from '@renderer/types'
import { KnowledgeItem } from '@renderer/types'
import { runAsyncFunction } from '@renderer/utils'
import { useEffect, useState } from 'react'
import { useDispatch, useSelector } from 'react-redux'
import { v4 as uuidv4 } from 'uuid'
export const useKnowledge = (baseId: string) => {
const dispatch = useDispatch()
const base = useSelector((state: RootState) => state.knowledge.bases.find((b) => b.id === baseId))
// 重命名知识库
const renameKnowledgeBase = (name: string) => {
dispatch(renameBase({ baseId, name }))
}
// 更新知识库
const updateKnowledgeBase = (base: KnowledgeBase) => {
dispatch(updateBase(base))
}
// 批量添加文件
const addFiles = (files: FileType[]) => {
const filesItems: KnowledgeItem[] = files.map((file) => ({
id: uuidv4(),
type: 'file' as const,
content: file,
created_at: Date.now(),
updated_at: Date.now(),
processingStatus: 'pending',
processingProgress: 0,
processingError: '',
retryCount: 0
}))
dispatch(addFilesAction({ baseId, items: filesItems }))
setTimeout(() => KnowledgeQueue.checkAllBases(), 0)
}
// 添加URL
const addUrl = (url: string) => {
const newUrlItem: KnowledgeItem = {
id: uuidv4(),
type: 'url' as const,
content: url,
created_at: Date.now(),
updated_at: Date.now(),
processingStatus: 'pending',
processingProgress: 0,
processingError: '',
retryCount: 0
}
dispatch(addItem({ baseId, item: newUrlItem }))
setTimeout(() => KnowledgeQueue.checkAllBases(), 0)
}
// 添加笔记
const addNote = async (content: string) => {
const noteId = uuidv4()
const note: KnowledgeItem = {
id: noteId,
type: 'note',
content,
created_at: Date.now(),
updated_at: Date.now()
}
// 存储完整笔记到数据库
await db.knowledge_notes.add(note)
// 在 store 中只存储引用
const noteRef: KnowledgeItem = {
id: noteId,
baseId,
type: 'note',
content: '', // store中不需要存储实际内容
created_at: Date.now(),
updated_at: Date.now(),
processingStatus: 'pending',
processingProgress: 0,
processingError: '',
retryCount: 0
}
dispatch(updateNotes({ baseId, item: noteRef }))
setTimeout(() => KnowledgeQueue.checkAllBases(), 0)
}
// 更新笔记内容
const updateNoteContent = async (noteId: string, content: string) => {
const note = await db.knowledge_notes.get(noteId)
if (note) {
const updatedNote = {
...note,
content,
updated_at: Date.now()
}
await db.knowledge_notes.put(updatedNote)
dispatch(updateNotes({ baseId, item: updatedNote }))
}
setTimeout(() => KnowledgeQueue.checkAllBases(), 0)
}
// 获取笔记内容
const getNoteContent = async (noteId: string) => {
return await db.knowledge_notes.get(noteId)
}
// 移除项目
const removeItem = async (item: KnowledgeItem) => {
dispatch(removeItemAction({ baseId, item }))
if (base) {
if (item?.uniqueId) {
await window.api.knowledgeBase.remove({ uniqueId: item.uniqueId, base: getKnowledgeBaseParams(base) })
}
if (item.type === 'file' && typeof item.content === 'object') {
await FileManager.deleteFile(item.content.id)
}
}
}
// 更新处理状态
const updateItemStatus = (itemId: string, status: ProcessingStatus, progress?: number, error?: string) => {
dispatch(
updateItemProcessingStatus({
baseId,
itemId,
status,
progress,
error
})
)
}
// 获取特定项目的处理状态
const getProcessingStatus = (itemId: string) => {
return base?.items.find((item) => item.id === itemId)?.processingStatus
}
// 获取特定类型的所有处理项
const getProcessingItemsByType = (type: 'file' | 'url' | 'note') => {
return base?.items.filter((item) => item.type === type && item.processingStatus !== undefined) || []
}
// 清除已完成的项目
const clearCompleted = () => {
dispatch(clearCompletedProcessing({ baseId }))
}
// 清除所有处理状态
const clearAll = () => {
dispatch(clearAllProcessing({ baseId }))
}
// 添加 Sitemap
const addSitemap = (url: string) => {
const newSitemapItem: KnowledgeItem = {
id: uuidv4(),
type: 'sitemap' as const,
content: url,
created_at: Date.now(),
updated_at: Date.now(),
processingStatus: 'pending',
processingProgress: 0,
processingError: '',
retryCount: 0
}
dispatch(addItem({ baseId, item: newSitemapItem }))
setTimeout(() => KnowledgeQueue.checkAllBases(), 0)
}
// Add directory support
const addDirectory = (path: string) => {
const newDirectoryItem: KnowledgeItem = {
id: uuidv4(),
type: 'directory',
content: path,
created_at: Date.now(),
updated_at: Date.now(),
processingStatus: 'pending',
processingProgress: 0,
processingError: '',
retryCount: 0
}
dispatch(addItem({ baseId, item: newDirectoryItem }))
setTimeout(() => KnowledgeQueue.checkAllBases(), 0)
}
const fileItems = base?.items.filter((item) => item.type === 'file') || []
const directoryItems = base?.items.filter((item) => item.type === 'directory') || []
const urlItems = base?.items.filter((item) => item.type === 'url') || []
const sitemapItems = base?.items.filter((item) => item.type === 'sitemap') || []
const [noteItems, setNoteItems] = useState<KnowledgeItem[]>([])
useEffect(() => {
const notes = base?.items.filter((item) => item.type === 'note') || []
runAsyncFunction(async () => {
const newNoteItems = await Promise.all(
notes.map(async (item) => {
const note = await db.knowledge_notes.get(item.id)
return { ...item, content: note?.content || '' }
})
)
setNoteItems(newNoteItems.filter((note) => note !== undefined) as KnowledgeItem[])
})
}, [base?.items])
return {
base,
fileItems,
urlItems,
sitemapItems,
noteItems,
renameKnowledgeBase,
updateKnowledgeBase,
addFiles,
addUrl,
addSitemap,
addNote,
updateNoteContent,
getNoteContent,
updateItemStatus,
getProcessingStatus,
getProcessingItemsByType,
clearCompleted,
clearAll,
removeItem,
directoryItems,
addDirectory
}
}
export const useKnowledgeBases = () => {
const dispatch = useDispatch()
const bases = useSelector((state: RootState) => state.knowledge.bases)
const addKnowledgeBase = (base: KnowledgeBase) => {
dispatch(addBase(base))
}
const renameKnowledgeBase = (baseId: string, name: string) => {
dispatch(renameBase({ baseId, name }))
}
const deleteKnowledgeBase = (baseId: string) => {
dispatch(deleteBase({ baseId }))
}
const updateKnowledgeBases = (bases: KnowledgeBase[]) => {
dispatch(updateBases(bases))
}
return {
bases,
addKnowledgeBase,
renameKnowledgeBase,
deleteKnowledgeBase,
updateKnowledgeBases
}
}

View File

@@ -8,6 +8,7 @@ import { uuid } from '@renderer/utils'
export function usePaintings() {
const paintings = useAppSelector((state) => state.paintings.paintings)
const dispatch = useAppDispatch()
const generateRandomSeed = () => Math.floor(Math.random() * 1000000).toString()
return {
paintings,
@@ -20,7 +21,7 @@ export function usePaintings() {
negativePrompt: '',
imageSize: '1024x1024',
numImages: 1,
seed: '',
seed: generateRandomSeed(),
steps: 25,
guidanceScale: 4.5,
model: TEXT_TO_IMAGES_MODELS[0].id

View File

@@ -1,8 +1,9 @@
import { useAppDispatch, useAppSelector } from '@renderer/store'
import store, { useAppDispatch, useAppSelector } from '@renderer/store'
import {
SendMessageShortcut,
setSendMessageShortcut as _setSendMessageShortcut,
setTheme,
SettingsState,
setTopicPosition,
setTray,
setWindowStyle
@@ -41,3 +42,7 @@ export function useMessageStyle() {
isBubbleStyle
}
}
export const getStoreSetting = (key: keyof SettingsState) => {
return store.getState().settings[key]
}

View File

@@ -0,0 +1,90 @@
import { isMac, isWindows } from '@renderer/config/constant'
import { useAppSelector } from '@renderer/store'
import { useCallback } from 'react'
import { useHotkeys } from 'react-hotkeys-hook'
interface UseShortcutOptions {
preventDefault?: boolean
enableOnFormTags?: boolean
enabled?: boolean
description?: string
}
const defaultOptions: UseShortcutOptions = {
preventDefault: true,
enableOnFormTags: true,
enabled: true
}
export const useShortcut = (
shortcutKey: string,
callback: (e: KeyboardEvent) => void,
options: UseShortcutOptions = defaultOptions
) => {
const shortcuts = useAppSelector((state) => state.shortcuts.shortcuts)
const formatShortcut = useCallback((shortcut: string[]) => {
return shortcut
.map((key) => {
switch (key.toLowerCase()) {
case 'command':
return 'meta'
default:
return key.toLowerCase()
}
})
.join('+')
}, [])
const shortcutConfig = shortcuts.find((s) => s.key === shortcutKey)
useHotkeys(
shortcutConfig?.enabled ? formatShortcut(shortcutConfig.shortcut) : 'none',
(e) => {
if (options.preventDefault) {
e.preventDefault()
}
if (options.enabled !== false) {
callback(e)
}
},
{
enableOnFormTags: options.enableOnFormTags,
description: options.description || shortcutConfig?.key,
enabled: !!shortcutConfig?.enabled
}
)
}
export function useShortcuts() {
const shortcuts = useAppSelector((state) => state.shortcuts.shortcuts)
return { shortcuts }
}
export function useShortcutDisplay(key: string) {
const formatShortcut = useCallback((shortcut: string[]) => {
return shortcut
.map((key) => {
switch (key.toLowerCase()) {
case 'control':
return isMac ? '⌃' : 'Ctrl'
case 'ctrl':
return isMac ? '⌃' : 'Ctrl'
case 'command':
return isMac ? '⌘' : isWindows ? 'Win' : 'Super'
case 'alt':
return isMac ? '⌥' : 'Alt'
case 'shift':
return isMac ? '⇧' : 'Shift'
case 'commandorcontrol':
return isMac ? '⌘' : 'Ctrl'
default:
return key.charAt(0).toUpperCase() + key.slice(1).toLowerCase()
}
})
.join('+')
}, [])
const shortcuts = useAppSelector((state) => state.shortcuts.shortcuts)
const shortcutConfig = shortcuts.find((s) => s.key === key)
return shortcutConfig?.enabled ? formatShortcut(shortcutConfig.shortcut) : ''
}

View File

@@ -1,5 +1,5 @@
import { useAppDispatch, useAppSelector } from '@renderer/store'
import { setShowTopics, toggleShowAssistants, toggleShowTopics } from '@renderer/store/settings'
import { setShowAssistants, setShowTopics, toggleShowAssistants, toggleShowTopics } from '@renderer/store/settings'
export function useShowAssistants() {
const showAssistants = useAppSelector((state) => state.settings.showAssistants)
@@ -7,6 +7,7 @@ export function useShowAssistants() {
return {
showAssistants,
setShowAssistants: (show: boolean) => dispatch(setShowAssistants(show)),
toggleShowAssistants: () => dispatch(toggleShowAssistants())
}
}

View File

@@ -0,0 +1,68 @@
import { useAppDispatch } from '@renderer/store'
import { setUpdateState } from '@renderer/store/runtime'
import type { ProgressInfo, UpdateInfo } from 'electron-updater'
import { useEffect } from 'react'
import { useTranslation } from 'react-i18next'
export default function useUpdateHandler() {
const dispatch = useAppDispatch()
const { t } = useTranslation()
useEffect(() => {
const ipcRenderer = window.electron.ipcRenderer
const removers = [
ipcRenderer.on('update-not-available', () => {
dispatch(setUpdateState({ checking: false }))
if (window.location.hash.includes('settings/about')) {
window.message.success(t('settings.about.updateNotAvailable'))
}
}),
ipcRenderer.on('update-available', (_, releaseInfo: UpdateInfo) => {
dispatch(
setUpdateState({
checking: false,
downloading: true,
info: releaseInfo,
available: true
})
)
}),
ipcRenderer.on('download-update', () => {
dispatch(
setUpdateState({
checking: false,
downloading: true
})
)
}),
ipcRenderer.on('download-progress', (_, progress: ProgressInfo) => {
dispatch(
setUpdateState({
downloading: progress.percent < 100,
downloadProgress: progress.percent
})
)
}),
ipcRenderer.on('update-downloaded', () => {
dispatch(setUpdateState({ downloading: false }))
}),
ipcRenderer.on('update-error', (_, error) => {
dispatch(
setUpdateState({
checking: false,
downloading: false,
downloadProgress: 0
})
)
if (window.location.hash.includes('settings/about')) {
window.modal.info({
title: t('settings.about.updateError'),
content: error?.message || t('settings.about.updateError'),
icon: null
})
}
})
]
return () => removers.forEach((remover) => remover())
}, [dispatch, t])
}

View File

@@ -2,13 +2,17 @@ import i18n from 'i18next'
import { initReactI18next } from 'react-i18next'
import enUS from './locales/en-us.json'
import jaJP from './locales/ja-jp.json'
import ruRU from './locales/ru-ru.json'
import zhCN from './locales/zh-cn.json'
import zhTW from './locales/zh-tw.json'
const resources = {
'en-US': enUS,
'zh-CN': zhCN,
'zh-TW': zhTW
'zh-TW': zhTW,
'ja-JP': jaJP,
'ru-RU': ruRU
}
i18n.use(initReactI18next).init({

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,587 @@
{
"translation": {
"agents": {
"add.button": "アシスタントに追加",
"add.name": "名前",
"add.name.placeholder": "名前を入力",
"add.prompt": "プロンプト",
"add.prompt.placeholder": "プロンプトを入力",
"add.title": "エージェントを作成",
"delete.popup.content": "このエージェントを削除してもよろしいですか?",
"edit.message.add.title": "追加",
"edit.message.assistant.placeholder": "アシスタントのメッセージを入力",
"edit.message.assistant.title": "アシスタント",
"edit.message.empty.content": "会話の入力内容が空です",
"edit.message.group.title": "メッセージグループ",
"edit.message.title": "プリセットメッセージ",
"edit.message.user.placeholder": "ユーザーメッセージを入力",
"edit.message.user.title": "ユーザー",
"edit.model.select.title": "モデルを選択",
"edit.settings.hide_preset_messages": "プリセットメッセージを非表示",
"edit.title": "エージェントを編集",
"manage.title": "エージェントを管理",
"my_agents": "マイエージェント",
"search.no_results": "結果が見つかりません",
"sorting.title": "並び替え",
"tag.agent": "エージェント",
"tag.default": "デフォルト",
"tag.new": "新規",
"tag.system": "システム",
"title": "エージェント"
},
"assistants": {
"abbr": "アシスタント",
"clear.content": "トピックをクリアすると、アシスタント内のすべてのトピックとファイルが削除されます。続行しますか?",
"clear.title": "トピックをクリア",
"copy.title": "アシスタントをコピー",
"delete.content": "アシスタントを削除すると、そのアシスタントのすべてのトピックとファイルが削除されます。削除しますか?",
"delete.title": "アシスタントを削除",
"edit.title": "アシスタントを編集",
"save.success": "保存に成功しました",
"save.title": "エージェントに保存",
"search": "アシスタントを検索...",
"settings.auto_reset_model": "自動リセットモデル",
"settings.auto_reset_model.tip": "新しいトピックを作成する際にモデルを自動的にリセットします",
"settings.default_model": "デフォルトモデル",
"settings.model": "モデル設定",
"settings.preset_messages": "プリセットメッセージ",
"settings.prompt": "プロンプト設定",
"title": "アシスタント"
},
"button": {
"add": "追加",
"added": "追加済み",
"collapse": "折りたたむ",
"manage": "管理",
"select_model": "モデルを選択",
"show.all": "すべて表示"
},
"chat": {
"add.assistant.title": "アシスタントを追加",
"artifacts.button.download": "ダウンロード",
"artifacts.button.preview": "プレビュー",
"assistant.search.placeholder": "検索",
"default.description": "こんにちは、私はデフォルトのアシスタントです。すぐにチャットを始められます。",
"default.name": "⭐️ デフォルトアシスタント",
"default.topic.name": "デフォルトトピック",
"input.clear": "クリア",
"input.clear.content": "現在のトピックのすべてのメッセージをクリアしますか?",
"input.clear.title": "すべてのメッセージをクリアしますか?",
"input.collapse": "折りたたむ",
"input.context_count.tip": "コンテキスト数",
"input.estimated_tokens.tip": "推定トークン数",
"input.expand": "展開",
"input.new.context": "コンテキストをクリア",
"input.new_topic": "新しいトピック {{Command}}",
"input.pause": "一時停止",
"input.placeholder": "ここにメッセージを入力...",
"input.send": "送信",
"input.settings": "設定",
"input.topics": " トピック ",
"input.translate": "英語に翻訳",
"input.upload": "画像またはドキュメントをアップロード",
"input.web_search": "ウェブ検索を有効にする",
"input.knowledge_base": "ナレッジベース",
"message.new.branch": "新しいブランチ",
"message.new.branch.created": "新しいブランチが作成されました",
"message.regenerate.model": "モデルを切り替え",
"message.new.context": "新しいコンテキスト",
"save": "保存",
"settings.code_collapsible": "コードブロックを折りたたむ",
"settings.context_count": "コンテキスト",
"settings.context_count.tip": "コンテキストに保持する以前のメッセージの数",
"settings.max": "最大",
"settings.max_tokens": "最大トークン制限を有効にする",
"settings.max_tokens.tip": "モデルが生成できる最大トークン数。通常のチャットでは500-800、短いテキスト生成では800-2000、コード生成では2000-3600、長いテキスト生成では4000以上を推奨",
"settings.reset": "リセット",
"settings.set_as_default": "デフォルトのアシスタントに適用",
"settings.show_line_numbers": "コードに行番号を表示",
"settings.temperature": "温度",
"settings.temperature.tip": "低い値はモデルをより創造的で予測不可能にし、高い値はより決定論的で正確にします",
"settings.top_p": "Top-P",
"settings.top_p.tip": "デフォルト値は1で、値が小さいほど回答の多様性が減り、理解しやすくなります。値が大きいほど、AIの語彙範囲が広がり、多様性が増します",
"suggestions.title": "提案された質問",
"topics.auto_rename": "自動リネーム",
"topics.clear.title": "メッセージをクリア",
"topics.edit.placeholder": "新しい名前を入力",
"topics.edit.title": "名前を編集",
"topics.export.image": "画像としてエクスポート",
"topics.export.md": "Markdownとしてエクスポート",
"topics.export.title": "エクスポート",
"topics.export.word": "Wordとしてエクスポート",
"topics.list": "トピックリスト",
"topics.move_to": "移動先",
"topics.title": "トピック",
"translate": "翻訳",
"resend": "再送信"
},
"common": {
"and": "と",
"assistant": "アシスタント",
"avatar": "アバター",
"back": "戻る",
"cancel": "キャンセル",
"chat": "チャット",
"close": "閉じる",
"copy": "コピー",
"cut": "切り取り",
"default": "デフォルト",
"delete": "削除",
"description": "説明",
"docs": "ドキュメント",
"download": "ダウンロード",
"duplicate": "複製",
"edit": "編集",
"footnotes": "脚注",
"language": "言語",
"model": "モデル",
"models": "モデル",
"name": "名前",
"paste": "貼り付け",
"prompt": "プロンプト",
"provider": "プロバイダー",
"regenerate": "再生成",
"rename": "名前を変更",
"reset": "リセット",
"save": "保存",
"search": "検索",
"select": "選択",
"topics": "トピック",
"warning": "警告",
"you": "あなた",
"clear": "クリア",
"add": "追加"
},
"error": {
"backup.file_format": "バックアップファイルの形式エラー",
"chat.response": "エラーが発生しました。APIキーが設定されていない場合は、設定 > プロバイダーでキーを設定してください",
"no_api_key": "APIキーが設定されていません",
"provider_disabled": "モデルプロバイダーが有効になっていません",
"render": {
"title": "レンダリングエラー",
"description": "数式のレンダリングに失敗しました。数式の形式が正しいか確認してください"
}
},
"export": {
"assistant": "アシスタント",
"attached_files": "添付ファイル",
"conversation_details": "会話の詳細",
"conversation_history": "会話履歴",
"created": "作成日",
"last_updated": "最終更新日",
"messages": "メッセージ",
"user": "ユーザー"
},
"files": {
"actions": "操作",
"all": "すべてのファイル",
"count": "数",
"created_at": "作成日",
"document": "ドキュメント",
"file": "ファイル",
"image": "画像",
"name": "名前",
"open": "開く",
"size": "サイズ",
"text": "テキスト",
"title": "ファイル"
},
"history": {
"continue_chat": "チャットを続ける",
"locate.message": "メッセージを探す",
"search.messages": "すべてのメッセージを検索",
"search.placeholder": "トピックまたはメッセージを検索...",
"search.topics.empty": "トピックが見つかりませんでした。Enterキーを押してすべてのメッセージを検索",
"title": "トピック検索"
},
"languages": {
"arabic": "アラビア語",
"chinese": "中国語",
"chinese-traditional": "繁体字中国語",
"english": "英語",
"french": "フランス語",
"italian": "イタリア語",
"japanese": "日本語",
"korean": "韓国語",
"portuguese": "ポルトガル語",
"russian": "ロシア語",
"spanish": "スペイン語"
},
"mermaid": {
"download": {
"png": "PNGをダウンロード",
"svg": "SVGをダウンロード"
},
"tabs": {
"preview": "プレビュー",
"source": "ソース"
},
"title": "Mermaid図"
},
"message": {
"api.connection.failed": "接続に失敗しました",
"api.connection.success": "接続に成功しました",
"assistant.added.content": "アシスタントが追加されました",
"backup.failed": "バックアップに失敗しました",
"backup.success": "バックアップに成功しました",
"backup.start.success": "バックアップを開始しました",
"chat.completion.paused": "チャットの完了が一時停止されました",
"copied": "コピーしました!",
"error.enter.api.host": "APIホストを入力してください",
"error.enter.api.key": "APIキーを入力してください",
"error.enter.model": "モデルを選択してください",
"error.invalid.proxy.url": "無効なプロキシURL",
"error.invalid.webdav": "無効なWebDAV設定",
"message.code_style": "コードスタイル",
"message.delete.content": "このメッセージを削除してもよろしいですか?",
"message.delete.title": "メッセージを削除",
"message.style": "メッセージスタイル",
"message.style.bubble": "バブル",
"message.style.plain": "プレーン",
"reset.confirm.content": "すべてのデータをリセットしてもよろしいですか?",
"reset.double.confirm.content": "すべてのデータが失われます。続行しますか?",
"reset.double.confirm.title": "データが失われます!!!",
"restore.success": "復元に成功しました",
"save.success.title": "保存に成功しました",
"switch.disabled": "アシスタントが生成中は切り替えが無効です",
"topic.added": "新しいトピックが追加されました",
"upgrade.success.button": "再起動",
"upgrade.success.content": "アップグレードを完了するためにアプリケーションを再起動してください",
"upgrade.success.title": "アップグレードに成功しました",
"regenerate.confirm": "再生成すると現在のメッセージが置き換えられます",
"copy.success": "コピーしました!"
},
"minapp": {
"title": "ミニアプリ"
},
"ollama": {
"keep_alive_time.description": "モデルがメモリに保持される時間デフォルト5分",
"keep_alive_time.placeholder": "分",
"keep_alive_time.title": "保持時間",
"title": "Ollama"
},
"paintings": {
"button.delete.image": "画像を削除",
"button.delete.image.confirm": "この画像を削除してもよろしいですか?",
"button.new.image": "新しい画像",
"guidance_scale": "ガイダンススケール",
"guidance_scale_tip": "分類器なしのガイダンス。モデルが関連する画像を探す際にプロンプトにどれだけ従うかを制御します",
"image.size": "画像サイズ",
"inference_steps": "推論ステップ数",
"inference_steps_tip": "実行する推論ステップ数。ステップ数が多いほど品質が向上しますが、時間がかかります",
"negative_prompt": "ネガティブプロンプト",
"negative_prompt_tip": "画像に含めたくない内容を説明します",
"number_images": "生成数",
"number_images_tip": "生成する画像の数1-4",
"prompt_placeholder": "作成したい画像を説明します。例:夕日の湖畔、遠くに山々",
"regenerate.confirm": "これにより、既存の生成画像が置き換えられます。続行しますか?",
"seed": "シード",
"seed_tip": "同じシードとプロンプトで似た画像を生成できます",
"title": "画像"
},
"provider": {
"aihubmix": "AiHubMix",
"anthropic": "Anthropic",
"azure-openai": "Azure OpenAI",
"baichuan": "百川",
"dashscope": "Alibaba Cloud",
"deepseek": "DeepSeek",
"doubao": "豆包",
"fireworks": "Fireworks",
"gemini": "Gemini",
"github": "GitHub Models",
"graphrag-kylin-mountain": "GraphRAG",
"grok": "Grok",
"groq": "Groq",
"hunyuan": "腾讯混元",
"hyperbolic": "Hyperbolic",
"jina": "Jina",
"minimax": "MiniMax",
"mistral": "Mistral",
"moonshot": "月の暗面",
"nvidia": "NVIDIA",
"ocoolai": "ocoolAI",
"ollama": "Ollama",
"openai": "OpenAI",
"openrouter": "OpenRouter",
"silicon": "SiliconFlow",
"stepfun": "StepFun",
"together": "Together",
"yi": "零一万物",
"zhinao": "360智脳",
"zhipu": "智譜AI"
},
"settings": {
"about": "について",
"about.checkUpdate": "更新を確認",
"about.checkUpdate.available": "今すぐ更新",
"about.checkingUpdate": "更新を確認中...",
"about.contact.button": "メール",
"about.contact.title": "連絡先",
"about.description": "クリエイターのための強力なAIアシスタント",
"about.downloading": "ダウンロード中...",
"about.feedback.button": "フィードバック",
"about.feedback.title": "フィードバック",
"about.license.button": "ライセンス",
"about.license.title": "ライセンス",
"about.releases.button": "リリース",
"about.releases.title": "リリースノート",
"about.title": "について",
"about.updateAvailable": "新しいバージョン {{version}} が見つかりました",
"about.updateError": "更新エラー",
"about.updateNotAvailable": "最新バージョンを使用しています",
"about.website.button": "ウェブサイト",
"about.website.title": "公式ウェブサイト",
"about.social.title": "ソーシャルアカウント",
"advanced.auto_switch_to_topics": "トピックに自動的に切り替える",
"advanced.title": "詳細設定",
"assistant": "デフォルトアシスタント",
"assistant.model_params": "モデルパラメータ",
"assistant.title": "デフォルトアシスタント",
"data": {
"app_data": "アプリデータ",
"app_logs": "アプリログ",
"clear_cache": {
"button": "キャッシュをクリア",
"confirm": "キャッシュをクリアすると、アプリのキャッシュデータ(ミニアプリデータを含む)が削除されます。この操作は元に戻せません。続行しますか?",
"error": "キャッシュのクリアに失敗しました",
"success": "キャッシュがクリアされました",
"title": "キャッシュをクリア"
},
"data.title": "データディレクトリ",
"title": "データ設定",
"webdav.backup.button": "WebDAVにバックアップ",
"webdav.host": "WebDAVホスト",
"webdav.host.placeholder": "http://localhost:8080",
"webdav.password": "WebDAVパスワード",
"webdav.path": "WebDAVパス",
"webdav.path.placeholder": "/backup",
"webdav.autoSync": "自動同期",
"webdav.minutes": "分",
"webdav.restore.button": "WebDAVから復元",
"webdav.title": "WebDAV",
"webdav.user": "WebDAVユーザー"
},
"display.title": "表示設定",
"font_size.title": "メッセージのフォントサイズ",
"general": "一般設定",
"general.backup.button": "バックアップ",
"general.backup.title": "データのバックアップと復元",
"general.manually_check_update.title": "更新チェックを無効にする",
"general.reset.button": "リセット",
"general.reset.title": "データをリセット",
"general.restore.button": "復元",
"general.title": "一般設定",
"general.user_name": "ユーザー名",
"general.user_name.placeholder": "ユーザー名を入力",
"general.view_webdav_settings": "WebDAV設定を表示",
"general.display.title": "表示設定",
"display.sidebar.minapp.icon": "ミニアプリのアイコンを表示",
"display.sidebar.files.icon": "ファイルのアイコンを表示",
"display.sidebar.title": "サイドバー設定",
"display.topic.title": "トピック設定",
"display.custom.css": "カスタムCSS",
"display.custom.css.placeholder": "/* ここにカスタムCSSを入力 */",
"input.auto_translate_with_space": "スペースを3回押して翻訳",
"messages.divider": "メッセージ間に区切り線を表示",
"messages.input.paste_long_text_as_file": "長いテキストをファイルとして貼り付け",
"messages.input.send_shortcuts": "送信ショートカット",
"messages.input.show_estimated_tokens": "推定トークン数を表示",
"messages.metrics": "最初のトークンまでの時間 {{time_first_token_millsec}}ms | トークン速度 {{token_speed}} tok/sec",
"messages.input.title": "入力設定",
"messages.markdown_rendering_input_message": "Markdownで入力メッセージをレンダリング",
"messages.math_engine": "数式エンジン",
"messages.model.title": "モデル設定",
"messages.title": "メッセージ設定",
"messages.use_serif_font": "セリフフォントを使用",
"messages.input.paste_long_text_threshold": "長いテキストの長さ",
"model": "デフォルトモデル",
"models.add.add_model": "モデルを追加",
"models.add.group_name": "グループ名",
"models.add.group_name.placeholder": "例ChatGPT",
"models.add.group_name.tooltip": "例ChatGPT",
"models.add.model_id": "モデルID",
"models.add.model_id.placeholder": "必須 例gpt-3.5-turbo",
"models.add.model_id.tooltip": "例gpt-3.5-turbo",
"models.add.model_name": "モデル名",
"models.add.model_name.placeholder": "例GPT-3.5",
"models.default_assistant_model": "デフォルトアシスタントモデル",
"models.default_assistant_model_description": "新しいアシスタントを作成する際に使用されるモデル。アシスタントがモデルを設定していない場合、このモデルが使用されます",
"models.empty": "モデルが見つかりません",
"models.topic_naming_model": "トピック命名モデル",
"models.topic_naming_model_description": "新しいトピックを自動的に命名する際に使用されるモデル",
"models.translate_model": "翻訳モデル",
"models.translate_model_description": "翻訳サービスに使用されるモデル",
"models.translate_model_prompt_message": "翻訳モデルのプロンプトを入力してください",
"models.translate_model_prompt_title": "翻訳モデルのプロンプト",
"models.topic_naming_model_setting_title": "トピック命名モデルの設定",
"models.enable_topic_naming": "トピックの自動命名",
"models.topic_naming_prompt": "トピック命名プロンプト",
"provider": {
"add.name": "プロバイダー名",
"add.name.placeholder": "例OpenAI",
"add.title": "プロバイダーを追加",
"add.type": "プロバイダータイプ",
"api.url.preview": "プレビュー: {{url}}",
"api.url.reset": "リセット",
"api.url.tip": "/で終わる場合、v1を無視します。#で終わる場合、入力されたアドレスを強制的に使用します",
"api_host": "APIホスト",
"api_key": "APIキー",
"api_key.tip": "複数のキーはカンマで区切ります",
"api_version": "APIバージョン",
"check": "チェック",
"check_all_keys": "すべてのキーをチェック",
"check_multiple_keys": "複数のAPIキーをチェック",
"delete.content": "このプロバイダーを削除してもよろしいですか?",
"delete.title": "プロバイダーを削除",
"docs_check": "チェック",
"docs_more_details": "詳細を確認",
"get_api_key": "APIキーを取得",
"no_models": "API接続をチェックする前に、モデルを追加してください",
"not_checked": "未チェック",
"remove_duplicate_keys": "重複キーを削除",
"remove_invalid_keys": "無効なキーを削除",
"search_placeholder": "モデルIDまたは名前を検索",
"title": "モデルプロバイダー"
},
"proxy": {
"mode": {
"custom": "カスタムプロキシ",
"none": "プロキシを使用しない",
"system": "システムプロキシ",
"title": "プロキシモード"
},
"title": "プロキシ設定"
},
"proxy.title": "プロキシアドレス",
"shortcuts": {
"action": "操作",
"key": "キー",
"new_topic": "新しいトピック",
"title": "ショートカット",
"zoom_in": "ズームイン",
"zoom_out": "ズームアウト",
"zoom_reset": "ズームをリセット",
"show_app": "アプリを表示",
"reset_defaults": "デフォルトのショートカットをリセット",
"reset_defaults_confirm": "すべてのショートカットをリセットしてもよろしいですか?",
"press_shortcut": "ショートカットを押す",
"alt_warning": "MacではOption + 文字をショートカットとして使用できません",
"reset_to_default": "デフォルトにリセット",
"clear_shortcut": "ショートカットをクリア",
"toggle_show_assistants": "アシスタントの表示を切り替え",
"toggle_show_topics": "トピックの表示を切り替え",
"copy_last_message": "最後のメッセージをコピー"
},
"theme.auto": "自動",
"theme.dark": "ダークテーマ",
"theme.light": "ライトテーマ",
"theme.title": "テーマ",
"theme.window.style.opaque": "不透明ウィンドウ",
"theme.window.style.title": "ウィンドウスタイル",
"theme.window.style.transparent": "透明ウィンドウ",
"title": "設定",
"topic.position": "トピックの位置",
"topic.position.left": "左",
"topic.position.right": "右",
"topic.show.time": "トピックの時間を表示",
"tray.title": "システムトレイアイコンを有効にする"
},
"translate": {
"any.language": "任意の言語",
"button.translate": "翻訳",
"confirm": {
"content": "翻訳すると元のテキストが上書きされます。続行しますか?",
"title": "翻訳確認"
},
"error.not_configured": "翻訳モデルが設定されていません",
"error.failed": "翻訳に失敗しました",
"input.placeholder": "翻訳するテキストを入力",
"output.placeholder": "翻訳",
"processing": "翻訳中...",
"title": "翻訳",
"close": "閉じる"
},
"tray": {
"quit": "終了",
"show_window": "ウィンドウを表示"
},
"words": {
"knowledgeGraph": "ナレッジグラフ",
"visualization": "可視化",
"show_window": "ウィンドウを表示",
"quit": "終了"
},
"knowledge_base": {
"title": "ナレッジベース",
"search": "ナレッジベースを検索",
"empty": "ナレッジベースが見つかりません",
"drag_file": "ファイルをここにドラッグ",
"file_hint": "{{file_types}} 形式をサポート",
"add": {
"title": "ナレッジベースを追加"
},
"notes": "ノート",
"notes_placeholder": "このナレッジベースの追加情報やコンテキストを入力...",
"delete": "削除",
"rename": "名前を変更",
"urls": "URL",
"add_url": "URLを追加",
"url_placeholder": "URLを入力",
"invalid_url": "無効なURL",
"add_file": "ファイルを追加",
"status": "状態",
"index_all": "すべてをインデックス",
"index_started": "インデックスを開始",
"cancel_index": "インデックスをキャンセル",
"index_cancelled": "インデックスがキャンセルされました",
"status_new": "追加済み",
"status_pending": "保留中",
"status_processing": "処理中",
"status_completed": "完了",
"status_failed": "失敗",
"url_added": "URLが追加されました",
"search_placeholder": "検索するテキストを入力",
"add_note": "ノートを追加",
"no_bases": "ナレッジベースがありません",
"clear_selection": "選択をクリア",
"delete_confirm": "このナレッジベースを削除してもよろしいですか?",
"sitemaps": "サイトマップ",
"add_sitemap": "サイトマップを追加",
"sitemap_placeholder": "サイトマップURLを入力",
"directories": "ディレクトリ",
"add_directory": "ディレクトリを追加",
"directory_placeholder": "ディレクトリパスを入力"
},
"models": {
"pinned": "固定済み",
"search": "モデルを検索...",
"stream_output": "ストリーム出力",
"type": {
"select": "モデルタイプを選択",
"text": "テキスト",
"vision": "画像",
"embedding": "埋め込み"
},
"all": "すべて",
"vision": "画像モデル",
"websearch": "ウェブ検索モデル",
"free": "無料モデル",
"embedding": "埋め込みモデル",
"embedding_model": "埋め込みモデル",
"embedding_model_tooltip": "設定->モデルサービス->管理で追加",
"dimensions": "{{dimensions}} 次元",
"custom_parameters": "カスタムパラメータ",
"add_parameter": "パラメータを追加",
"parameter_name": "パラメータ名",
"parameter_type": {
"string": "テキスト",
"number": "数値",
"boolean": "真偽値"
}
},
"prompts": {
"summarize": "あなたは会話を得意とするアシスタントです。ユーザーの会話を10文字以内のタイトルに要約し、ユーザーの主言語と一致していることを確認してください。句読点や特殊記号は使用しないでください。"
}
}
}

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