Compare commits

...

181 Commits

Author SHA1 Message Date
Teo 8c66f0e41a fix: 修复选中问题 2025-01-27 12:30:35 +08:00
Teo fd1629e004 refactor(settings): 重构设置页面,改为弹框 2025-01-27 12:30:35 +08:00
Nanami 790caae2ab feat: Support configurable chunk size and overlap for knowledge base 2025-01-27 12:30:22 +08:00
Nanami 7f7300e6dc feat: Support configurable chunk size and overlap for knowledge base 2025-01-27 12:30:22 +08:00
kangfenmao 4464992873 docs: update Japanese and Chinese README files to include QQ group link 2025-01-24 18:16:51 +08:00
kangfenmao 37d1c250d2 docs: update README files to include Discord link for community support
- Added a Discord link to the English, Japanese, and Chinese README files, encouraging users to join discussions and seek help alongside the existing Telegram group invitation.
- This change enhances community engagement options for Cherry Studio users.
2025-01-24 18:07:29 +08:00
kangfenmao e9c51579a2 chore(version): 0.9.17 2025-01-24 13:54:04 +08:00
kangfenmao aec2952780 feat: add delete group message confirm modal 2025-01-24 13:13:00 +08:00
kangfenmao 95a1bdac72 fix: resend message logic 2025-01-24 13:02:57 +08:00
kangfenmao 306cb04ef0 fix: siliconflow image url with query params #844
close #844
2025-01-24 09:31:31 +08:00
kangfenmao dc9444a9d4 feat(constants): add C# file extension to textExts array #835
- Updated the textExts array in constant.ts to include '.cs' for C# files, enhancing the file type recognition capabilities.
2025-01-23 13:22:17 +08:00
kangfenmao ad9fefe902 chore(migration): update version and adjust provider type for QwenLM #833
- Incremented version from 60 to 61 in the persisted reducer configuration.
- Updated migration logic to change the provider type for 'qwenlm' from 'openai' to 'qwenlm', ensuring correct identification in the state management.
2025-01-23 13:20:15 +08:00
kangfenmao e07d4838a9 docs: update README files to encourage community support
- Added a call-to-action in English, Japanese, and Chinese README files inviting users to star the project or sponsor its development.
- Enhanced visibility of community engagement options to foster support for Cherry Studio.
2025-01-23 11:59:15 +08:00
hxp0618 30d070040c fix: apikey and ApiHost incorrectly set to empty 2025-01-23 08:30:07 +08:00
hobee f335699958 feat: add new minimax model configuration 2025-01-23 08:29:48 +08:00
kangfenmao b1bc576e3f chore(version): 0.9.16 2025-01-22 16:32:57 +08:00
kangfenmao a6f086e3be fix: group message bugs 2025-01-22 16:29:05 +08:00
kangfenmao 084da9ebab feat: enhance message model handling and user display
- Updated Message component to fallback to message.model if model retrieval fails, improving robustness.
- Refactored MessageHeader to utilize getModelName for better user name display based on message role, enhancing clarity.
- Introduced getModelName function in ModelService to streamline model name retrieval, improving code modularity and readability.
2025-01-22 15:08:44 +08:00
kangfenmao 57aef23741 feat: enhance agent management and UI in AddAssistantPopup and AgentsPage
- Updated AddAssistantPopup to improve layout and styling, ensuring better overflow handling and text display.
- Refactored AgentsPage to utilize a new utility function for grouping agents, enhancing data management and organization.
- Exported getAgentsFromSystemAgents function for better modularity and reusability across components.
2025-01-22 14:47:35 +08:00
kangfenmao 900b11bdf7 feat: enhance translation functionality in MessageMenubar
- Updated translateText function to accept an optional callback for handling translated text directly within the function.
- Refactored MessageMenubar to utilize the new callback mechanism, improving the flow of translated content handling.
- Enhanced error handling during translation to ensure better user feedback in case of failures.
2025-01-22 14:37:15 +08:00
kangfenmao 8aec8a60b3 feat: add file reading functionality and integrate system agents
- Introduced FileService to handle file reading operations via IPC.
- Implemented a new IPC handler for reading files, enhancing the application's ability to access and manage data.
- Integrated system agents from a JSON file, allowing dynamic loading of agent data into the application.
- Updated the AgentsPage and AddAssistantPopup components to utilize the new system agents, improving user experience and functionality.
- Enhanced application state management by adding resourcesPath to the runtime state, ensuring proper resource handling across components.
2025-01-22 14:35:38 +08:00
kangfenmao a566b0e91a refactor: unify message model handling across components
- Replaced direct usage of modelId with model object in Message, MessageHeader, MessageMenubar, and TranslatePage components for consistency.
- Introduced getMessageModelId utility function to streamline model retrieval from messages.
- Updated event handling in Messages component to align with new model structure.
- Enhanced code readability and maintainability by reducing redundancy in model handling.
2025-01-22 13:29:21 +08:00
kangfenmao 4d201059ad feat: conditionally render resend button in MessageMenubar
- Updated MessageMenubar to display the resend button only for user messages, enhancing user experience and preventing unnecessary actions for other roles.
- Refactored the children prop of TextEditPopup to include conditional rendering logic based on message role.
2025-01-22 12:26:40 +08:00
kangfenmao 00d91ecf01 feat: enhance message grouping and styling
- Added new styles for message thought containers and group message wrappers to improve UI layout.
- Updated MessageGroup component to dynamically set the selected message index based on message length.
- Introduced a new event for appending messages, enhancing message handling capabilities.
- Refactored MessageMenubar to support the new append message functionality.
- Adjusted multi-model message style setting to 'fold' for better user experience.
- Improved responsiveness of message grid layout for smaller screens.
2025-01-22 12:04:21 +08:00
kangfenmao 462ac39897 feat: streamline language translation options in MessageMenubar
- Replaced hardcoded language translation options with a dynamic mapping from TranslateLanguageOptions.
- Improved maintainability and scalability of the translation feature by utilizing a centralized configuration for language options.
2025-01-22 10:18:19 +08:00
kangfenmao 3fa1e8c842 feat: add FlagOpen logo to model configuration
- Introduced a new image asset for the FlagOpen model in the assets directory.
- Updated the models configuration to include the FlagOpen logo, allowing for its use in the model logo mapping.
2025-01-22 10:05:50 +08:00
kangfenmao d32a76c087 refactor: improve message rendering and add reasoning content extraction
- Refactored `getMessageBackground` function for better readability.
- Updated `MessageContent` component to use a new `withMessageThought` utility for extracting reasoning content from messages.
- Changed fragment usage to `Fragment` for consistency in JSX.
- Enhanced message handling by separating reasoning content from the main message content.
2025-01-22 09:50:29 +08:00
duanyongcheng77 9e9fd37bda fix: 🐛 fixed bug #779
助手的预设消息保存逻辑的修改
2025-01-21 22:06:52 +08:00
kangfenmao dd464db594 feat: add group message action bar 2025-01-21 17:58:34 +08:00
Teo ccac5358f4 chore(version): update version to 60 and add migration for multiModelMessageStyle setting 2025-01-21 15:16:18 +08:00
Teo e72e324155 refact: 多模型回答优化 2025-01-21 15:16:18 +08:00
kangfenmao 28c18b6651 fix: regenerate message not rewrite reasoning_content 2025-01-21 15:15:55 +08:00
kangfenmao 3d432d810f chore(version): 0.9.15 2025-01-21 14:28:01 +08:00
kangfenmao 21ad28ee62 feat: add deepseek-reasoner model support 2025-01-21 14:28:01 +08:00
kangfenmao f7db1289e4 feat(miniwindow): add up and down key switch menu #792 2025-01-21 10:11:42 +08:00
Cololi f5c547cdb2 feat: add deepseek-reasoner & delete deepseek-coder 2025-01-21 10:05:21 +08:00
ousugo 9160cee919 feat: add WebDAV backup hour options and optimize english hour translations 2025-01-21 08:38:08 +08:00
kangfenmao 298bb8be29 feat: update minapp url to 'https://grok.com' #791
close #791
2025-01-20 16:53:33 +08:00
kangfenmao b800c64fed docs: update readme.md 2025-01-20 15:32:01 +08:00
kangfenmao 504d7b88d4 chore(version): 0.9.14 2025-01-20 13:56:52 +08:00
kangfenmao 713d6dba8f fix: added warning for manual download on failed auto updates, simplified window lifecycle 2025-01-20 13:56:25 +08:00
kangfenmao a6833d5994 chore(version): 0.9.13 2025-01-20 13:11:26 +08:00
kangfenmao d850fd315a feat: add onclick event to login icon in footer component 2025-01-20 12:57:26 +08:00
kangfenmao c04fd62bec feat: extended safety threshold check to include 'thinking-exp' model ids 2025-01-20 12:55:24 +08:00
kangfenmao f86a274cd3 feat: update contact email address 2025-01-20 12:20:46 +08:00
kangfenmao 798a6e8c3e chore(version): 0.9.12 2025-01-20 11:52:26 +08:00
kangfenmao 749353f460 feat: added copy last message feature and translations 2025-01-20 11:09:57 +08:00
kangfenmao c510f5dcce feat: added utility function, sorting, and new shortcut 2025-01-20 10:29:44 +08:00
kangfenmao 46b314303c feat: enable pinned functionality for minapps and update 'flowith' configuration 2025-01-20 09:58:47 +08:00
kangfenmao b01aca9066 fix: prevent unnecessary route changes and trim input field on change 2025-01-20 09:52:58 +08:00
ousugo 725f81c165 fix: conditionally render pin button based on app ID 2025-01-20 09:32:13 +08:00
ousugo c0e25879e5 feat: add Flowith minapp, resolve #780 2025-01-20 09:31:34 +08:00
MrChen 4c22c404ca feat: add the shortcuts for 'clear' and 'new context' and fix (#786)
* Fix: ESC key to exit the expanded editor

* Add the shortcuts for 'clear' and 'new context' to the input bar
Clear Messages: Ctrl+L
New Context: Ctrl+R
https://github.com/CherryHQ/cherry-studio/issues/740
https://github.com/CherryHQ/cherry-studio/issues/766

* Fix: the paste issue when copying from an email (content was pasted as an image; ensure it is pasted as text). Prioritize the text in the clipboard during pasting.
2025-01-20 09:31:09 +08:00
kangfenmao 63673ec39f chore(version): 0.9.11 2025-01-19 20:50:33 +08:00
kangfenmao 88cc783a95 fix: quick assistant bugs 2025-01-19 20:03:45 +08:00
kangfenmao 9c55b4516c feat: add a startup switch for quick assistant 2025-01-19 19:22:25 +08:00
kangfenmao aecc5fefcf feat: translate support stream output 2025-01-19 16:56:35 +08:00
kangfenmao afc2e2f595 feat: auto-scroll to selected menu item on model open 2025-01-19 15:47:19 +08:00
kangfenmao 67b63ee07a refactor: add qwenlm provider 2025-01-19 15:39:48 +08:00
kangfenmao fd7132cd3a fix: store minapp url use base64 data image 2025-01-19 15:35:17 +08:00
kangfenmao a7d9700f06 feat: add mini window 2025-01-19 13:59:32 +08:00
ousugo d9bb552f3f feat: add pinning functionality for MinApp component 2025-01-19 13:59:06 +08:00
ousugo ad2713c0be fix: fix wrong NVIDIA official website link, fix #771 2025-01-19 13:59:06 +08:00
牡丹凤凰 1e756614f9 Delete .github/workflows/update-lmarena.yml 2025-01-19 13:59:06 +08:00
牡丹凤凰 d457dfa3d3 Update update-lmarena.yml 2025-01-19 13:59:06 +08:00
牡丹凤凰 b24d88dfe3 Create update-lmarena.yml 2025-01-19 13:59:06 +08:00
kangfenmao b6d598c52e fix: remove default message for webdav backup initiation 2025-01-19 13:59:06 +08:00
kangfenmao 67e1dd56e9 style: increased padding at the bottom of the sidebar component 2025-01-19 13:59:06 +08:00
kangfenmao 8b5dd427d0 fix: WebDAV not automatic backup on app reopened #752 2025-01-19 13:59:06 +08:00
kangfenmao 4f44afeec4 feat: auto focs input textarea #759
close #759
2025-01-19 13:59:06 +08:00
kangfenmao c46219cd6c feat: improved 'my agents' list rendering 2025-01-19 13:59:06 +08:00
magicdmer 999bd802c4 perf: 优化智能体页面性能和体验 (#756)
* feat: improved model validation and error handling

* refactor: 优化智能体页面下拉流畅度和分类切换效果,让其更加顺畅自然

---------

Co-authored-by: kangfenmao <kangfenmao@qq.com>
Co-authored-by: magicdmer <magicdmer@163.com>
2025-01-19 13:59:06 +08:00
kangfenmao 2300cca070 refactor: improved code organization and reusability 2025-01-19 13:59:06 +08:00
kangfenmao b4de6292c3 feat: improved model safety settings for geminiprovider class 2025-01-19 13:59:06 +08:00
magicdmer 42908e8834 refactor: (GeminiProvider) optimize safety settings handling
- Extract safety threshold logic into getModelSafetySetting method
- gemini-exp-* models not support 'OFF', must use 'BLOCK_NONE'
2025-01-19 13:59:06 +08:00
kangfenmao 57718dda6f feat: update harmblockthreshold for harm_category_civic_integrity 2025-01-19 13:59:06 +08:00
kangfenmao c87e88a53a feat: add civic integrity category to harm block settings in GeminiProvider 2025-01-19 13:59:06 +08:00
kangfenmao 5b00c21f15 feat: update safety settings for specific categories #696
Gemini安全设置是否没有完全关闭
2025-01-19 13:59:06 +08:00
kangfenmao 6276890e5b feat: replaced visionicon with modeltags 2025-01-19 13:59:06 +08:00
kangfenmao a7337ed4b0 feat: add 思维链(CoT) agent 2025-01-19 13:59:06 +08:00
kangfenmao fe0f6318c9 fix: improved openai provider model id validation logic 2025-01-19 13:59:06 +08:00
magicdmer 75742323ea fix: 修正o1模型无法使用的问题 2025-01-19 13:59:06 +08:00
kangfenmao f7f8c6f0c6 fix: remove specific unicode characters from removespecialcharacters function 2025-01-19 13:59:06 +08:00
Linjun e4f4c6cd86 fix issue#762: upon clicking to resend, the conversation content is cleared.
If there is no subsequent message or if the next message is from the user, this message should be resent. delete the old message after processing is complete.
2025-01-19 12:26:55 +08:00
kangfenmao 8eac836e05 feat: improved model validation and error handling 2025-01-16 10:14:32 +08:00
Nanami a6795289da fix: qwenlm context error 2025-01-15 09:09:01 +08:00
kangfenmao eff639ddf9 chore(version): 0.9.10 2025-01-15 08:57:05 +08:00
kangfenmao a046cf32ba fix: artifacts cannot preview 2025-01-14 23:27:54 +08:00
kangfenmao 66bc9cb3f9 refactor: improved type safety and consistency for file handling 2025-01-14 21:02:55 +08:00
kangfenmao 247d1a1846 chore(version): 0.9.9 2025-01-14 20:57:16 +08:00
kangfenmao 0e7fb2b19c refactor: update model group names and sync interval 2025-01-14 20:53:52 +08:00
kangfenmao 8a94bb05ea fix: fix model type logic based on provider properties 2025-01-14 20:32:04 +08:00
Nanami bc454d4dec feat: add support for qwenlm and image upload (#726)
* feat: add support for qwenlm and image upload

* fix: qwenlm return

* feat: add provider config
2025-01-14 18:59:19 +08:00
Teo d388aeecfb feat: 添加模型提及功能,支持多个模型一起回答 2025-01-14 17:46:55 +08:00
kangfenmao 3e33ee6cc5 feat: add release workflow behavior control option 2025-01-14 14:55:32 +08:00
kangfenmao 1991df18d2 chore(version): 0.9.8 2025-01-14 14:36:05 +08:00
kangfenmao de3206b052 chore: update store version and migration 2025-01-14 14:34:36 +08:00
kangfenmao cb3ed42846 style: update markdown link text color 2025-01-14 13:54:10 +08:00
kangfenmao edbc8560cc chore(version): 0.9.7 2025-01-14 13:24:54 +08:00
kangfenmao 56761d6f69 fix: improved input validation and debouncing for assistant settings updates 2025-01-14 13:18:34 +08:00
kangfenmao 2b4cfe7cb1 feat: add grounding source info to gemini message 2025-01-14 12:32:50 +08:00
kangfenmao 6a5faa6610 feat: auto focus search input box #705
close #705
2025-01-13 18:09:59 +08:00
kangfenmao 84979a975c feat: add native app regions support 2025-01-13 18:06:22 +08:00
kangfenmao 74740d7fcc style: update pinned apps style and refactor config model 2025-01-13 17:56:16 +08:00
kangfenmao dff04187be feat: add refresh icon to knowledge base items #567
close #567
2025-01-13 17:42:59 +08:00
kangfenmao a0a13a4015 feat: added openai model configuration and search parameter logic 2025-01-13 16:42:53 +08:00
kangfenmao 2ad6a1f24c feat: check api use selected model 2025-01-13 16:11:09 +08:00
kangfenmao cf7c0fc1fc fix: enforce max tokens above 0 in assistantservice #530 2025-01-13 15:03:37 +08:00
kangfenmao 4ecbf3edab feat: csv download #710
close #710
2025-01-13 14:44:30 +08:00
kangfenmao 83cc4ccec7 refactor: update terminology to 'backup' throughout the application 2025-01-13 14:00:35 +08:00
kangfenmao 3998ad08de feat: add qwenlm minapp 2025-01-13 13:52:45 +08:00
kangfenmao 49a5bc7900 refactor: sidebar minapps 2025-01-13 13:04:01 +08:00
hxp0618 7633d70435 feat: MinApp added to the sidebar does not support direct hiding. 2025-01-13 10:13:47 +08:00
hxp0618 ad9fb9aa6d feat: Adjust the order of settings 2025-01-13 10:13:34 +08:00
hxp0618 fc3d15fae8 feat: minApp supports show/hide, add to the sidebar 2025-01-13 10:13:34 +08:00
王瑞 c45fc2bbad feat: add Grok app logo and configuration 2025-01-12 22:33:46 +08:00
kangfenmao 270216f461 chore(version): 0.9.6 2025-01-09 16:23:29 +08:00
kangfenmao 112e90c15c fix: create agent popup error 2025-01-09 09:15:16 +08:00
kangfenmao c579eff86e chore(version): 0.9.5 2025-01-08 16:52:03 +08:00
kangfenmao f9f5befc59 fix: window navbar layout 2025-01-08 14:35:48 +08:00
kangfenmao 7271a86677 style: update container component styling and navbar responsiveness 2025-01-08 13:25:34 +08:00
kangfenmao 42ede42f62 feat: narrow layout 2025-01-08 12:44:01 +08:00
kangfenmao ea7a42f736 style: adjusted padding and container gap styles 2025-01-08 11:06:51 +08:00
kangfenmao d2836826e7 fix: removed unnecessary conditional logic for attachment button #667 2025-01-08 10:56:22 +08:00
kangfenmao 7d61af7170 Revert "fix:修复单行CodeBlock中显示sub"
This reverts commit 09e6756efe.
2025-01-08 10:46:35 +08:00
kangfenmao 3f4fa9b0ec refactor: refactor upload component layout and styling for responsiveness #674
fix: 当插入文件过多的时候,无法看到输入框了。 close #674
2025-01-08 10:21:17 +08:00
kangfenmao 1bdf6c7955 fix: update model filtering logic to exclude empty ids #493
close #493
2025-01-08 10:00:23 +08:00
kangfenmao 5d005cf5a7 chore: standardize artifact names across platforms 2025-01-08 09:42:38 +08:00
kangfenmao 1fbd727a7b fix: @google/generative-ai local compilation issue #682
close #682
2025-01-07 23:18:18 +08:00
亢奋猫 c9813bb1e2 feature: customizable sidebar module #644 (#680)
* feat:对话的时候支持侧边栏拖拽调整宽度

* feat:对话的时候支持侧边栏拖拽调整宽度

* feat: 隐藏app sidebar 用户体验度提升,不支持隐藏对话

* fix:对话勾选知识库 国际化错误

* refactor: split the SidebarIconsManager module out of DisplaySettings

* style: update SidebarIconsManager style

* ci: fix typecheck

* Revert "feat:对话的时候支持侧边栏拖拽调整宽度"

This reverts commit 58072128f0.

* refactor: merge migrate versions

* refactor: simplify sidebarIcons data structure

* chore: move react-beautiful-dnd to dev dependencies

* chore: use @hello-pangea/dnd replace react-beautiful-dnd

* docs: update translation and formatting of input messages

---------

Co-authored-by: hxp0618 <1169924772@qq.com>
Co-authored-by: huang <hxp0618@gmail.com>
2025-01-07 19:11:12 +08:00
kangfenmao edac2004a0 feat: add gemini files support 2025-01-07 16:49:11 +08:00
kangfenmao a051f9fa44 feat: add optional free model tag display 2025-01-07 11:23:32 +08:00
kangfenmao a70e69caf9 feat: enable web search for zhipu ai provider #657 2025-01-07 10:53:34 +08:00
kangfenmao 4896db93fd fix: improved error message formatting in api service 2025-01-07 10:19:21 +08:00
kangfenmao 2e7ecbc753 feat: add ModelTags component 2025-01-07 09:54:22 +08:00
kangfenmao f68bd4d8d8 feat: add support for 'aihubmix' models and aihubmix llm provider 2025-01-07 09:46:05 +08:00
kangfenmao d0948e6f8a feature: customizable sidebar module #644
close #644
2025-01-06 16:59:10 +08:00
kangfenmao ac9017c031 feat: add search message shortcut #366 2025-01-06 16:29:39 +08:00
kangfenmao de1d79abb8 fix: the minimum width limit of the window is too large #544
close #544
2025-01-06 16:25:00 +08:00
kangfenmao ad577818dd fix: generating topic name after exporting prompt file name is invalid #641
close #641
2025-01-06 15:50:57 +08:00
kangfenmao bb50447a98 fix: Ollama is unable to create a knowledge base using a local embedding model #630 2025-01-06 15:43:20 +08:00
kangfenmao 158f9bf1ad fix: turn off spell check #648
The next version will be released. close #648
2025-01-06 15:10:03 +08:00
kangfenmao 6a9bc103d7 feat: added optional chaining for code variable 2025-01-06 14:54:04 +08:00
xx-moos 529ec3612e fix: 修复 message 显示时间过长的问题 2025-01-06 14:43:31 +08:00
kangfenmao d241c38c61 style: border radius use var 2025-01-04 22:50:44 +08:00
kangfenmao ee5ed8c565 style: logo v3
# Conflicts:
#	src/renderer/src/assets/images/logo.png
2025-01-04 21:52:05 +08:00
huang dc73661678 feat: 支持 mermaid 点击按钮放大缩小以及鼠标滑轮放大缩小 2025-01-04 19:17:39 +08:00
huang ce973ce3a0 feat: 支持 mermaid 点击按钮放大缩小以及鼠标滑轮放大缩小 2025-01-04 19:17:39 +08:00
huang a0413158c8 fix: 修复在macOS m1 中点击全屏幕后,点击关闭后黑屏的问题 2025-01-04 19:17:39 +08:00
kangfenmao 6cb3b16451 fix: Qwen2.5和Qwen的划分不合理 #633 2025-01-03 18:05:01 +08:00
huang 08b0990cf9 fix: 中文国际化错误 2025-01-03 17:35:17 +08:00
kangfenmao 10b9940edd chore(version): 0.9.4 2025-01-02 21:34:30 +08:00
kangfenmao 4cbdd563e8 feat: add translations and file management features 2025-01-02 18:29:36 +08:00
kangfenmao dba1f76db7 feat: update assistantmodelsettings to persist custom parameters 2025-01-02 17:21:33 +08:00
kangfenmao 15fb605eb4 feat: improved form validation and model addition functionality 2025-01-02 16:58:58 +08:00
kangfenmao 1bf147fa6a refactor: improve model generation and handling functionality 2025-01-02 16:39:30 +08:00
kangfenmao a782b2b4aa fix: 腾讯混元的联网开关 #575 2025-01-02 16:26:24 +08:00
kangfenmao 7f92cb59a6 feat: add more classname 2025-01-02 16:25:50 +08:00
kangfenmao 6009ae84fb fix: 重新发送按钮无反应 #587 2025-01-02 15:42:47 +08:00
kangfenmao 038aa2d5cc feat: paintings add prompt enhancement params 2025-01-02 14:51:52 +08:00
kangfenmao 6384525e20 feat: added error handling and knowledge base provider support 2025-01-02 14:16:37 +08:00
kangfenmao 3fc7911c97 feat: add new branch option to message menubar 2025-01-02 13:41:51 +08:00
kangfenmao 5f55d8c22c style: adjusted padding and border styles in settingsgroup component 2025-01-02 13:37:04 +08:00
kangfenmao d9f7bcfc21 feat: custom parameters add json type 2025-01-02 13:34:21 +08:00
kangfenmao aa72794967 feat: improved translation features and settings 2025-01-02 12:21:22 +08:00
zhouxl 09e6756efe fix:修复单行CodeBlock中显示sub 2025-01-02 11:47:34 +08:00
kangfenmao dde0400f0d chore: update hika app assets and styles 2025-01-02 11:18:15 +08:00
kangfenmao 1d3a01dd49 feat: add sync status show 2025-01-02 11:07:20 +08:00
YongHao Hu 63cdc15bc2 feat: add hika minapp 2025-01-02 11:06:57 +08:00
kangfenmao b2818f8619 fix: reduce batch size for knowledge service and openai embeddings 2024-12-31 14:41:08 +08:00
kangfenmao 8ef9fb0216 feat: add github auto assignment workflow 2024-12-31 00:43:16 +08:00
kangfenmao 63488e6fab chore(version): 0.9.3 2024-12-31 00:38:20 +08:00
kangfenmao 6d9013f0a1 fix: 知识库无法向量化 MD 文件 #569 2024-12-31 00:11:51 +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
191 changed files with 10987 additions and 3513 deletions
+5 -14
View File
@@ -78,19 +78,10 @@ jobs:
run: node scripts/replace-spaces.js run: node scripts/replace-spaces.js
- name: Release - name: Release
uses: softprops/action-gh-release@v2 uses: ncipollo/release-action@v1
with: with:
draft: true draft: true
files: | allowUpdates: true
dist/*.exe makeLatest: false
dist/*.zip artifacts: 'dist/*.exe,dist/*.zip,dist/*.dmg,dist/*.AppImage,dist/*.snap,dist/*.deb,dist/*.rpm,dist/*.tar.gz,dist/latest*.yml,dist/*.blockmap'
dist/*.dmg token: ${{ secrets.GH_TOKEN }}
dist/*.AppImage
dist/*.snap
dist/*.deb
dist/*.rpm
dist/*.tar.gz
dist/latest*.yml
dist/*.blockmap
env:
GITHUB_TOKEN: ${{ secrets.GH_TOKEN }}
@@ -0,0 +1,19 @@
diff --git a/src/markdown-loader.js b/src/markdown-loader.js
index 8a17cb7f5a68d90d2be21682db6e95ce22a3e71c..9ee868ef9d4ff3dc914b3abc3c8006deb1e9c6c6 100644
--- a/src/markdown-loader.js
+++ b/src/markdown-loader.js
@@ -1,5 +1,4 @@
import { micromark } from 'micromark';
-import { mdxJsx } from 'micromark-extension-mdx-jsx';
import { gfmHtml, gfm } from 'micromark-extension-gfm';
import createDebugMessages from 'debug';
import fs from 'node:fs';
@@ -21,7 +20,7 @@ export class MarkdownLoader extends BaseLoader {
? (await getSafe(this.filePathOrUrl, { format: 'buffer' })).body
: await stream2buffer(fs.createReadStream(this.filePathOrUrl));
this.debug('MarkdownLoader stream created');
- const result = micromark(buffer, { extensions: [gfm(), mdxJsx()], htmlExtensions: [gfmHtml()] });
+ const result = micromark(buffer, { extensions: [gfm()], htmlExtensions: [gfmHtml()] });
this.debug('Markdown parsed...');
const webLoader = new WebLoader({
urlOrContent: result,
@@ -15,3 +15,203 @@ index 50c3c4064af17bc4c7c46554d8f2419b3afceb0e..632c9b2e04d2e0e3bb09ef1cd8f29d25
} }
static getInstance() { static getInstance() {
return RAGEmbedding.singleton; return RAGEmbedding.singleton;
diff --git a/src/loaders/local-path-loader.d.ts b/src/loaders/local-path-loader.d.ts
index 48c20e68c469cd309be2dc8f28e44c1bd04a26e9..87002be39e7305a02e2a607b0c0d95cbbc359f9d 100644
--- a/src/loaders/local-path-loader.d.ts
+++ b/src/loaders/local-path-loader.d.ts
@@ -1,19 +1,29 @@
-import { BaseLoader } from '@llm-tools/embedjs-interfaces';
+import { BaseLoader } from "@llm-tools/embedjs-interfaces";
export declare class LocalPathLoader extends BaseLoader<{
- type: 'LocalPathLoader';
+ type: "LocalPathLoader";
}> {
- private readonly debug;
- private readonly path;
- constructor({ path }: {
- path: string;
- });
- getUnfilteredChunks(): AsyncGenerator<{
- metadata: {
- type: "LocalPathLoader";
- originalPath: string;
- source: string;
- };
- pageContent: string;
- }, void, unknown>;
- private recursivelyAddPath;
+ private readonly debug;
+ private readonly path;
+ constructor({
+ path,
+ chunkSize,
+ chunkOverlap,
+ }: {
+ path: string;
+ chunkSize?: number;
+ chunkOverlap?: number;
+ });
+ getUnfilteredChunks(): AsyncGenerator<
+ {
+ metadata: {
+ type: "LocalPathLoader";
+ originalPath: string;
+ source: string;
+ };
+ pageContent: string;
+ },
+ void,
+ unknown
+ >;
+ private recursivelyAddPath;
}
diff --git a/src/loaders/local-path-loader.js b/src/loaders/local-path-loader.js
index 4cf8a6bd1d890244c8ec49d4a05ee3bd58861c79..fd0fe1951c73da315b0c9bf4a8f33effbadb9f8f 100644
--- a/src/loaders/local-path-loader.js
+++ b/src/loaders/local-path-loader.js
@@ -8,8 +8,8 @@ import { BaseLoader } from '@llm-tools/embedjs-interfaces';
export class LocalPathLoader extends BaseLoader {
debug = createDebugMessages('embedjs:loader:LocalPathLoader');
path;
- constructor({ path }) {
- super(`LocalPathLoader_${md5(path)}`, { path });
+ constructor({ path, chunkSize, chunkOverlap}) {
+ super(`LocalPathLoader_${md5(path)}`, { path }, chunkSize ?? 1000, chunkOverlap ?? 0);
this.path = path;
}
async *getUnfilteredChunks() {
@@ -36,10 +36,12 @@ export class LocalPathLoader extends BaseLoader {
const extension = currentPath.split('.').pop().toLowerCase();
if (extension === 'md' || extension === 'mdx')
mime = 'text/markdown';
+ if (extension === 'txt')
+ mime = 'text/plain';
this.debug(`File '${this.path}' mime type updated to 'text/markdown'`);
}
try {
- const loader = await createLoaderFromMimeType(currentPath, mime);
+ const loader = await createLoaderFromMimeType(currentPath, mime, this.chunkSize, this.chunkOverlap);
for await (const result of await loader.getUnfilteredChunks()) {
yield {
pageContent: result.pageContent,
diff --git a/src/util/mime.d.ts b/src/util/mime.d.ts
index 57f56a1b8edc98366af9f84d671676c41c2f01ca..f53856fa9c78afbeee9e085c7ed0b3a131f8ee5a 100644
--- a/src/util/mime.d.ts
+++ b/src/util/mime.d.ts
@@ -1,2 +1,7 @@
-import { BaseLoader } from '@llm-tools/embedjs-interfaces';
-export declare function createLoaderFromMimeType(loaderData: string, mimeType: string): Promise<BaseLoader>;
+import { BaseLoader } from "@llm-tools/embedjs-interfaces";
+export declare function createLoaderFromMimeType(
+ loaderData: string,
+ mimeType: string,
+ chunkSize?: number,
+ chunkOverlap?: number
+): Promise<BaseLoader>;
diff --git a/src/util/mime.js b/src/util/mime.js
index 9af30bd5b8cf42985f547073a4c19756292c33a3..54ae20343131a533ab70236d3060b6accc8f6126 100644
--- a/src/util/mime.js
+++ b/src/util/mime.js
@@ -1,7 +1,9 @@
import mime from 'mime';
import createDebugMessages from 'debug';
import { TextLoader } from '../loaders/text-loader.js';
-export async function createLoaderFromMimeType(loaderData, mimeType) {
+import fs from 'node:fs';
+
+export async function createLoaderFromMimeType(loaderData, mimeType, chunkSize, chunkOverlap) {
createDebugMessages('embedjs:util:createLoaderFromMimeType')(`Incoming mime type '${mimeType}'`);
switch (mimeType) {
case 'application/msword':
@@ -10,7 +12,7 @@ export async function createLoaderFromMimeType(loaderData, mimeType) {
throw new Error('Package `@llm-tools/embedjs-loader-msoffice` needs to be installed to load docx files');
});
createDebugMessages('embedjs:util:createLoaderFromMimeType')('Dynamically imported DocxLoader');
- return new DocxLoader({ filePathOrUrl: loaderData });
+ return new DocxLoader({ filePathOrUrl: loaderData, chunkSize, chunkOverlap });
}
case 'application/vnd.ms-excel':
case 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet': {
@@ -18,21 +20,21 @@ export async function createLoaderFromMimeType(loaderData, mimeType) {
throw new Error('Package `@llm-tools/embedjs-loader-msoffice` needs to be installed to load excel files');
});
createDebugMessages('embedjs:util:createLoaderFromMimeType')('Dynamically imported ExcelLoader');
- return new ExcelLoader({ filePathOrUrl: loaderData });
+ return new ExcelLoader({ filePathOrUrl: loaderData, chunkSize, chunkOverlap });
}
case 'application/pdf': {
const { PdfLoader } = await import('@llm-tools/embedjs-loader-pdf').catch(() => {
throw new Error('Package `@llm-tools/embedjs-loader-pdf` needs to be installed to load PDF files');
});
createDebugMessages('embedjs:util:createLoaderFromMimeType')('Dynamically imported PdfLoader');
- return new PdfLoader({ filePathOrUrl: loaderData });
+ return new PdfLoader({ filePathOrUrl: loaderData, chunkSize, chunkOverlap });
}
case 'application/vnd.openxmlformats-officedocument.presentationml.presentation': {
const { PptLoader } = await import('@llm-tools/embedjs-loader-msoffice').catch(() => {
throw new Error('Package `@llm-tools/embedjs-loader-msoffice` needs to be installed to load pptx files');
});
createDebugMessages('embedjs:util:createLoaderFromMimeType')('Dynamically imported PptLoader');
- return new PptLoader({ filePathOrUrl: loaderData });
+ return new PptLoader({ filePathOrUrl: loaderData, chunkSize, chunkOverlap });
}
case 'text/plain': {
const fineType = mime.getType(loaderData);
@@ -42,24 +44,26 @@ export async function createLoaderFromMimeType(loaderData, mimeType) {
throw new Error('Package `@llm-tools/embedjs-loader-csv` needs to be installed to load CSV files');
});
createDebugMessages('embedjs:util:createLoaderFromMimeType')('Dynamically imported CsvLoader');
- return new CsvLoader({ filePathOrUrl: loaderData });
+ return new CsvLoader({ filePathOrUrl: loaderData, chunkSize, chunkOverlap });
+ }
+ else{
+ const content = fs.readFileSync(loaderData, 'utf-8');
+ return new TextLoader({ text: content, chunkSize, chunkOverlap });
}
- else
- return new TextLoader({ text: loaderData });
}
case 'application/csv': {
const { CsvLoader } = await import('@llm-tools/embedjs-loader-csv').catch(() => {
throw new Error('Package `@llm-tools/embedjs-loader-csv` needs to be installed to load CSV files');
});
createDebugMessages('embedjs:util:createLoaderFromMimeType')('Dynamically imported CsvLoader');
- return new CsvLoader({ filePathOrUrl: loaderData });
+ return new CsvLoader({ filePathOrUrl: loaderData, chunkSize, chunkOverlap });
}
case 'text/html': {
const { WebLoader } = await import('@llm-tools/embedjs-loader-web').catch(() => {
throw new Error('Package `@llm-tools/embedjs-loader-web` needs to be installed to load web documents');
});
createDebugMessages('embedjs:util:createLoaderFromMimeType')('Dynamically imported WebLoader');
- return new WebLoader({ urlOrContent: loaderData });
+ return new WebLoader({ urlOrContent: loaderData, chunkSize, chunkOverlap });
}
case 'text/xml': {
const { SitemapLoader } = await import('@llm-tools/embedjs-loader-sitemap').catch(() => {
@@ -67,14 +71,14 @@ export async function createLoaderFromMimeType(loaderData, mimeType) {
});
createDebugMessages('embedjs:util:createLoaderFromMimeType')('Dynamically imported SitemapLoader');
if (await SitemapLoader.test(loaderData)) {
- return new SitemapLoader({ url: loaderData });
+ return new SitemapLoader({ url: loaderData, chunkSize, chunkOverlap });
}
//This is not a Sitemap but is still XML
const { XmlLoader } = await import('@llm-tools/embedjs-loader-xml').catch(() => {
throw new Error('Package `@llm-tools/embedjs-loader-xml` needs to be installed to load XML documents');
});
createDebugMessages('embedjs:util:createLoaderFromMimeType')('Dynamically imported XmlLoader');
- return new XmlLoader({ filePathOrUrl: loaderData });
+ return new XmlLoader({ filePathOrUrl: loaderData, chunkSize, chunkOverlap });
}
case 'text/x-markdown':
case 'text/markdown': {
@@ -82,7 +86,7 @@ export async function createLoaderFromMimeType(loaderData, mimeType) {
throw new Error('Package `@llm-tools/embedjs-loader-markdown` needs to be installed to load markdown files');
});
createDebugMessages('embedjs:util:createLoaderFromMimeType')('Dynamically imported MarkdownLoader');
- return new MarkdownLoader({ filePathOrUrl: loaderData });
+ return new MarkdownLoader({ filePathOrUrl: loaderData, chunkSize, chunkOverlap });
}
case undefined:
throw new Error(`MIME type could not be detected. Please file an issue if you think this is a bug.`);
+5 -3
View File
@@ -9,11 +9,11 @@
# 🍒 Cherry Studio # 🍒 Cherry Studio
![](https://github.com/user-attachments/assets/7b4f2f78-5cbe-4be8-9aec-f98d8405a505)
Cherry Studio is a desktop client that supports for multiple LLM providers, available on Windows, Mac and Linux. Cherry Studio is a desktop client that supports for multiple LLM providers, available on Windows, Mac and Linux.
👏 Join [Telegram Group](https://t.me/CherryStudioAI) 👏 Join [Telegram Group](https://t.me/CherryStudioAI)[Discord](https://discord.gg/C3xrXWjY) | [QQ Group](https://qm.qq.com/q/pQPuHMjUeQ)
❤️ Like Cherry Studio? Give it a star 🌟 or [Sponsor](docs/sponsor.md) to support the development!
# 🌠 Screenshot # 🌠 Screenshot
@@ -23,6 +23,8 @@ Cherry Studio is a desktop client that supports for multiple LLM providers, avai
# 🌟 Key Features # 🌟 Key Features
![](https://github.com/user-attachments/assets/7b4f2f78-5cbe-4be8-9aec-f98d8405a505)
1. **Diverse LLM Provider Support**: 1. **Diverse LLM Provider Support**:
- ☁️ Major LLM Cloud Services: OpenAI, Gemini, Anthropic, and more - ☁️ Major LLM Cloud Services: OpenAI, Gemini, Anthropic, and more
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 353 KiB

After

Width:  |  Height:  |  Size: 41 KiB

BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 210 KiB

After

Width:  |  Height:  |  Size: 137 KiB

BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 195 KiB

After

Width:  |  Height:  |  Size: 84 KiB

+47
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
Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.9 KiB

After

Width:  |  Height:  |  Size: 4.0 KiB

+5 -3
View File
@@ -9,11 +9,11 @@
# 🍒 Cherry Studio # 🍒 Cherry Studio
![](https://github.com/user-attachments/assets/7b4f2f78-5cbe-4be8-9aec-f98d8405a505)
Cherry Studioは、複数のLLMプロバイダーをサポートするデスクトップクライアントで、Windows、Mac、Linuxで利用可能です。 Cherry Studioは、複数のLLMプロバイダーをサポートするデスクトップクライアントで、Windows、Mac、Linuxで利用可能です。
👏 [Telegramグループ](https://t.me/CherryStudioAI)に参加しましょう 👏 [Telegram](https://t.me/CherryStudioAI)[Discord](https://discord.gg/C3xrXWjY) | [QQグループ](https://qm.qq.com/q/pQPuHMjUeQ)
❤️ Cherry Studioをお気に入りにしましたか?小さな星をつけてください 🌟 または [スポンサー](sponsor.md) をして開発をサポートしてください!❤️
# 🌠 スクリーンショット # 🌠 スクリーンショット
@@ -23,6 +23,8 @@ Cherry Studioは、複数のLLMプロバイダーをサポートするデスク
# 🌟 主な機能 # 🌟 主な機能
![](https://github.com/user-attachments/assets/7b4f2f78-5cbe-4be8-9aec-f98d8405a505)
1. **多様な LLM サービス対応** 1. **多様な LLM サービス対応**
- ☁️ 主要な LLM クラウドサービス対応:OpenAI、Gemini、Anthropic など - ☁️ 主要な LLM クラウドサービス対応:OpenAI、Gemini、Anthropic など
+5 -3
View File
@@ -9,11 +9,11 @@
# 🍒 Cherry Studio # 🍒 Cherry Studio
![](https://github.com/user-attachments/assets/995910f3-177a-4d1e-97ea-04e3b009ba36)
Cherry Studio 是一款支持多个大语言模型(LLM)服务商的桌面客户端,兼容 Windows、Mac 和 Linux 系统。 Cherry Studio 是一款支持多个大语言模型(LLM)服务商的桌面客户端,兼容 Windows、Mac 和 Linux 系统。
👏 欢迎加入 [Telegram 群组](https://t.me/CherryStudioAI) 👏 欢迎加入 [Telegram 群组](https://t.me/CherryStudioAI)[Discord](https://discord.gg/C3xrXWjY) | [QQ 群](https://qm.qq.com/q/pQPuHMjUeQ)
❤️ 喜欢 Cherry Studio? 点亮小星星 🌟 或 [赞助开发者](sponsor.md)! ❤️
# 🌠 界面 # 🌠 界面
@@ -23,6 +23,8 @@ Cherry Studio 是一款支持多个大语言模型(LLM)服务商的桌面客
# 🌟 主要特性 # 🌟 主要特性
![](https://github.com/user-attachments/assets/995910f3-177a-4d1e-97ea-04e3b009ba36)
1. **多样化 LLM 服务支持** 1. **多样化 LLM 服务支持**
- ☁️ 支持主流 LLM 云服务:OpenAI、Gemini、Anthropic、硅基流动等 - ☁️ 支持主流 LLM 云服务:OpenAI、Gemini、Anthropic、硅基流动等
+8 -12
View File
@@ -32,6 +32,10 @@ asarUnpack:
- '**/*.{node,dll,metal,exp,lib}' - '**/*.{node,dll,metal,exp,lib}'
win: win:
executableName: Cherry Studio executableName: Cherry Studio
artifactName: ${productName}-${version}-portable.${ext}
target:
- target: nsis
- target: portable
nsis: nsis:
artifactName: ${productName}-${version}-setup.${ext} artifactName: ${productName}-${version}-setup.${ext}
shortcutName: ${productName} shortcutName: ${productName}
@@ -39,9 +43,11 @@ nsis:
createDesktopShortcut: always createDesktopShortcut: always
allowToChangeInstallationDirectory: true allowToChangeInstallationDirectory: true
oneClick: false oneClick: false
include: build/nsis-installer.nsh
mac: mac:
entitlementsInherit: build/entitlements.mac.plist entitlementsInherit: build/entitlements.mac.plist
notarize: false notarize: false
artifactName: ${productName}-${version}-${arch}.${ext}
extendInfo: extendInfo:
- NSCameraUsageDescription: Application requests access to the device's camera. - NSCameraUsageDescription: Application requests access to the device's camera.
- NSMicrophoneUsageDescription: Application requests access to the device's microphone. - NSMicrophoneUsageDescription: Application requests access to the device's microphone.
@@ -56,9 +62,8 @@ mac:
arch: arch:
- arm64 - arm64
- x64 - x64
dmg:
artifactName: ${productName}-${version}-${arch}.${ext}
linux: linux:
artifactName: ${productName}-${version}-${arch}.${ext}
target: target:
- target: AppImage - target: AppImage
arch: arch:
@@ -66,8 +71,6 @@ linux:
- x64 - x64
maintainer: electronjs.org maintainer: electronjs.org
category: Utility category: Utility
appImage:
artifactName: ${productName}-${version}-${arch}.${ext}
publish: publish:
provider: generic provider: generic
url: https://cherrystudio.ocool.online url: https://cherrystudio.ocool.online
@@ -77,11 +80,4 @@ afterPack: scripts/after-pack.js
afterSign: scripts/notarize.js afterSign: scripts/notarize.js
releaseInfo: releaseInfo:
releaseNotes: | releaseNotes: |
Azure OpenAI 嵌入模型支持 错误修复
增加嵌入模型专用服务商 JINA
支持选择图片和文件后不输入内容即可发送
增加 GitHub Copilot 小程序
模型编辑支持显示模型 ID
模型选择按钮增加服务商显示
修复话题移动到不同的助手后搜索结果不能定位问题
修复输出包含 sub 无法正常显示问题
+1 -1
View File
@@ -50,7 +50,7 @@ export default defineConfig({
} }
}, },
optimizeDeps: { optimizeDeps: {
exclude: ['chunk-QH6N6I7P.js', 'chunk-PB73W2YU.js'] exclude: ['chunk-RK3FTE5R.js']
} }
} }
}) })
+4 -3
View File
@@ -1,6 +1,6 @@
{ {
"name": "CherryStudio", "name": "CherryStudio",
"version": "0.9.2", "version": "0.9.17",
"private": true, "private": true,
"description": "A powerful AI assistant for producer.", "description": "A powerful AI assistant for producer.",
"main": "./out/main/index.js", "main": "./out/main/index.js",
@@ -50,10 +50,11 @@
"@electron-toolkit/preload": "^3.0.0", "@electron-toolkit/preload": "^3.0.0",
"@electron-toolkit/utils": "^3.0.0", "@electron-toolkit/utils": "^3.0.0",
"@electron/notarize": "^2.5.0", "@electron/notarize": "^2.5.0",
"@google/generative-ai": "^0.21.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": "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-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-csv": "^0.1.25",
"@llm-tools/embedjs-loader-markdown": "^0.1.25", "@llm-tools/embedjs-loader-markdown": "patch:@llm-tools/embedjs-loader-markdown@npm%3A0.1.25#~/.yarn/patches/@llm-tools-embedjs-loader-markdown-npm-0.1.25-d1d536d640.patch",
"@llm-tools/embedjs-loader-msoffice": "^0.1.25", "@llm-tools/embedjs-loader-msoffice": "^0.1.25",
"@llm-tools/embedjs-loader-pdf": "^0.1.25", "@llm-tools/embedjs-loader-pdf": "^0.1.25",
"@llm-tools/embedjs-loader-sitemap": "^0.1.25", "@llm-tools/embedjs-loader-sitemap": "^0.1.25",
@@ -80,7 +81,6 @@
"@electron-toolkit/eslint-config-prettier": "^2.0.0", "@electron-toolkit/eslint-config-prettier": "^2.0.0",
"@electron-toolkit/eslint-config-ts": "^1.0.1", "@electron-toolkit/eslint-config-ts": "^1.0.1",
"@electron-toolkit/tsconfig": "^1.0.1", "@electron-toolkit/tsconfig": "^1.0.1",
"@google/generative-ai": "^0.21.0",
"@hello-pangea/dnd": "^16.6.0", "@hello-pangea/dnd": "^16.6.0",
"@kangfenmao/keyv-storage": "^0.1.0", "@kangfenmao/keyv-storage": "^0.1.0",
"@reduxjs/toolkit": "^2.2.5", "@reduxjs/toolkit": "^2.2.5",
@@ -95,6 +95,7 @@
"@types/tinycolor2": "^1", "@types/tinycolor2": "^1",
"@vitejs/plugin-react": "^4.2.1", "@vitejs/plugin-react": "^4.2.1",
"antd": "^5.22.5", "antd": "^5.22.5",
"applescript": "^1.0.0",
"axios": "^1.7.3", "axios": "^1.7.3",
"browser-image-compression": "^2.0.2", "browser-image-compression": "^2.0.2",
"dayjs": "^1.11.11", "dayjs": "^1.11.11",
+2 -1
View File
@@ -87,7 +87,8 @@ export const textExts = [
'.gradle', // Gradle 构建文件 '.gradle', // Gradle 构建文件
'.groovy', // Gradle 构建文件 '.groovy', // Gradle 构建文件
'.kts', // Kotlin Script 文件 '.kts', // Kotlin Script 文件
'.java' // Java 代码文件 '.java', // Java 代码文件
'.cs' // C# 代码文件
] ]
export const ZOOM_SHORTCUTS = [ export const ZOOM_SHORTCUTS = [
+202
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>
File diff suppressed because one or more lines are too long
+117
View File
@@ -0,0 +1,117 @@
import Cocoa
import Foundation
class TextSelectionObserver: NSObject {
let workspace = NSWorkspace.shared
var lastSelectedText: String?
override init() {
super.init()
//
let observer = NSWorkspace.shared.notificationCenter
observer.addObserver(
self,
selector: #selector(handleSelectionChange),
name: NSWorkspace.didActivateApplicationNotification,
object: nil
)
//
var axObserver: AXObserver?
let error = AXObserverCreate(getpid(), { observer, element, notification, userData in
let selfPointer = userData!.load(as: TextSelectionObserver.self)
selfPointer.checkSelectedText()
}, &axObserver)
if error == .success, let axObserver = axObserver {
CFRunLoopAddSource(
RunLoop.main.getCFRunLoop(),
AXObserverGetRunLoopSource(axObserver),
.defaultMode
)
//
updateActiveAppObserver(axObserver)
}
}
@objc func handleSelectionChange(_ notification: Notification) {
//
var axObserver: AXObserver?
let error = AXObserverCreate(getpid(), { _, _, _, _ in }, &axObserver)
if error == .success, let axObserver = axObserver {
updateActiveAppObserver(axObserver)
}
}
func updateActiveAppObserver(_ axObserver: AXObserver) {
guard let app = workspace.frontmostApplication else { return }
let pid = app.processIdentifier
let element = AXUIElementCreateApplication(pid)
//
AXObserverAddNotification(
axObserver,
element,
kAXSelectedTextChangedNotification as CFString,
UnsafeMutableRawPointer(Unmanaged.passUnretained(self).toOpaque())
)
}
func checkSelectedText() {
if let text = getSelectedText() {
if text.count > 0 && text != lastSelectedText {
print(text)
fflush(stdout)
lastSelectedText = text
}
}
}
func getSelectedText() -> String? {
guard let app = NSWorkspace.shared.frontmostApplication else { return nil }
let pid = app.processIdentifier
let axApp = AXUIElementCreateApplication(pid)
var focusedElement: AnyObject?
// Get focused element
let result = AXUIElementCopyAttributeValue(axApp, kAXFocusedUIElementAttribute as CFString, &focusedElement)
guard result == .success else { return nil }
// Try different approaches to get selected text
var selectedText: AnyObject?
// First try: Direct selected text
var textResult = AXUIElementCopyAttributeValue(focusedElement as! AXUIElement, kAXSelectedTextAttribute as CFString, &selectedText)
// Second try: Selected text in text area
if textResult != .success {
var selectedTextRange: AnyObject?
textResult = AXUIElementCopyAttributeValue(focusedElement as! AXUIElement, kAXSelectedTextRangeAttribute as CFString, &selectedTextRange)
if textResult == .success {
textResult = AXUIElementCopyAttributeValue(focusedElement as! AXUIElement, kAXValueAttribute as CFString, &selectedText)
}
}
// Third try: Get selected text from parent element
if textResult != .success {
var parent: AnyObject?
if AXUIElementCopyAttributeValue(focusedElement as! AXUIElement, kAXParentAttribute as CFString, &parent) == .success {
textResult = AXUIElementCopyAttributeValue(parent as! AXUIElement, kAXSelectedTextAttribute as CFString, &selectedText)
}
}
guard textResult == .success, let text = selectedText as? String else { return nil }
return text
}
}
let observer = TextSelectionObserver()
signal(SIGINT) { _ in
exit(0)
}
RunLoop.main.run()
+45
View File
@@ -10,10 +10,14 @@ import AppUpdater from './services/AppUpdater'
import BackupManager from './services/BackupManager' import BackupManager from './services/BackupManager'
import { configManager } from './services/ConfigManager' import { configManager } from './services/ConfigManager'
import { ExportService } from './services/ExportService' import { ExportService } from './services/ExportService'
import FileService from './services/FileService'
import FileStorage from './services/FileStorage' import FileStorage from './services/FileStorage'
import { GeminiService } from './services/GeminiService'
import KnowledgeService from './services/KnowledgeService' import KnowledgeService from './services/KnowledgeService'
import { registerShortcuts, unregisterAllShortcuts } from './services/ShortcutService' import { registerShortcuts, unregisterAllShortcuts } from './services/ShortcutService'
import { TrayService } from './services/TrayService'
import { windowService } from './services/WindowService' import { windowService } from './services/WindowService'
import { getResourcePath } from './utils'
import { compress, decompress } from './utils/zip' import { compress, decompress } from './utils/zip'
const fileManager = new FileStorage() const fileManager = new FileStorage()
@@ -29,6 +33,7 @@ export function registerIpc(mainWindow: BrowserWindow, app: Electron.App) {
appPath: app.getAppPath(), appPath: app.getAppPath(),
filesPath: path.join(app.getPath('userData'), 'Data', 'Files'), filesPath: path.join(app.getPath('userData'), 'Data', 'Files'),
appDataPath: app.getPath('userData'), appDataPath: app.getPath('userData'),
resourcesPath: getResourcePath(),
logsPath: log.transports.file.getFile().path logsPath: log.transports.file.getFile().path
})) }))
@@ -51,6 +56,16 @@ export function registerIpc(mainWindow: BrowserWindow, app: Electron.App) {
configManager.setTray(isActive) configManager.setTray(isActive)
}) })
ipcMain.handle('app:restart-tray', () => TrayService.getInstance().restartTray())
ipcMain.handle('config:set', (_, key: string, value: any) => {
configManager.set(key, value)
})
ipcMain.handle('config:get', (_, key: string) => {
return configManager.get(key)
})
// theme // theme
ipcMain.handle('app:set-theme', (_, theme: ThemeMode) => { ipcMain.handle('app:set-theme', (_, theme: ThemeMode) => {
configManager.setTheme(theme) configManager.setTheme(theme)
@@ -116,6 +131,10 @@ export function registerIpc(mainWindow: BrowserWindow, app: Electron.App) {
ipcMain.handle('file:base64Image', fileManager.base64Image) ipcMain.handle('file:base64Image', fileManager.base64Image)
ipcMain.handle('file:download', fileManager.downloadFile) ipcMain.handle('file:download', fileManager.downloadFile)
ipcMain.handle('file:copy', fileManager.copyFile) ipcMain.handle('file:copy', fileManager.copyFile)
ipcMain.handle('file:binaryFile', fileManager.binaryFile)
// fs
ipcMain.handle('fs:read', FileService.readFile)
// minapp // minapp
ipcMain.handle('minapp', (_, args) => { ipcMain.handle('minapp', (_, args) => {
@@ -154,4 +173,30 @@ export function registerIpc(mainWindow: BrowserWindow, app: Electron.App) {
ipcMain.handle('knowledge-base:add', KnowledgeService.add) ipcMain.handle('knowledge-base:add', KnowledgeService.add)
ipcMain.handle('knowledge-base:remove', KnowledgeService.remove) ipcMain.handle('knowledge-base:remove', KnowledgeService.remove)
ipcMain.handle('knowledge-base:search', KnowledgeService.search) ipcMain.handle('knowledge-base:search', KnowledgeService.search)
// window
ipcMain.handle('window:set-minimum-size', (_, width: number, height: number) => {
mainWindow?.setMinimumSize(width, height)
})
ipcMain.handle('window:reset-minimum-size', () => {
mainWindow?.setMinimumSize(1080, 600)
const [width, height] = mainWindow?.getSize() ?? [1080, 600]
if (width < 1080) {
mainWindow?.setSize(1080, height)
}
})
// gemini
ipcMain.handle('gemini:upload-file', GeminiService.uploadFile)
ipcMain.handle('gemini:base64-file', GeminiService.base64File)
ipcMain.handle('gemini:retrieve-file', GeminiService.retrieveFile)
ipcMain.handle('gemini:list-files', GeminiService.listFiles)
ipcMain.handle('gemini:delete-file', GeminiService.deleteFile)
// mini window
ipcMain.handle('miniwindow:show', () => windowService.showMiniWindow())
ipcMain.handle('miniwindow:hide', () => windowService.hideMiniWindow())
ipcMain.handle('miniwindow:close', () => windowService.closeMiniWindow())
ipcMain.handle('miniwindow:toggle', () => windowService.toggleMiniWindow())
} }
+74
View File
@@ -0,0 +1,74 @@
interface CacheItem<T> {
data: T
timestamp: number
duration: number
}
export class CacheService {
private static cache: Map<string, CacheItem<any>> = new Map()
/**
* Set cache
* @param key Cache key
* @param data Cache data
* @param duration Cache duration (in milliseconds)
*/
static set<T>(key: string, data: T, duration: number): void {
this.cache.set(key, {
data,
timestamp: Date.now(),
duration
})
}
/**
* Get cache
* @param key Cache key
* @returns Returns data if cache exists and not expired, otherwise returns null
*/
static get<T>(key: string): T | null {
const item = this.cache.get(key)
if (!item) return null
const now = Date.now()
if (now - item.timestamp > item.duration) {
this.remove(key)
return null
}
return item.data
}
/**
* Remove specific cache
* @param key Cache key
*/
static remove(key: string): void {
this.cache.delete(key)
}
/**
* Clear all cache
*/
static clear(): void {
this.cache.clear()
}
/**
* Check if cache exists and is valid
* @param key Cache key
* @returns boolean
*/
static has(key: string): boolean {
const item = this.cache.get(key)
if (!item) return false
const now = Date.now()
if (now - item.timestamp > item.duration) {
this.remove(key)
return false
}
return true
}
}
+118
View File
@@ -0,0 +1,118 @@
import { debounce, getResourcePath } from '@main/utils'
import { exec } from 'child_process'
import { screen } from 'electron'
import path from 'path'
import { windowService } from './WindowService'
export default class ClipboardMonitor {
private platform: string
private lastText: string
private user32: any
private observer: any
public onTextSelected: (text: string) => void
constructor() {
this.platform = process.platform
this.lastText = ''
this.onTextSelected = debounce((text: string) => this.handleTextSelected(text), 550)
if (this.platform === 'win32') {
this.setupWindows()
} else if (this.platform === 'darwin') {
this.setupMacOS()
}
}
setupMacOS() {
// 使用 Swift 脚本来监听文本选择
const scriptPath = path.join(getResourcePath(), 'textMonitor.swift')
// 启动 Swift 进程来监听文本选择
const process = exec(`swift ${scriptPath}`)
process?.stdout?.on('data', (data: string) => {
console.log('[ClipboardMonitor] MacOS data:', data)
const text = data.toString().trim()
if (text && text !== this.lastText) {
this.lastText = text
this.onTextSelected(text)
}
})
process.on('error', (error) => {
console.error('[ClipboardMonitor] MacOS error:', error)
})
}
setupWindows() {
// 使用 Windows API 监听文本选择事件
const ffi = require('ffi-napi')
const ref = require('ref-napi')
this.user32 = new ffi.Library('user32', {
SetWinEventHook: ['pointer', ['uint32', 'uint32', 'pointer', 'pointer', 'uint32', 'uint32', 'uint32']],
UnhookWinEvent: ['bool', ['pointer']]
})
// 定义事件常量
const EVENT_OBJECT_SELECTION = 0x8006
const WINEVENT_OUTOFCONTEXT = 0x0000
const WINEVENT_SKIPOWNTHREAD = 0x0001
const WINEVENT_SKIPOWNPROCESS = 0x0002
// 创建回调函数
const callback = ffi.Callback('void', ['pointer', 'uint32', 'pointer', 'long', 'long', 'uint32', 'uint32'], () => {
this.getSelectedText()
})
// 设置事件钩子
this.observer = this.user32.SetWinEventHook(
EVENT_OBJECT_SELECTION,
EVENT_OBJECT_SELECTION,
ref.NULL,
callback,
0,
0,
WINEVENT_OUTOFCONTEXT | WINEVENT_SKIPOWNTHREAD | WINEVENT_SKIPOWNPROCESS
)
}
getSelectedText() {
// Get selected text
if (this.platform === 'win32') {
const ref = require('ref-napi')
if (this.user32.OpenClipboard(ref.NULL)) {
// Get clipboard content
const text = this.user32.GetClipboardData(1) // CF_TEXT = 1
this.user32.CloseClipboard()
if (text && text !== this.lastText) {
this.lastText = text
this.onTextSelected(text)
}
}
}
}
private handleTextSelected(text: string) {
if (!text) return
console.debug('[ClipboardMonitor] handleTextSelected', text)
windowService.setLastSelectedText(text)
const mousePosition = screen.getCursorScreenPoint()
windowService.showSelectionMenu({
x: mousePosition.x,
y: mousePosition.y + 10
})
}
dispose() {
if (this.platform === 'win32' && this.observer) {
this.user32.UnhookWinEvent(this.observer)
}
}
}
+25 -1
View File
@@ -30,7 +30,7 @@ export class ConfigManager {
this.store.set('theme', theme) this.store.set('theme', theme)
} }
isTray(): boolean { getTray(): boolean {
return !!this.store.get('tray', true) return !!this.store.get('tray', true)
} }
@@ -83,6 +83,30 @@ export class ConfigManager {
) )
this.notifySubscribers('shortcuts', shortcuts) this.notifySubscribers('shortcuts', shortcuts)
} }
getClickTrayToShowQuickAssistant(): boolean {
return this.store.get('clickTrayToShowQuickAssistant', false) as boolean
}
setClickTrayToShowQuickAssistant(value: boolean) {
this.store.set('clickTrayToShowQuickAssistant', value)
}
getEnableQuickAssistant(): boolean {
return this.store.get('enableQuickAssistant', false) as boolean
}
setEnableQuickAssistant(value: boolean) {
this.store.set('enableQuickAssistant', value)
}
set(key: string, value: any) {
this.store.set(key, value)
}
get(key: string) {
return this.store.get(key)
}
} }
export const configManager = new ConfigManager() export const configManager = new ConfigManager()
+7
View File
@@ -0,0 +1,7 @@
import fs from 'node:fs'
export default class FileService {
public static async readFile(_: Electron.IpcMainInvokeEvent, path: string) {
return fs.readFileSync(path, 'utf8')
}
}
+8 -1
View File
@@ -263,6 +263,13 @@ class FileStorage {
} }
} }
public binaryFile = async (_: Electron.IpcMainInvokeEvent, id: string): Promise<{ data: Buffer; mime: string }> => {
const filePath = path.join(this.storageDir, id)
const data = await fs.promises.readFile(filePath)
const mime = `image/${path.extname(filePath).slice(1)}`
return { data, mime }
}
public clear = async (): Promise<void> => { public clear = async (): Promise<void> => {
await fs.promises.rmdir(this.storageDir, { recursive: true }) await fs.promises.rmdir(this.storageDir, { recursive: true })
await this.initStorageDir() await this.initStorageDir()
@@ -381,7 +388,7 @@ class FileStorage {
} }
// 如果URL中有文件名,使用URL中的文件名 // 如果URL中有文件名,使用URL中的文件名
const urlFilename = url.split('/').pop() const urlFilename = url.split('/').pop()?.split('?')[0]
if (urlFilename && urlFilename.includes('.')) { if (urlFilename && urlFilename.includes('.')) {
filename = urlFilename filename = urlFilename
} }
+63
View File
@@ -0,0 +1,63 @@
import { FileMetadataResponse, FileState, GoogleAIFileManager } from '@google/generative-ai/server'
import { FileType } from '@types'
import fs from 'fs'
import { CacheService } from './CacheService'
export class GeminiService {
private static readonly FILE_LIST_CACHE_KEY = 'gemini_file_list'
private static readonly CACHE_DURATION = 3000
static async uploadFile(_: Electron.IpcMainInvokeEvent, file: FileType, apiKey: string) {
const fileManager = new GoogleAIFileManager(apiKey)
const uploadResult = await fileManager.uploadFile(file.path, {
mimeType: 'application/pdf',
displayName: file.origin_name
})
return uploadResult
}
static async base64File(_: Electron.IpcMainInvokeEvent, file: FileType) {
return {
data: Buffer.from(fs.readFileSync(file.path)).toString('base64'),
mimeType: 'application/pdf'
}
}
static async retrieveFile(
_: Electron.IpcMainInvokeEvent,
file: FileType,
apiKey: string
): Promise<FileMetadataResponse | undefined> {
const fileManager = new GoogleAIFileManager(apiKey)
const cachedResponse = CacheService.get<any>(GeminiService.FILE_LIST_CACHE_KEY)
if (cachedResponse) {
return GeminiService.processResponse(cachedResponse, file)
}
const response = await fileManager.listFiles()
CacheService.set(GeminiService.FILE_LIST_CACHE_KEY, response, GeminiService.CACHE_DURATION)
return GeminiService.processResponse(response, file)
}
private static processResponse(response: any, file: FileType) {
if (response.files) {
return response.files
.filter((file) => file.state === FileState.ACTIVE)
.find((i) => i.displayName === file.origin_name && Number(i.sizeBytes) === file.size)
}
return undefined
}
static async listFiles(_: Electron.IpcMainInvokeEvent, apiKey: string) {
const fileManager = new GoogleAIFileManager(apiKey)
return await fileManager.listFiles()
}
static async deleteFile(_: Electron.IpcMainInvokeEvent, apiKey: string, fileId: string) {
const fileManager = new GoogleAIFileManager(apiKey)
await fileManager.deleteFile(fileId)
}
}
+64 -13
View File
@@ -45,14 +45,14 @@ class KnowledgeService {
azureOpenAIApiDeploymentName: model, azureOpenAIApiDeploymentName: model,
azureOpenAIApiInstanceName: getInstanceName(baseURL), azureOpenAIApiInstanceName: getInstanceName(baseURL),
dimensions, dimensions,
batchSize: 15 batchSize: 10
}) })
: new OpenAiEmbeddings({ : new OpenAiEmbeddings({
model, model,
apiKey, apiKey,
configuration: { baseURL }, configuration: { baseURL },
dimensions, dimensions,
batchSize: 15 batchSize: 10
}) })
) )
.setVectorDatabase(new LibSqlDb({ path: path.join(this.storageDir, id) })) .setVectorDatabase(new LibSqlDb({ path: path.join(this.storageDir, id) }))
@@ -83,52 +83,103 @@ class KnowledgeService {
if (item.type === 'directory') { if (item.type === 'directory') {
const directory = item.content as string const directory = item.content as string
return await ragApplication.addLoader(new LocalPathLoader({ path: directory }), forceReload) return await ragApplication.addLoader(
new LocalPathLoader({ path: directory, chunkSize: base.chunkSize, chunkOverlap: base.chunkOverlap }) as any,
forceReload
)
} }
if (item.type === 'url') { if (item.type === 'url') {
const content = item.content as string const content = item.content as string
if (content.startsWith('http')) { if (content.startsWith('http')) {
return await ragApplication.addLoader(new WebLoader({ urlOrContent: content }), forceReload) return await ragApplication.addLoader(
new WebLoader({ urlOrContent: content, chunkSize: base.chunkSize, chunkOverlap: base.chunkOverlap }) as any,
forceReload
)
} }
} }
if (item.type === 'sitemap') { if (item.type === 'sitemap') {
const content = item.content as string const content = item.content as string
return await ragApplication.addLoader(new SitemapLoader({ url: content }), forceReload) // @ts-ignore loader type
return await ragApplication.addLoader(
new SitemapLoader({ url: content, chunkSize: base.chunkSize, chunkOverlap: base.chunkOverlap }) as any,
forceReload
)
} }
if (item.type === 'note') { if (item.type === 'note') {
const content = item.content as string const content = item.content as string
return await ragApplication.addLoader(new TextLoader({ text: content }), forceReload) return await ragApplication.addLoader(
new TextLoader({ text: content, chunkSize: base.chunkSize, chunkOverlap: base.chunkOverlap }),
forceReload
)
} }
if (item.type === 'file') { if (item.type === 'file') {
const file = item.content as FileType const file = item.content as FileType
if (file.ext === '.pdf') { if (file.ext === '.pdf') {
return await ragApplication.addLoader(new PdfLoader({ filePathOrUrl: file.path }) as any, forceReload) return await ragApplication.addLoader(
new PdfLoader({
filePathOrUrl: file.path,
chunkSize: base.chunkSize,
chunkOverlap: base.chunkOverlap
}) as any,
forceReload
)
} }
if (file.ext === '.docx') { if (file.ext === '.docx') {
return await ragApplication.addLoader(new DocxLoader({ filePathOrUrl: file.path }) as any, forceReload) return await ragApplication.addLoader(
new DocxLoader({
filePathOrUrl: file.path,
chunkSize: base.chunkSize,
chunkOverlap: base.chunkOverlap
}) as any,
forceReload
)
} }
if (file.ext === '.pptx') { if (file.ext === '.pptx') {
return await ragApplication.addLoader(new PptLoader({ filePathOrUrl: file.path }) as any, forceReload) return await ragApplication.addLoader(
new PptLoader({
filePathOrUrl: file.path,
chunkSize: base.chunkSize,
chunkOverlap: base.chunkOverlap
}) as any,
forceReload
)
} }
if (file.ext === '.xlsx') { if (file.ext === '.xlsx') {
return await ragApplication.addLoader(new ExcelLoader({ filePathOrUrl: file.path }) as any, forceReload) return await ragApplication.addLoader(
new ExcelLoader({
filePathOrUrl: file.path,
chunkSize: base.chunkSize,
chunkOverlap: base.chunkOverlap
}) as any,
forceReload
)
} }
if (['.md', '.mdx'].includes(file.ext)) { if (['.md'].includes(file.ext)) {
return await ragApplication.addLoader(new MarkdownLoader({ filePathOrUrl: file.path }) as any, forceReload) return await ragApplication.addLoader(
new MarkdownLoader({
filePathOrUrl: file.path,
chunkSize: base.chunkSize,
chunkOverlap: base.chunkOverlap
}) as any,
forceReload
)
} }
const fileContent = fs.readFileSync(file.path, 'utf-8') const fileContent = fs.readFileSync(file.path, 'utf-8')
return await ragApplication.addLoader(new TextLoader({ text: fileContent }), forceReload) return await ragApplication.addLoader(
new TextLoader({ text: fileContent, chunkSize: base.chunkSize, chunkOverlap: base.chunkOverlap }),
forceReload
)
} }
return { entriesAdded: 0, uniqueId: '', loaderType: '' } return { entriesAdded: 0, uniqueId: '', loaderType: '' }
+17 -1
View File
@@ -3,8 +3,10 @@ import { BrowserWindow, globalShortcut } from 'electron'
import Logger from 'electron-log' import Logger from 'electron-log'
import { configManager } from './ConfigManager' import { configManager } from './ConfigManager'
import { windowService } from './WindowService'
let showAppAccelerator: string | null = null let showAppAccelerator: string | null = null
let showMiniWindowAccelerator: string | null = null
function getShortcutHandler(shortcut: Shortcut) { function getShortcutHandler(shortcut: Shortcut) {
switch (shortcut.key) { switch (shortcut.key) {
@@ -26,6 +28,10 @@ function getShortcutHandler(shortcut: Shortcut) {
window.focus() window.focus()
} }
} }
case 'mini_window':
return () => {
windowService.toggleMiniWindow()
}
default: default:
return null return null
} }
@@ -73,6 +79,10 @@ export function registerShortcuts(window: BrowserWindow) {
showAppAccelerator = accelerator showAppAccelerator = accelerator
} }
if (shortcut.key === 'mini_window') {
showMiniWindowAccelerator = accelerator
}
if (shortcut.key.includes('zoom')) { if (shortcut.key.includes('zoom')) {
switch (shortcut.key) { switch (shortcut.key) {
case 'zoom_in': case 'zoom_in':
@@ -90,7 +100,7 @@ export function registerShortcuts(window: BrowserWindow) {
} }
if (shortcut.enabled) { if (shortcut.enabled) {
globalShortcut.register(accelerator, () => handler(window)) globalShortcut.register(formatShortcutKey(shortcut.shortcut), () => handler(window))
} }
} catch (error) { } catch (error) {
Logger.error(`[ShortcutService] Failed to register shortcut ${shortcut.key}`) Logger.error(`[ShortcutService] Failed to register shortcut ${shortcut.key}`)
@@ -108,6 +118,11 @@ export function registerShortcuts(window: BrowserWindow) {
const handler = getShortcutHandler({ key: 'show_app' } as Shortcut) const handler = getShortcutHandler({ key: 'show_app' } as Shortcut)
handler && globalShortcut.register(showAppAccelerator, () => handler(window)) handler && globalShortcut.register(showAppAccelerator, () => handler(window))
} }
if (showMiniWindowAccelerator) {
const handler = getShortcutHandler({ key: 'mini_window' } as Shortcut)
handler && globalShortcut.register(showMiniWindowAccelerator, () => handler(window))
}
} catch (error) { } catch (error) {
Logger.error('[ShortcutService] Failed to unregister shortcuts') Logger.error('[ShortcutService] Failed to unregister shortcuts')
} }
@@ -124,6 +139,7 @@ export function registerShortcuts(window: BrowserWindow) {
export function unregisterAllShortcuts() { export function unregisterAllShortcuts() {
try { try {
showAppAccelerator = null showAppAccelerator = null
showMiniWindowAccelerator = null
globalShortcut.unregisterAll() globalShortcut.unregisterAll()
} catch (error) { } catch (error) {
Logger.error('[ShortcutService] Failed to unregister all shortcuts') Logger.error('[ShortcutService] Failed to unregister all shortcuts')
+33 -5
View File
@@ -1,6 +1,6 @@
import { isMac } from '@main/constant' import { isMac } from '@main/constant'
import { locales } from '@main/utils/locales' import { locales } from '@main/utils/locales'
import { app, Menu, nativeImage, nativeTheme, Tray } from 'electron' import { app, Menu, MenuItemConstructorOptions, nativeImage, nativeTheme, Tray } from 'electron'
import icon from '../../../build/tray_icon.png?asset' import icon from '../../../build/tray_icon.png?asset'
import iconDark from '../../../build/tray_icon_dark.png?asset' import iconDark from '../../../build/tray_icon_dark.png?asset'
@@ -9,14 +9,22 @@ import { configManager } from './ConfigManager'
import { windowService } from './WindowService' import { windowService } from './WindowService'
export class TrayService { export class TrayService {
private static instance: TrayService
private tray: Tray | null = null private tray: Tray | null = null
constructor() { constructor() {
this.updateTray() this.updateTray()
this.watchTrayChanges() this.watchTrayChanges()
TrayService.instance = this
}
public static getInstance() {
return TrayService.instance
} }
private createTray() { private createTray() {
this.destroyTray()
const iconPath = isMac ? (nativeTheme.shouldUseDarkColors ? iconLight : iconDark) : icon const iconPath = isMac ? (nativeTheme.shouldUseDarkColors ? iconLight : iconDark) : icon
const tray = new Tray(iconPath) const tray = new Tray(iconPath)
@@ -38,17 +46,25 @@ export class TrayService {
const locale = locales[configManager.getLanguage()] const locale = locales[configManager.getLanguage()]
const { tray: trayLocale } = locale.translation const { tray: trayLocale } = locale.translation
const contextMenu = Menu.buildFromTemplate([ const enableQuickAssistant = configManager.getEnableQuickAssistant()
const template = [
{ {
label: trayLocale.show_window, label: trayLocale.show_window,
click: () => windowService.showMainWindow() click: () => windowService.showMainWindow()
}, },
enableQuickAssistant && {
label: trayLocale.show_mini_window,
click: () => windowService.showMiniWindow()
},
{ type: 'separator' }, { type: 'separator' },
{ {
label: trayLocale.quit, label: trayLocale.quit,
click: () => this.quit() click: () => this.quit()
} }
]) ].filter(Boolean) as MenuItemConstructorOptions[]
const contextMenu = Menu.buildFromTemplate(template)
if (process.platform === 'linux') { if (process.platform === 'linux') {
this.tray.setContextMenu(contextMenu) this.tray.setContextMenu(contextMenu)
@@ -61,18 +77,30 @@ export class TrayService {
}) })
this.tray.on('click', () => { this.tray.on('click', () => {
windowService.showMainWindow() if (enableQuickAssistant && configManager.getClickTrayToShowQuickAssistant()) {
windowService.showMiniWindow()
} else {
windowService.showMainWindow()
}
}) })
} }
private updateTray() { private updateTray() {
if (configManager.isTray()) { const showTray = configManager.getTray()
if (showTray) {
this.createTray() this.createTray()
} else { } else {
this.destroyTray() this.destroyTray()
} }
} }
public restartTray() {
if (configManager.getTray()) {
this.destroyTray()
this.createTray()
}
}
private destroyTray() { private destroyTray() {
if (this.tray) { if (this.tray) {
this.tray.destroy() this.tray.destroy()
+189 -9
View File
@@ -1,6 +1,6 @@
import { is } from '@electron-toolkit/utils' import { is } from '@electron-toolkit/utils'
import { isLinux, isWin } from '@main/constant' import { isLinux, isWin } from '@main/constant'
import { app, BrowserWindow, Menu, MenuItem, shell } from 'electron' import { app, BrowserWindow, ipcMain, Menu, MenuItem, shell } from 'electron'
import Logger from 'electron-log' import Logger from 'electron-log'
import windowStateKeeper from 'electron-window-state' import windowStateKeeper from 'electron-window-state'
import path, { join } from 'path' import path, { join } from 'path'
@@ -13,6 +13,10 @@ import { configManager } from './ConfigManager'
export class WindowService { export class WindowService {
private static instance: WindowService | null = null private static instance: WindowService | null = null
private mainWindow: BrowserWindow | null = null private mainWindow: BrowserWindow | null = null
private miniWindow: BrowserWindow | null = null
private wasFullScreen: boolean = false
private selectionMenuWindow: BrowserWindow | null = null
private lastSelectedText: string = ''
public static getInstance(): WindowService { public static getInstance(): WindowService {
if (!WindowService.instance) { if (!WindowService.instance) {
@@ -42,7 +46,7 @@ export class WindowService {
height: mainWindowState.height, height: mainWindowState.height,
minWidth: 1080, minWidth: 1080,
minHeight: 600, minHeight: 600,
show: true, show: false, // 初始不显示
autoHideMenuBar: true, autoHideMenuBar: true,
transparent: isMac, transparent: isMac,
vibrancy: 'under-window', vibrancy: 'under-window',
@@ -61,6 +65,7 @@ export class WindowService {
}) })
this.setupMainWindow(this.mainWindow, mainWindowState) this.setupMainWindow(this.mainWindow, mainWindowState)
return this.mainWindow return this.mainWindow
} }
@@ -118,9 +123,20 @@ export class WindowService {
} }
private setupWindowEvents(mainWindow: BrowserWindow) { private setupWindowEvents(mainWindow: BrowserWindow) {
mainWindow.on('ready-to-show', () => { mainWindow.once('ready-to-show', () => {
mainWindow.show() mainWindow.show()
}) })
// 处理全屏相关事件
mainWindow.on('enter-full-screen', () => {
this.wasFullScreen = true
mainWindow.webContents.send('fullscreen-status-changed', true)
})
mainWindow.on('leave-full-screen', () => {
this.wasFullScreen = false
mainWindow.webContents.send('fullscreen-status-changed', false)
})
} }
private setupWebContentsHandlers(mainWindow: BrowserWindow) { private setupWebContentsHandlers(mainWindow: BrowserWindow) {
@@ -183,18 +199,24 @@ export class WindowService {
private setupWindowLifecycleEvents(mainWindow: BrowserWindow) { private setupWindowLifecycleEvents(mainWindow: BrowserWindow) {
mainWindow.on('close', (event) => { mainWindow.on('close', (event) => {
const notInTray = !configManager.isTray() // 如果已经触发退出,直接退出
if (app.isQuitting) {
return app.quit()
}
// Windows and Linux // 没有开启托盘,且是Windows或Linux系统,直接退出
const notInTray = !configManager.getTray()
if ((isWin || isLinux) && notInTray) { if ((isWin || isLinux) && notInTray) {
return app.quit() return app.quit()
} }
// Mac // 如果是全屏状态,直接退出
if (!app.isQuitting) { if (this.wasFullScreen) {
event.preventDefault() return app.quit()
mainWindow.hide()
} }
event.preventDefault()
mainWindow.hide()
}) })
} }
@@ -209,6 +231,164 @@ export class WindowService {
this.createMainWindow() this.createMainWindow()
} }
} }
public showMiniWindow() {
const enableQuickAssistant = configManager.getEnableQuickAssistant()
if (!enableQuickAssistant) {
return
}
if (this.selectionMenuWindow) {
this.selectionMenuWindow.hide()
}
if (this.miniWindow && !this.miniWindow.isDestroyed()) {
if (this.miniWindow.isMinimized()) {
this.miniWindow.restore()
}
this.miniWindow.show()
this.miniWindow.center()
this.miniWindow.focus()
return
}
const isMac = process.platform === 'darwin'
this.miniWindow = new BrowserWindow({
width: 500,
height: 520,
show: true,
autoHideMenuBar: true,
transparent: isMac,
vibrancy: 'under-window',
visualEffectState: 'followWindow',
center: true,
frame: false,
alwaysOnTop: true,
resizable: false,
useContentSize: true,
webPreferences: {
preload: join(__dirname, '../preload/index.js'),
sandbox: false,
webSecurity: false,
webviewTag: true
}
})
this.miniWindow.on('blur', () => {
this.miniWindow?.hide()
})
this.miniWindow.on('closed', () => {
this.miniWindow = null
})
this.miniWindow.on('hide', () => {
this.miniWindow?.webContents.send('hide-mini-window')
})
this.miniWindow.on('show', () => {
this.miniWindow?.webContents.send('show-mini-window')
})
ipcMain.on('miniwindow-reload', () => {
this.miniWindow?.reload()
})
if (is.dev && process.env['ELECTRON_RENDERER_URL']) {
this.miniWindow.loadURL(process.env['ELECTRON_RENDERER_URL'] + '#/mini')
} else {
this.miniWindow.loadFile(join(__dirname, '../renderer/index.html'), {
hash: '#/mini'
})
}
}
public hideMiniWindow() {
this.miniWindow?.hide()
}
public closeMiniWindow() {
this.miniWindow?.close()
}
public toggleMiniWindow() {
if (this.miniWindow) {
this.miniWindow.isVisible() ? this.miniWindow.hide() : this.miniWindow.show()
} else {
this.showMiniWindow()
}
}
public showSelectionMenu(bounds: { x: number; y: number }) {
if (this.selectionMenuWindow && !this.selectionMenuWindow.isDestroyed()) {
this.selectionMenuWindow.setPosition(bounds.x, bounds.y)
this.selectionMenuWindow.show()
return
}
const theme = configManager.getTheme()
const isMac = process.platform === 'darwin'
this.selectionMenuWindow = new BrowserWindow({
width: 280,
height: 40,
x: bounds.x,
y: bounds.y,
show: true,
autoHideMenuBar: true,
transparent: true,
frame: false,
alwaysOnTop: false,
skipTaskbar: true,
backgroundColor: isMac ? undefined : theme === 'dark' ? '#181818' : '#FFFFFF',
resizable: false,
vibrancy: 'popover',
webPreferences: {
preload: join(__dirname, '../preload/index.js'),
sandbox: false,
webSecurity: false
}
})
// 点击其他地方时隐藏窗口
this.selectionMenuWindow.on('blur', () => {
this.selectionMenuWindow?.hide()
this.miniWindow?.webContents.send('selection-action', {
action: 'home',
selectedText: this.lastSelectedText
})
})
if (is.dev && process.env['ELECTRON_RENDERER_URL']) {
this.selectionMenuWindow.loadURL(process.env['ELECTRON_RENDERER_URL'] + '/src/windows/menu/menu.html')
} else {
this.selectionMenuWindow.loadFile(join(__dirname, '../renderer/src/windows/menu/menu.html'))
}
this.setupSelectionMenuEvents()
}
private setupSelectionMenuEvents() {
if (!this.selectionMenuWindow) return
ipcMain.removeHandler('selection-menu:action')
ipcMain.handle('selection-menu:action', (_, action) => {
this.selectionMenuWindow?.hide()
this.showMiniWindow()
setTimeout(() => {
this.miniWindow?.webContents.send('selection-action', {
action,
selectedText: this.lastSelectedText
})
}, 100)
})
}
public setLastSelectedText(text: string) {
this.lastSelectedText = text
}
} }
export const windowService = WindowService.getInstance() export const windowService = WindowService.getInstance()
+20
View File
@@ -22,3 +22,23 @@ export function getInstanceName(baseURL: string) {
return '' return ''
} }
} }
export function debounce(func: (...args: any[]) => void, wait: number, immediate: boolean = false) {
let timeout: NodeJS.Timeout | null = null
return function (...args: any[]) {
if (timeout) clearTimeout(timeout)
if (immediate) {
func(...args)
} else {
timeout = setTimeout(() => func(...args), wait)
}
}
}
export function dumpPersistState() {
const persistState = JSON.parse(localStorage.getItem('persist:cherry-studio') || '{}')
for (const key in persistState) {
persistState[key] = JSON.parse(persistState[key])
}
return JSON.stringify(persistState)
}
+30
View File
@@ -1,4 +1,5 @@
import { ElectronAPI } from '@electron-toolkit/preload' import { ElectronAPI } from '@electron-toolkit/preload'
import type { FileMetadataResponse, ListFilesResponse, UploadFileResponse } from '@google/generative-ai/server'
import { AddLoaderReturn, ExtractChunkData } from '@llm-tools/embedjs-interfaces' import { AddLoaderReturn, ExtractChunkData } from '@llm-tools/embedjs-interfaces'
import { FileType } from '@renderer/types' import { FileType } from '@renderer/types'
import { WebDavConfig } from '@renderer/types' import { WebDavConfig } from '@renderer/types'
@@ -17,6 +18,7 @@ declare global {
setProxy: (proxy: string | undefined) => void setProxy: (proxy: string | undefined) => void
setLanguage: (theme: LanguageVarious) => void setLanguage: (theme: LanguageVarious) => void
setTray: (isActive: boolean) => void setTray: (isActive: boolean) => void
restartTray: () => void
setTheme: (theme: 'light' | 'dark') => void setTheme: (theme: 'light' | 'dark') => void
minApp: (options: { url: string; windowOptions?: Electron.BrowserWindowConstructorOptions }) => void minApp: (options: { url: string; windowOptions?: Electron.BrowserWindowConstructorOptions }) => void
reload: () => void reload: () => void
@@ -52,6 +54,10 @@ declare global {
base64Image: (fileId: string) => Promise<{ mime: string; base64: string; data: string }> base64Image: (fileId: string) => Promise<{ mime: string; base64: string; data: string }>
download: (url: string) => Promise<FileType | null> download: (url: string) => Promise<FileType | null>
copy: (fileId: string, destPath: string) => Promise<void> copy: (fileId: string, destPath: string) => Promise<void>
binaryFile: (fileId: string) => Promise<{ data: Buffer; mime: string }>
}
fs: {
read: (path: string) => Promise<string>
} }
export: { export: {
toWord: (markdown: string, fileName: string) => Promise<void> toWord: (markdown: string, fileName: string) => Promise<void>
@@ -76,6 +82,30 @@ declare global {
remove: ({ uniqueId, base }: { uniqueId: string; base: KnowledgeBaseParams }) => Promise<void> remove: ({ uniqueId, base }: { uniqueId: string; base: KnowledgeBaseParams }) => Promise<void>
search: ({ search, base }: { search: string; base: KnowledgeBaseParams }) => Promise<ExtractChunkData[]> search: ({ search, base }: { search: string; base: KnowledgeBaseParams }) => Promise<ExtractChunkData[]>
} }
window: {
setMinimumSize: (width: number, height: number) => Promise<void>
resetMinimumSize: () => Promise<void>
}
gemini: {
uploadFile: (file: FileType, apiKey: string) => Promise<UploadFileResponse>
retrieveFile: (file: FileType, apiKey: string) => Promise<FileMetadataResponse | undefined>
base64File: (file: FileType) => Promise<{ data: string; mimeType: string }>
listFiles: (apiKey: string) => Promise<ListFilesResponse>
deleteFile: (apiKey: string, fileId: string) => Promise<void>
}
selectionMenu: {
action: (action: string) => Promise<void>
}
config: {
set: (key: string, value: any) => Promise<void>
get: (key: string) => Promise<any>
}
miniWindow: {
show: () => Promise<void>
hide: () => Promise<void>
close: () => Promise<void>
toggle: () => Promise<void>
}
} }
} }
} }
+31 -2
View File
@@ -1,5 +1,5 @@
import { electronAPI } from '@electron-toolkit/preload' import { electronAPI } from '@electron-toolkit/preload'
import { KnowledgeBaseParams, KnowledgeItem, Shortcut, WebDavConfig } from '@types' import { FileType, KnowledgeBaseParams, KnowledgeItem, Shortcut, WebDavConfig } from '@types'
import { contextBridge, ipcRenderer, OpenDialogOptions } from 'electron' import { contextBridge, ipcRenderer, OpenDialogOptions } from 'electron'
// Custom APIs for renderer // Custom APIs for renderer
@@ -10,6 +10,7 @@ const api = {
checkForUpdate: () => ipcRenderer.invoke('app:check-for-update'), checkForUpdate: () => ipcRenderer.invoke('app:check-for-update'),
setLanguage: (lang: string) => ipcRenderer.invoke('app:set-language', lang), setLanguage: (lang: string) => ipcRenderer.invoke('app:set-language', lang),
setTray: (isActive: boolean) => ipcRenderer.invoke('app:set-tray', isActive), setTray: (isActive: boolean) => ipcRenderer.invoke('app:set-tray', isActive),
restartTray: () => ipcRenderer.invoke('app:restart-tray'),
setTheme: (theme: 'light' | 'dark') => ipcRenderer.invoke('app:set-theme', theme), setTheme: (theme: 'light' | 'dark') => ipcRenderer.invoke('app:set-theme', theme),
openWebsite: (url: string) => ipcRenderer.invoke('open:website', url), openWebsite: (url: string) => ipcRenderer.invoke('open:website', url),
minApp: (url: string) => ipcRenderer.invoke('minapp', url), minApp: (url: string) => ipcRenderer.invoke('minapp', url),
@@ -43,7 +44,11 @@ const api = {
saveImage: (name: string, data: string) => ipcRenderer.invoke('file:saveImage', name, data), saveImage: (name: string, data: string) => ipcRenderer.invoke('file:saveImage', name, data),
base64Image: (fileId: string) => ipcRenderer.invoke('file:base64Image', fileId), base64Image: (fileId: string) => ipcRenderer.invoke('file:base64Image', fileId),
download: (url: string) => ipcRenderer.invoke('file:download', url), download: (url: string) => ipcRenderer.invoke('file:download', url),
copy: (fileId: string, destPath: string) => ipcRenderer.invoke('file:copy', fileId, destPath) copy: (fileId: string, destPath: string) => ipcRenderer.invoke('file:copy', fileId, destPath),
binaryFile: (fileId: string) => ipcRenderer.invoke('file:binaryFile', fileId)
},
fs: {
read: (path: string) => ipcRenderer.invoke('fs:read', path)
}, },
export: { export: {
toWord: (markdown: string, fileName: string) => ipcRenderer.invoke('export:word', markdown, fileName) toWord: (markdown: string, fileName: string) => ipcRenderer.invoke('export:word', markdown, fileName)
@@ -70,6 +75,30 @@ const api = {
ipcRenderer.invoke('knowledge-base:remove', { uniqueId, base }), ipcRenderer.invoke('knowledge-base:remove', { uniqueId, base }),
search: ({ search, base }: { search: string; base: KnowledgeBaseParams }) => search: ({ search, base }: { search: string; base: KnowledgeBaseParams }) =>
ipcRenderer.invoke('knowledge-base:search', { search, base }) ipcRenderer.invoke('knowledge-base:search', { search, base })
},
window: {
setMinimumSize: (width: number, height: number) => ipcRenderer.invoke('window:set-minimum-size', width, height),
resetMinimumSize: () => ipcRenderer.invoke('window:reset-minimum-size')
},
gemini: {
uploadFile: (file: FileType, apiKey: string) => ipcRenderer.invoke('gemini:upload-file', file, apiKey),
base64File: (file: FileType) => ipcRenderer.invoke('gemini:base64-file', file),
retrieveFile: (file: FileType, apiKey: string) => ipcRenderer.invoke('gemini:retrieve-file', file, apiKey),
listFiles: (apiKey: string) => ipcRenderer.invoke('gemini:list-files', apiKey),
deleteFile: (apiKey: string, fileId: string) => ipcRenderer.invoke('gemini:delete-file', apiKey, fileId)
},
selectionMenu: {
action: (action: string) => ipcRenderer.invoke('selection-menu:action', action)
},
config: {
set: (key: string, value: any) => ipcRenderer.invoke('config:set', key, value),
get: (key: string) => ipcRenderer.invoke('config:get', key)
},
miniWindow: {
show: () => ipcRenderer.invoke('miniwindow:show'),
hide: () => ipcRenderer.invoke('miniwindow:hide'),
close: () => ipcRenderer.invoke('miniwindow:close'),
toggle: () => ipcRenderer.invoke('miniwindow:toggle')
} }
} }
+2 -1
View File
@@ -17,10 +17,10 @@
position: fixed; position: fixed;
width: 100vw; width: 100vw;
height: 100vh; height: 100vh;
display: flex;
flex-direction: row; flex-direction: row;
justify-content: center; justify-content: center;
align-items: center; align-items: center;
display: none;
} }
#spinner img { #spinner img {
@@ -35,6 +35,7 @@
<div id="spinner"> <div id="spinner">
<img src="/src/assets/images/logo.png" /> <img src="/src/assets/images/logo.png" />
</div> </div>
<script type="module" src="/src/init.ts"></script>
<script type="module" src="/src/main.tsx"></script> <script type="module" src="/src/main.tsx"></script>
</body> </body>
-2
View File
@@ -16,7 +16,6 @@ import FilesPage from './pages/files/FilesPage'
import HomePage from './pages/home/HomePage' import HomePage from './pages/home/HomePage'
import KnowledgePage from './pages/knowledge/KnowledgePage' import KnowledgePage from './pages/knowledge/KnowledgePage'
import PaintingsPage from './pages/paintings/PaintingsPage' import PaintingsPage from './pages/paintings/PaintingsPage'
import SettingsPage from './pages/settings/SettingsPage'
import TranslatePage from './pages/translate/TranslatePage' import TranslatePage from './pages/translate/TranslatePage'
function App(): JSX.Element { function App(): JSX.Element {
@@ -37,7 +36,6 @@ function App(): JSX.Element {
<Route path="/files" element={<FilesPage />} /> <Route path="/files" element={<FilesPage />} />
<Route path="/knowledge" element={<KnowledgePage />} /> <Route path="/knowledge" element={<KnowledgePage />} />
<Route path="/apps" element={<AppsPage />} /> <Route path="/apps" element={<AppsPage />} />
<Route path="/settings/*" element={<SettingsPage />} />
</Routes> </Routes>
</HashRouter> </HashRouter>
</TopViewContainer> </TopViewContainer>
@@ -1,88 +1,91 @@
@font-face { @font-face {
font-family: 'iconfont'; /* Project id 4753420 */ font-family: "iconfont"; /* Project id 4753420 */
src: url('iconfont.woff2?t=1733224456443') format('woff2'); src: url('iconfont.woff2?t=1736309723926') format('woff2'),
url('iconfont.woff?t=1736309723926') format('woff'),
url('iconfont.ttf?t=1736309723926') format('truetype');
} }
.iconfont { .iconfont {
font-family: 'iconfont' !important; font-family: "iconfont" !important;
font-size: 16px; font-size: 16px;
font-style: normal; font-style: normal;
-webkit-font-smoothing: antialiased; -webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale; -moz-osx-font-smoothing: grayscale;
} }
.icon-at1:before { .icon-at:before {
content: '\e7df'; content: "\e623";
} }
.icon-at:before { .icon-icon-adaptive-width:before {
content: '\e630'; content: "\e87a";
} }
.icon-a-darkmode:before { .icon-a-darkmode:before {
content: '\e6cd'; content: "\e6cd";
} }
.icon-ai-model:before { .icon-ai-model:before {
content: '\e827'; content: "\e827";
} }
.icon-ai-model1:before { .icon-ai-model1:before {
content: '\ec09'; content: "\ec09";
} }
.icon-gridlines:before { .icon-gridlines:before {
content: '\e942'; content: "\e942";
} }
.icon-inbox:before { .icon-inbox:before {
content: '\e869'; content: "\e869";
} }
.icon-business-smart-assistant:before { .icon-business-smart-assistant:before {
content: '\e601'; content: "\e601";
} }
.icon-copy:before { .icon-copy:before {
content: '\e6ae'; content: "\e6ae";
} }
.icon-ic_send:before { .icon-ic_send:before {
content: '\e795'; content: "\e795";
} }
.icon-dark1:before { .icon-dark1:before {
content: '\e72f'; content: "\e72f";
} }
.icon-theme-light:before { .icon-theme-light:before {
content: '\e6b7'; content: "\e6b7";
} }
.icon-translate_line:before { .icon-translate_line:before {
content: '\e7de'; content: "\e7de";
} }
.icon-history:before { .icon-history:before {
content: '\e758'; content: "\e758";
} }
.icon-hide-sidebar:before { .icon-hide-sidebar:before {
content: '\e8eb'; content: "\e8eb";
} }
.icon-show-sidebar:before { .icon-show-sidebar:before {
content: '\e944'; content: "\e944";
} }
.icon-appstore:before { .icon-appstore:before {
content: '\e792'; content: "\e792";
} }
.icon-chat:before { .icon-chat:before {
content: '\e615'; content: "\e615";
} }
.icon-setting:before { .icon-setting:before {
content: '\e78e'; content: "\e78e";
} }
@@ -0,0 +1,4 @@
<svg width="464" height="464" viewBox="0 0 464 464" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect width="464" height="464" fill="white"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M243 127C235.268 127 229 133.268 229 141V322C229 329.732 235.268 336 243 336H283C290.732 336 297 329.732 297 322V141C297 133.268 290.732 127 283 127H243ZM167.562 128C163.762 128 160.317 129.518 157.805 131.978C157.787 131.995 157.759 131.977 157.767 131.954C157.775 131.93 157.743 131.913 157.727 131.933L157.311 132.486C156.679 133.171 156.115 133.92 155.629 134.722C154.303 136.486 153.139 138.365 152.152 140.338L88.8745 266.857L85.2894 274.899C85.2249 275.037 85.1626 275.177 85.1027 275.318L84.7141 276.189C84.7086 276.201 84.7223 276.213 84.7339 276.206C84.745 276.2 84.7583 276.211 84.7541 276.223C84.2654 277.639 84 279.16 84 280.742L84 322.399C84 330.067 90.2354 336.284 97.9271 336.284H139.708C147.4 336.284 153.635 330.067 153.635 322.399V266.857L153.636 252.97C153.636 222.295 178.577 197.428 209.344 197.428C217.035 197.428 223.271 191.211 223.271 183.542V141.886C223.271 134.217 217.035 128 209.344 128H167.562ZM304.5 301.57C304.5 282.398 320.088 266.856 339.318 266.856C358.547 266.856 374.135 282.398 374.135 301.57C374.135 320.742 358.547 336.284 339.318 336.284C320.088 336.284 304.5 320.742 304.5 301.57Z" fill="black"/>
</svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 44 KiB

After

Width:  |  Height:  |  Size: 84 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 81 KiB

+23 -9
View File
@@ -24,6 +24,7 @@
--color-background: var(--color-black); --color-background: var(--color-black);
--color-background-soft: var(--color-black-soft); --color-background-soft: var(--color-black-soft);
--color-background-mute: var(--color-black-mute); --color-background-mute: var(--color-black-mute);
--color-background-opacity: rgba(34, 34, 34, 0.7);
--color-primary: #00b96b; --color-primary: #00b96b;
--color-primary-soft: #00b96b99; --color-primary-soft: #00b96b99;
@@ -42,6 +43,9 @@
--color-active: rgba(55, 55, 55, 1); --color-active: rgba(55, 55, 55, 1);
--color-frame-border: #333; --color-frame-border: #333;
--color-group-background: var(--color-background-soft); --color-group-background: var(--color-background-soft);
--color-reference: #404040;
--color-reference-text: #ffffff;
--color-reference-background: #0b0e12; --color-reference-background: #0b0e12;
--navbar-background-mac: rgba(30, 30, 30, 0.6); --navbar-background-mac: rgba(30, 30, 30, 0.6);
@@ -60,6 +64,8 @@
--chat-background-user: #28b561; --chat-background-user: #28b561;
--chat-background-assistant: #2c2c2c; --chat-background-assistant: #2c2c2c;
--chat-text-user: var(--color-black); --chat-text-user: var(--color-black);
--list-item-border-radius: 16px;
} }
body[theme-mode='light'] { body[theme-mode='light'] {
@@ -82,6 +88,7 @@ body[theme-mode='light'] {
--color-background: var(--color-white); --color-background: var(--color-white);
--color-background-soft: var(--color-white-soft); --color-background-soft: var(--color-white-soft);
--color-background-mute: var(--color-white-mute); --color-background-mute: var(--color-white-mute);
--color-background-opacity: rgba(255, 255, 255, 0.7);
--color-primary: #00b96b; --color-primary: #00b96b;
--color-primary-soft: #00b96b99; --color-primary-soft: #00b96b99;
@@ -100,6 +107,9 @@ body[theme-mode='light'] {
--color-active: var(--color-white-soft); --color-active: var(--color-white-soft);
--color-frame-border: #ddd; --color-frame-border: #ddd;
--color-group-background: var(--color-white); --color-group-background: var(--color-white);
--color-reference: #cfe1ff;
--color-reference-text: #000000;
--color-reference-background: #f1f7ff; --color-reference-background: #f1f7ff;
--navbar-background-mac: rgba(255, 255, 255, 0.6); --navbar-background-mac: rgba(255, 255, 255, 0.6);
@@ -169,12 +179,9 @@ body,
#content-container { #content-container {
background-color: var(--color-background); background-color: var(--color-background);
border-top: 0.5px solid var(--color-border); border-top: 0.5px solid var(--color-border);
} border-top-left-radius: 10px;
#content-container {
border-top-left-radius: 12px;
border-left: 0.5px solid var(--color-border); border-left: 0.5px solid var(--color-border);
box-shadow: -2px 0px 20px -4px rgba(0, 0, 0, 0.08); box-shadow: -2px 0px 20px -4px rgba(0, 0, 0, 0.06);
} }
.loader { .loader {
@@ -216,10 +223,7 @@ body,
background-color: var(--chat-background); background-color: var(--chat-background);
} }
#inputbar { #inputbar {
border-radius: 0; margin: -5px 15px 15px 15px;
margin: 0;
border: none;
border-top: 1px solid var(--color-border-mute);
background: var(--color-background); background: var(--color-background);
} }
.system-prompt { .system-prompt {
@@ -230,6 +234,9 @@ body,
border-radius: 8px; border-radius: 8px;
padding: 10px 15px 0 15px; padding: 10px 15px 0 15px;
} }
.message-thought-container {
margin-top: 8px;
}
.message-user { .message-user {
color: var(--chat-text-user); color: var(--chat-text-user);
.markdown, .markdown,
@@ -242,6 +249,13 @@ body,
background-color: var(--color-white-soft); background-color: var(--color-white-soft);
} }
} }
.group-message-wrapper {
background-color: var(--color-background);
.message-content-container {
width: 100%;
border: 1px solid var(--color-background-mute);
}
}
code { code {
color: var(--color-text); color: var(--color-text);
} }
+48 -36
View File
@@ -208,6 +208,14 @@
sup { sup {
top: -0.5em; top: -0.5em;
border-radius: 50%;
background-color: var(--color-reference);
color: var(--color-reference-text);
padding: 2px 5px;
zoom: 0.8;
& > span.link {
color: var(--color-reference-text);
}
} }
sub { sub {
@@ -226,51 +234,55 @@
text-decoration: underline; text-decoration: underline;
} }
} }
}
.footnotes { .footnotes {
margin-top: 1em; margin-top: 1em;
margin-bottom: 1em; margin-bottom: 1em;
padding-top: 1em; padding-top: 1em;
background-color: var(--color-reference-background); background-color: var(--color-reference-background);
border-radius: 8px; border-radius: 8px;
padding: 8px 12px; padding: 8px 12px;
h4 { h4 {
margin-bottom: 5px; margin-bottom: 5px;
font-size: 12px; font-size: 12px;
}
a {
color: var(--color-link);
}
ol {
padding-left: 1em;
margin: 0;
li:last-child {
margin-bottom: 0;
} }
}
ol { li {
padding-left: 1em; font-size: 0.9em;
margin-bottom: 0.5em;
color: var(--color-text-light);
p {
display: inline;
margin: 0; margin: 0;
li:last-child {
margin-bottom: 0;
}
} }
}
li { .footnote-backref {
font-size: 0.9em; font-size: 0.8em;
margin-bottom: 0.5em; vertical-align: super;
color: var(--color-text-light); line-height: 0;
margin-left: 5px;
color: var(--color-primary);
text-decoration: none;
p { &:hover {
display: inline; text-decoration: underline;
margin: 0;
}
}
.footnote-backref {
font-size: 0.8em;
vertical-align: super;
line-height: 0;
margin-left: 5px;
color: var(--color-primary);
text-decoration: none;
&:hover {
text-decoration: underline;
}
} }
} }
} }
@@ -47,19 +47,22 @@ const DragableList: FC<Props<any>> = ({
<Droppable droppableId="droppable" {...droppableProps}> <Droppable droppableId="droppable" {...droppableProps}>
{(provided) => ( {(provided) => (
<div {...provided.droppableProps} ref={provided.innerRef} style={{ ...style }}> <div {...provided.droppableProps} ref={provided.innerRef} style={{ ...style }}>
{list.map((item, index) => ( {list.map((item, index) => {
<Draggable key={`draggable_${item.id}_${index}`} draggableId={item.id} index={index} {...droppableProps}> const id = item.id || item
{(provided) => ( return (
<div <Draggable key={`draggable_${id}_${index}`} draggableId={id} index={index} {...droppableProps}>
ref={provided.innerRef} {(provided) => (
{...provided.draggableProps} <div
{...provided.dragHandleProps} ref={provided.innerRef}
style={{ ...provided.draggableProps.style, marginBottom: 8, ...listStyle }}> {...provided.draggableProps}
{children(item, index)} {...provided.dragHandleProps}
</div> style={{ ...provided.draggableProps.style, marginBottom: 8, ...listStyle }}>
)} {children(item, index)}
</Draggable> </div>
))} )}
</Draggable>
)
})}
</div> </div>
)} )}
</Droppable> </Droppable>
@@ -0,0 +1,39 @@
import { DEFAULT_MIN_APPS } from '@renderer/config/minapps'
import { MinAppType } from '@renderer/types'
import { FC } from 'react'
import styled from 'styled-components'
interface Props {
app: MinAppType
size?: number
style?: React.CSSProperties
}
const MinAppIcon: FC<Props> = ({ app, size = 48, style }) => {
const _app = DEFAULT_MIN_APPS.find((item) => item.id === app.id)
if (!_app) {
return null
}
return (
<Container
src={_app.logo}
style={{
border: _app.bodered ? '0.5px solid var(--color-border)' : 'none',
width: `${size}px`,
height: `${size}px`,
backgroundColor: _app.background,
...style
}}
/>
)
}
const Container = styled.img`
border-radius: 16px;
user-select: none;
-webkit-user-drag: none;
`
export default MinAppIcon
@@ -10,9 +10,8 @@ interface ListItemProps {
} }
const ListItem = ({ active, icon, title, subtitle, onClick }: ListItemProps) => { const ListItem = ({ active, icon, title, subtitle, onClick }: ListItemProps) => {
const borderRadius = subtitle ? '10px' : '16px'
return ( return (
<ListItemContainer className={active ? 'active' : ''} onClick={onClick} style={{ borderRadius }}> <ListItemContainer className={active ? 'active' : ''} onClick={onClick}>
<ListItemContent> <ListItemContent>
{icon && <IconWrapper>{icon}</IconWrapper>} {icon && <IconWrapper>{icon}</IconWrapper>}
<TextContainer> <TextContainer>
@@ -26,7 +25,7 @@ const ListItem = ({ active, icon, title, subtitle, onClick }: ListItemProps) =>
const ListItemContainer = styled.div` const ListItemContainer = styled.div`
padding: 7px 12px; padding: 7px 12px;
border-radius: 16px; border-radius: var(--list-item-border-radius);
font-size: 13px; font-size: 13px;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
+48 -11
View File
@@ -1,10 +1,13 @@
/* eslint-disable react/no-unknown-property */ /* eslint-disable react/no-unknown-property */
import { CloseOutlined, ExportOutlined, ReloadOutlined } from '@ant-design/icons' import { CloseOutlined, ExportOutlined, PushpinOutlined, ReloadOutlined } from '@ant-design/icons'
import { isMac, isWindows } from '@renderer/config/constant' import { isMac, isWindows } from '@renderer/config/constant'
import { DEFAULT_MIN_APPS } from '@renderer/config/minapps'
import { useBridge } from '@renderer/hooks/useBridge' import { useBridge } from '@renderer/hooks/useBridge'
import { useMinapps } from '@renderer/hooks/useMinapps'
import store from '@renderer/store' import store from '@renderer/store'
import { setMinappShow } from '@renderer/store/runtime' import { setMinappShow } from '@renderer/store/runtime'
import { MinAppType } from '@renderer/types' import { MinAppType } from '@renderer/types'
import { delay } from '@renderer/utils'
import { Avatar, Drawer } from 'antd' import { Avatar, Drawer } from 'antd'
import { WebviewTag } from 'electron' import { WebviewTag } from 'electron'
import { useEffect, useRef, useState } from 'react' import { useEffect, useRef, useState } from 'react'
@@ -19,6 +22,8 @@ interface Props {
} }
const PopupContainer: React.FC<Props> = ({ app, resolve }) => { const PopupContainer: React.FC<Props> = ({ app, resolve }) => {
const { pinned, updatePinnedMinapps } = useMinapps()
const isPinned = pinned.some((p) => p.id === app.id)
const [open, setOpen] = useState(true) const [open, setOpen] = useState(true)
const [opened, setOpened] = useState(false) const [opened, setOpened] = useState(false)
const [isReady, setIsReady] = useState(false) const [isReady, setIsReady] = useState(false)
@@ -27,10 +32,12 @@ const PopupContainer: React.FC<Props> = ({ app, resolve }) => {
useBridge() useBridge()
const canOpenExternalLink = app.url.startsWith('http://') || app.url.startsWith('https://') const canOpenExternalLink = app.url.startsWith('http://') || app.url.startsWith('https://')
const canPinned = DEFAULT_MIN_APPS.some((i) => i.id === app?.id)
const onClose = () => { const onClose = async (_delay = 0.3) => {
setOpen(false) setOpen(false)
setTimeout(() => resolve({}), 300) await delay(_delay)
resolve({})
} }
MinApp.onClose = onClose MinApp.onClose = onClose
@@ -45,6 +52,11 @@ const PopupContainer: React.FC<Props> = ({ app, resolve }) => {
window.api.openWebsite(app.url) window.api.openWebsite(app.url)
} }
const onTogglePin = () => {
const newPinned = isPinned ? pinned.filter((item) => item.id !== app.id) : [...pinned, app]
updatePinnedMinapps(newPinned)
}
const Title = () => { const Title = () => {
return ( return (
<TitleContainer style={{ justifyContent: 'space-between' }}> <TitleContainer style={{ justifyContent: 'space-between' }}>
@@ -53,12 +65,17 @@ const PopupContainer: React.FC<Props> = ({ app, resolve }) => {
<Button onClick={onReload}> <Button onClick={onReload}>
<ReloadOutlined /> <ReloadOutlined />
</Button> </Button>
{canPinned && (
<Button onClick={onTogglePin} className={isPinned ? 'pinned' : ''}>
<PushpinOutlined style={{ fontSize: 16 }} />
</Button>
)}
{canOpenExternalLink && ( {canOpenExternalLink && (
<Button onClick={onOpenLink}> <Button onClick={onOpenLink}>
<ExportOutlined /> <ExportOutlined />
</Button> </Button>
)} )}
<Button onClick={onClose}> <Button onClick={() => onClose()}>
<CloseOutlined /> <CloseOutlined />
</Button> </Button>
</ButtonsGroup> </ButtonsGroup>
@@ -99,7 +116,7 @@ const PopupContainer: React.FC<Props> = ({ app, resolve }) => {
<Drawer <Drawer
title={<Title />} title={<Title />}
placement="bottom" placement="bottom"
onClose={onClose} onClose={() => onClose()}
open={open} open={open}
mask={true} mask={true}
rootClassName="minapp-drawer" rootClassName="minapp-drawer"
@@ -138,7 +155,7 @@ const TitleContainer = styled.div`
display: flex; display: flex;
flex-direction: row; flex-direction: row;
align-items: center; align-items: center;
padding-left: ${isMac ? '20px' : '15px'}; padding-left: ${isMac ? '20px' : '10px'};
padding-right: 10px; padding-right: 10px;
position: absolute; position: absolute;
top: 0; top: 0;
@@ -186,6 +203,10 @@ const Button = styled.div`
color: var(--color-text-1); color: var(--color-text-1);
background-color: var(--color-background-mute); background-color: var(--color-background-mute);
} }
&.pinned {
color: var(--color-primary);
background-color: var(--color-primary-bg);
}
` `
const EmptyView = styled.div` const EmptyView = styled.div`
@@ -202,12 +223,22 @@ const EmptyView = styled.div`
export default class MinApp { export default class MinApp {
static topviewId = 0 static topviewId = 0
static onClose = () => {} static onClose = () => {}
static close() { static app: MinAppType | null = null
TopView.hide('MinApp')
store.dispatch(setMinappShow(false)) static async start(app: MinAppType) {
} if (app?.id && MinApp.app?.id === app?.id) {
static start(app: MinAppType) { return
}
if (MinApp.app) {
// @ts-ignore delay params
await MinApp.onClose(0)
await delay(0)
}
MinApp.app = app
store.dispatch(setMinappShow(true)) store.dispatch(setMinappShow(true))
return new Promise<any>((resolve) => { return new Promise<any>((resolve) => {
TopView.show( TopView.show(
<PopupContainer <PopupContainer
@@ -221,4 +252,10 @@ export default class MinApp {
) )
}) })
} }
static close() {
TopView.hide('MinApp')
store.dispatch(setMinappShow(false))
MinApp.app = null
}
} }
+36
View File
@@ -0,0 +1,36 @@
import { isEmbeddingModel, isVisionModel, isWebSearchModel } from '@renderer/config/models'
import { Model } from '@renderer/types'
import { isFreeModel } from '@renderer/utils'
import { Tag } from 'antd'
import { FC } from 'react'
import { useTranslation } from 'react-i18next'
import VisionIcon from './Icons/VisionIcon'
import WebSearchIcon from './Icons/WebSearchIcon'
interface ModelTagsProps {
model: Model
showFree?: boolean
}
const ModelTags: FC<ModelTagsProps> = ({ model, showFree = true }) => {
const { t } = useTranslation()
return (
<>
{isVisionModel(model) && <VisionIcon />}
{isWebSearchModel(model) && <WebSearchIcon />}
{showFree && isFreeModel(model) && (
<Tag style={{ marginLeft: 10 }} color="green">
{t('models.free')}
</Tag>
)}
{isEmbeddingModel(model) && (
<Tag style={{ marginLeft: 10 }} color="orange">
{t('models.embedding')}
</Tag>
)}
</>
)
}
export default ModelTags
@@ -1,8 +1,8 @@
import { SearchOutlined } from '@ant-design/icons' import { SearchOutlined } from '@ant-design/icons'
import { TopView } from '@renderer/components/TopView' import { TopView } from '@renderer/components/TopView'
import systemAgents from '@renderer/config/agents.json'
import { useAgents } from '@renderer/hooks/useAgents' import { useAgents } from '@renderer/hooks/useAgents'
import { useAssistants, useDefaultAssistant } from '@renderer/hooks/useAssistant' import { useAssistants, useDefaultAssistant } from '@renderer/hooks/useAssistant'
import { useSystemAgents } from '@renderer/pages/agents'
import { createAssistantFromAgent } from '@renderer/services/AssistantService' import { createAssistantFromAgent } from '@renderer/services/AssistantService'
import { EVENT_NAMES, EventEmitter } from '@renderer/services/EventService' import { EVENT_NAMES, EventEmitter } from '@renderer/services/EventService'
import { Agent, Assistant } from '@renderer/types' import { Agent, Assistant } from '@renderer/types'
@@ -28,6 +28,7 @@ const PopupContainer: React.FC<Props> = ({ resolve }) => {
const { defaultAssistant } = useDefaultAssistant() const { defaultAssistant } = useDefaultAssistant()
const { assistants, addAssistant } = useAssistants() const { assistants, addAssistant } = useAssistants()
const inputRef = useRef<InputRef>(null) const inputRef = useRef<InputRef>(null)
const systemAgents = useSystemAgents()
const agents = useMemo(() => { const agents = useMemo(() => {
const allAgents = [...userAgents, ...systemAgents] as Agent[] const allAgents = [...userAgents, ...systemAgents] as Agent[]
@@ -48,7 +49,7 @@ const PopupContainer: React.FC<Props> = ({ resolve }) => {
return [newAgent, ...filtered] return [newAgent, ...filtered]
} }
return filtered return filtered
}, [assistants, defaultAssistant, searchText, userAgents]) }, [assistants, defaultAssistant, searchText, systemAgents, userAgents])
const onCreateAssistant = async (agent: Agent) => { const onCreateAssistant = async (agent: Agent) => {
let assistant: Assistant let assistant: Assistant
@@ -120,7 +121,11 @@ const PopupContainer: React.FC<Props> = ({ resolve }) => {
key={agent.id} key={agent.id}
onClick={() => onCreateAssistant(agent)} onClick={() => onCreateAssistant(agent)}
className={agent.id === 'default' ? 'default' : ''}> className={agent.id === 'default' ? 'default' : ''}>
<HStack alignItems="center" gap={5}> <HStack
alignItems="center"
gap={5}
style={{ overflow: 'hidden', maxWidth: '100%' }}
className="text-nowrap">
{agent.emoji} {agent.name} {agent.emoji} {agent.name}
</HStack> </HStack>
{agent.id === 'default' && <Tag color="green">{t('agents.tag.system')}</Tag>} {agent.id === 'default' && <Tag color="green">{t('agents.tag.system')}</Tag>}
@@ -149,6 +154,7 @@ const AgentItem = styled.div`
user-select: none; user-select: none;
margin-bottom: 8px; margin-bottom: 8px;
cursor: pointer; cursor: pointer;
overflow: hidden;
&.default { &.default {
background-color: var(--color-background-mute); background-color: var(--color-background-mute);
} }
@@ -1,5 +1,5 @@
import { Center } from '@renderer/components/Layout' import { Center } from '@renderer/components/Layout'
import { getAllMinApps } from '@renderer/config/minapps' import { useMinapps } from '@renderer/hooks/useMinapps'
import App from '@renderer/pages/apps/App' import App from '@renderer/pages/apps/App'
import { Popover } from 'antd' import { Popover } from 'antd'
import { Empty } from 'antd' import { Empty } from 'antd'
@@ -14,9 +14,9 @@ interface Props {
children: React.ReactNode children: React.ReactNode
} }
const AppStorePopover: FC<Props> = ({ children }) => { const MinAppsPopover: FC<Props> = ({ children }) => {
const [open, setOpen] = useState(false) const [open, setOpen] = useState(false)
const apps = getAllMinApps() const { minapps } = useMinapps()
useHotkeys('esc', () => { useHotkeys('esc', () => {
setOpen(false) setOpen(false)
@@ -29,10 +29,10 @@ const AppStorePopover: FC<Props> = ({ children }) => {
const content = ( const content = (
<PopoverContent> <PopoverContent>
<AppsContainer> <AppsContainer>
{apps.map((app) => ( {minapps.map((app) => (
<App key={app.id} app={app} onClick={handleClose} size={50} /> <App key={app.id} app={app} onClick={handleClose} size={50} />
))} ))}
{isEmpty(apps) && ( {isEmpty(minapps) && (
<Center> <Center>
<Empty /> <Empty />
</Center> </Center>
@@ -48,7 +48,7 @@ const AppStorePopover: FC<Props> = ({ children }) => {
content={content} content={content}
trigger="click" trigger="click"
placement="bottomRight" placement="bottomRight"
overlayInnerStyle={{ padding: 25 }}> styles={{ body: { padding: 25 } }}>
{children} {children}
</Popover> </Popover>
) )
@@ -59,7 +59,7 @@ const PopoverContent = styled(Scrollbar)``
const AppsContainer = styled.div` const AppsContainer = styled.div`
display: grid; display: grid;
grid-template-columns: repeat(6, minmax(90px, 1fr)); grid-template-columns: repeat(6, minmax(90px, 1fr));
gap: 25px; gap: 18px;
` `
export default AppStorePopover export default MinAppsPopover
@@ -1,7 +1,6 @@
import { PushpinOutlined, SearchOutlined } from '@ant-design/icons' import { PushpinOutlined, SearchOutlined } from '@ant-design/icons'
import VisionIcon from '@renderer/components/Icons/VisionIcon'
import { TopView } from '@renderer/components/TopView' import { TopView } from '@renderer/components/TopView'
import { getModelLogo, isEmbeddingModel, isVisionModel, isWebSearchModel } from '@renderer/config/models' import { getModelLogo, isEmbeddingModel } from '@renderer/config/models'
import db from '@renderer/databases' import db from '@renderer/databases'
import { useProviders } from '@renderer/hooks/useProvider' import { useProviders } from '@renderer/hooks/useProvider'
import { getModelUniqId } from '@renderer/services/ModelService' import { getModelUniqId } from '@renderer/services/ModelService'
@@ -12,8 +11,8 @@ import { useEffect, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import styled from 'styled-components' import styled from 'styled-components'
import WebSearchIcon from '../Icons/WebSearchIcon'
import { HStack } from '../Layout' import { HStack } from '../Layout'
import ModelTags from '../ModelTags'
import Scrollbar from '../Scrollbar' import Scrollbar from '../Scrollbar'
type MenuItem = Required<MenuProps>['items'][number] type MenuItem = Required<MenuProps>['items'][number]
@@ -33,6 +32,7 @@ const PopupContainer: React.FC<PopupContainerProps> = ({ model, resolve }) => {
const inputRef = useRef<InputRef>(null) const inputRef = useRef<InputRef>(null)
const { providers } = useProviders() const { providers } = useProviders()
const [pinnedModels, setPinnedModels] = useState<string[]>([]) const [pinnedModels, setPinnedModels] = useState<string[]>([])
const scrollContainerRef = useRef<HTMLDivElement>(null)
useEffect(() => { useEffect(() => {
const loadPinnedModels = async () => { const loadPinnedModels = async () => {
@@ -75,7 +75,7 @@ const PopupContainer: React.FC<PopupContainerProps> = ({ model, resolve }) => {
label: ( label: (
<ModelItem> <ModelItem>
<span> <span>
{m?.name} {isVisionModel(m) && <VisionIcon />} {isWebSearchModel(m) && <WebSearchIcon />} {m?.name} <ModelTags model={m} />
</span> </span>
<PinIcon <PinIcon
onClick={(e) => { onClick={(e) => {
@@ -118,7 +118,7 @@ const PopupContainer: React.FC<PopupContainerProps> = ({ model, resolve }) => {
key: getModelUniqId(m) + '_pinned', key: getModelUniqId(m) + '_pinned',
label: ( label: (
<ModelItem> <ModelItem>
{m?.name} {isVisionModel(m) && <VisionIcon />} {m?.name} <ModelTags model={m} />
<PinIcon <PinIcon
onClick={(e) => { onClick={(e) => {
e.stopPropagation() e.stopPropagation()
@@ -163,6 +163,17 @@ const PopupContainer: React.FC<PopupContainerProps> = ({ model, resolve }) => {
open && setTimeout(() => inputRef.current?.focus(), 0) open && setTimeout(() => inputRef.current?.focus(), 0)
}, [open]) }, [open])
useEffect(() => {
if (open && model) {
setTimeout(() => {
const selectedElement = document.querySelector('.ant-menu-item-selected')
if (selectedElement && scrollContainerRef.current) {
selectedElement.scrollIntoView({ block: 'center', behavior: 'auto' })
}
}, 100) // Small delay to ensure menu is rendered
}
}, [open, model])
return ( return (
<Modal <Modal
centered centered
@@ -200,7 +211,7 @@ const PopupContainer: React.FC<PopupContainerProps> = ({ model, resolve }) => {
/> />
</HStack> </HStack>
<Divider style={{ margin: 0, borderBlockStartWidth: 0.5 }} /> <Divider style={{ margin: 0, borderBlockStartWidth: 0.5 }} />
<Scrollbar style={{ height: '50vh' }}> <Scrollbar style={{ height: '50vh' }} ref={scrollContainerRef}>
<Container> <Container>
{filteredItems.length > 0 ? ( {filteredItems.length > 0 ? (
<StyledMenu <StyledMenu
@@ -0,0 +1,66 @@
import SettingsPage, { SettingsTab } from '@renderer/pages/settings/SettingsPage'
import { Modal } from 'antd'
import { FC, useState } from 'react'
import styled, { createGlobalStyle } from 'styled-components'
interface Props {
actionButton?: React.ReactNode
activeTab?: SettingsTab
}
const SettingsPopup: FC<Props> = (props) => {
const [open, setOpen] = useState(false)
const [activeTab, setActiveTab] = useState<SettingsTab | undefined>(props.activeTab)
const onOpen = () => {
if (props.activeTab) {
setActiveTab(props.activeTab)
}
setOpen(true)
}
const onCancel = () => {
setOpen(false)
}
return (
<>
<div onClick={onOpen}>{props.actionButton}</div>
<GlobalStyle />
<StyledModal
transitionName="ant-move-down"
width="80vw"
title={null}
open={open}
onCancel={onCancel}
footer={null}>
<SettingsPage activeTab={activeTab} onTabChange={setActiveTab} />
</StyledModal>
</>
)
}
const GlobalStyle = createGlobalStyle`
.ant-modal-mask {
backdrop-filter: blur(10px);
background-color: transparent !important;
}
`
const StyledModal = styled(Modal)`
min-width: 900px;
max-width: 1300px;
padding-bottom: 0;
.ant-modal-content {
padding: 0;
overflow: hidden;
border-radius: 12px;
}
.ant-modal-close {
top: 4px;
right: 4px;
}
`
export default SettingsPopup
@@ -4,6 +4,7 @@ import { TextAreaProps } from 'antd/lib/input'
import { TextAreaRef } from 'antd/lib/input/TextArea' import { TextAreaRef } from 'antd/lib/input/TextArea'
import { useEffect, useRef, useState } from 'react' import { useEffect, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import styled from 'styled-components'
import { TopView } from '../TopView' import { TopView } from '../TopView'
@@ -11,13 +12,14 @@ interface ShowParams {
text: string text: string
textareaProps?: TextAreaProps textareaProps?: TextAreaProps
modalProps?: ModalProps modalProps?: ModalProps
children?: (props: { onOk?: () => void; onCancel?: () => void }) => React.ReactNode
} }
interface Props extends ShowParams { interface Props extends ShowParams {
resolve: (data: any) => void 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 [open, setOpen] = useState(true)
const { t } = useTranslation() const { t } = useTranslation()
const [textValue, setTextValue] = useState(text) const [textValue, setTextValue] = useState(text)
@@ -68,17 +70,23 @@ const PopupContainer: React.FC<Props> = ({ text, textareaProps, modalProps, reso
ref={textareaRef} ref={textareaRef}
rows={2} rows={2}
autoFocus autoFocus
spellCheck={false}
{...textareaProps} {...textareaProps}
value={textValue} value={textValue}
onInput={resizeTextArea} onInput={resizeTextArea}
onChange={(e) => setTextValue(e.target.value)} onChange={(e) => setTextValue(e.target.value)}
/> />
<ChildrenContainer>{children && children({ onOk, onCancel })}</ChildrenContainer>
</Modal> </Modal>
) )
} }
const TopViewKey = 'TextEditPopup' const TopViewKey = 'TextEditPopup'
const ChildrenContainer = styled.div`
position: relative;
`
export default class TextEditPopup { export default class TextEditPopup {
static topviewId = 0 static topviewId = 0
static hide() { static hide() {
+1 -1
View File
@@ -55,7 +55,7 @@ const NavbarCenterContainer = styled.div`
flex: 1; flex: 1;
display: flex; display: flex;
align-items: center; align-items: center;
padding: 0 ${isMac ? '20px' : '15px'}; padding: 0 ${isMac ? '20px' : 0};
font-weight: bold; font-weight: bold;
color: var(--color-text-1); color: var(--color-text-1);
` `
+131 -79
View File
@@ -1,45 +1,40 @@
import { FileSearchOutlined, FolderOutlined, PictureOutlined, TranslationOutlined } from '@ant-design/icons' import { FileSearchOutlined, FolderOutlined, PictureOutlined, TranslationOutlined } from '@ant-design/icons'
import { isMac } from '@renderer/config/constant' import { isMac } from '@renderer/config/constant'
import { isLocalAi, UserAvatar } from '@renderer/config/env' import { UserAvatar } from '@renderer/config/env'
import { useTheme } from '@renderer/context/ThemeProvider' import { useTheme } from '@renderer/context/ThemeProvider'
import useAvatar from '@renderer/hooks/useAvatar' import useAvatar from '@renderer/hooks/useAvatar'
import { useMinapps } from '@renderer/hooks/useMinapps'
import { useRuntime } from '@renderer/hooks/useRuntime' import { useRuntime } from '@renderer/hooks/useRuntime'
import { useSettings } from '@renderer/hooks/useSettings' import { useSettings } from '@renderer/hooks/useSettings'
import type { MenuProps } from 'antd'
import { Tooltip } from 'antd' import { Tooltip } from 'antd'
import { Avatar } from 'antd' import { Avatar } from 'antd'
import { Dropdown } from 'antd'
import { FC } from 'react' import { FC } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { useLocation, useNavigate } from 'react-router-dom' import { useLocation, useNavigate } from 'react-router-dom'
import styled from 'styled-components' import styled from 'styled-components'
import DragableList from '../DragableList'
import MinAppIcon from '../Icons/MinAppIcon'
import MinApp from '../MinApp' import MinApp from '../MinApp'
import SettingsPopup from '../Popups/SettingsPopup'
import UserPopup from '../Popups/UserPopup' import UserPopup from '../Popups/UserPopup'
const Sidebar: FC = () => { const Sidebar: FC = () => {
const { pathname } = useLocation()
const avatar = useAvatar() const avatar = useAvatar()
const { minappShow } = useRuntime() const { minappShow } = useRuntime()
const { generating } = useRuntime()
const { t } = useTranslation() const { t } = useTranslation()
const navigate = useNavigate() const { windowStyle, sidebarIcons } = useSettings()
const { windowStyle, showMinappIcon, showFilesIcon } = useSettings()
const { theme, toggleTheme } = useTheme() const { theme, toggleTheme } = useTheme()
const { pinned } = useMinapps()
const isRoute = (path: string): string => (pathname === path ? 'active' : '')
const isRoutes = (path: string): string => (pathname.startsWith(path) ? 'active' : '')
const onEditUser = () => UserPopup.show() const onEditUser = () => UserPopup.show()
const macTransparentWindow = isMac && windowStyle === 'transparent' const macTransparentWindow = isMac && windowStyle === 'transparent'
const sidebarBgColor = macTransparentWindow ? 'transparent' : 'var(--navbar-background)' const sidebarBgColor = macTransparentWindow ? 'transparent' : 'var(--navbar-background)'
const to = (path: string) => { const showPinnedApps = pinned.length > 0 && sidebarIcons.visible.includes('minapp')
if (generating) {
window.message.warning({ content: t('message.switch.disabled'), key: 'switch-assistant' })
return
}
navigate(path)
}
return ( return (
<Container <Container
@@ -49,63 +44,19 @@ const Sidebar: FC = () => {
zIndex: minappShow ? 10000 : 'initial' zIndex: minappShow ? 10000 : 'initial'
}}> }}>
<AvatarImg src={avatar || UserAvatar} draggable={false} className="nodrag" onClick={onEditUser} /> <AvatarImg src={avatar || UserAvatar} draggable={false} className="nodrag" onClick={onEditUser} />
<MainMenus> <MainMenusContainer>
<Menus onClick={MinApp.onClose}> <Menus onClick={MinApp.onClose}>
<Tooltip title={t('assistants.title')} mouseEnterDelay={0.8} placement="right"> <MainMenus />
<StyledLink onClick={() => to('/')}>
<Icon className={isRoute('/')}>
<i className="iconfont icon-chat" />
</Icon>
</StyledLink>
</Tooltip>
<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.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.8} placement="right">
<StyledLink onClick={() => to('/translate')}>
<Icon className={isRoute('/translate')}>
<TranslationOutlined />
</Icon>
</StyledLink>
</Tooltip>
{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> </Menus>
</MainMenus> {showPinnedApps && (
<AppsContainer>
<Divider />
<Menus>
<PinnedApps />
</Menus>
</AppsContainer>
)}
</MainMenusContainer>
<Menus onClick={MinApp.onClose}> <Menus onClick={MinApp.onClose}>
<Tooltip title={t('settings.theme.title')} mouseEnterDelay={0.8} placement="right"> <Tooltip title={t('settings.theme.title')} mouseEnterDelay={0.8} placement="right">
<Icon onClick={() => toggleTheme()}> <Icon onClick={() => toggleTheme()}>
@@ -116,23 +67,102 @@ const Sidebar: FC = () => {
)} )}
</Icon> </Icon>
</Tooltip> </Tooltip>
<Tooltip title={t('settings.title')} mouseEnterDelay={0.8} placement="right"> <SettingsPopup
<StyledLink onClick={() => to(isLocalAi ? '/settings/assistant' : '/settings/provider')}> actionButton={
<Icon className={pathname.startsWith('/settings') ? 'active' : ''}> <Tooltip title={t('settings.title')} mouseEnterDelay={0.8} placement="right">
<i className="iconfont icon-setting" /> <Icon>
</Icon> <i className="iconfont icon-setting" />
</StyledLink> </Icon>
</Tooltip> </Tooltip>
}
/>
</Menus> </Menus>
</Container> </Container>
) )
} }
const MainMenus: FC = () => {
const { t } = useTranslation()
const { pathname } = useLocation()
const { sidebarIcons } = useSettings()
const navigate = useNavigate()
const isRoute = (path: string): string => (pathname === path ? 'active' : '')
const isRoutes = (path: string): string => (pathname.startsWith(path) ? 'active' : '')
const iconMap = {
assistants: <i className="iconfont icon-chat" />,
agents: <i className="iconfont icon-business-smart-assistant" />,
paintings: <PictureOutlined style={{ fontSize: 16 }} />,
translate: <TranslationOutlined />,
minapp: <i className="iconfont icon-appstore" />,
knowledge: <FileSearchOutlined />,
files: <FolderOutlined />
}
const pathMap = {
assistants: '/',
agents: '/agents',
paintings: '/paintings',
translate: '/translate',
minapp: '/apps',
knowledge: '/knowledge',
files: '/files'
}
return sidebarIcons.visible.map((icon) => {
const path = pathMap[icon]
const isActive = path === '/' ? isRoute(path) : isRoutes(path)
return (
<Tooltip key={icon} title={t(`${icon}.title`)} mouseEnterDelay={0.8} placement="right">
<StyledLink onClick={() => navigate(path)}>
<Icon className={isActive}>{iconMap[icon]}</Icon>
</StyledLink>
</Tooltip>
)
})
}
const PinnedApps: FC = () => {
const { pinned, updatePinnedMinapps } = useMinapps()
const { t } = useTranslation()
return (
<DragableList list={pinned} onUpdate={updatePinnedMinapps} listStyle={{ marginBottom: 5 }}>
{(app) => {
const menuItems: MenuProps['items'] = [
{
key: 'togglePin',
label: t('minapp.sidebar.remove.title'),
onClick: () => {
const newPinned = pinned.filter((item) => item.id !== app.id)
updatePinnedMinapps(newPinned)
}
}
]
return (
<Tooltip key={app.id} title={app.name} mouseEnterDelay={0.8} placement="right">
<StyledLink>
<Dropdown menu={{ items: menuItems }} trigger={['contextMenu']}>
<Icon onClick={() => MinApp.start(app)}>
<MinAppIcon size={20} app={app} style={{ borderRadius: 6 }} />
</Icon>
</Dropdown>
</StyledLink>
</Tooltip>
)
}}
</DragableList>
)
}
const Container = styled.div` const Container = styled.div`
display: flex; display: flex;
flex-direction: column; flex-direction: column;
align-items: center; align-items: center;
padding: 8px 0; padding: 8px 0;
padding-bottom: 12px;
width: var(--sidebar-width); width: var(--sidebar-width);
min-width: var(--sidebar-width); min-width: var(--sidebar-width);
height: ${isMac ? 'calc(100vh - var(--navbar-height))' : '100vh'}; height: ${isMac ? 'calc(100vh - var(--navbar-height))' : '100vh'};
@@ -149,15 +179,18 @@ const AvatarImg = styled(Avatar)`
border: none; border: none;
cursor: pointer; cursor: pointer;
` `
const MainMenus = styled.div` const MainMenusContainer = styled.div`
display: flex; display: flex;
flex: 1; flex: 1;
flex-direction: column;
overflow: hidden;
` `
const Menus = styled.div` const Menus = styled.div`
display: flex; display: flex;
flex-direction: column; flex-direction: column;
align-items: center; align-items: center;
gap: 5px;
` `
const Icon = styled.div` const Icon = styled.div`
@@ -167,7 +200,6 @@ const Icon = styled.div`
justify-content: center; justify-content: center;
align-items: center; align-items: center;
border-radius: 50%; border-radius: 50%;
margin-bottom: 5px;
-webkit-app-region: none; -webkit-app-region: none;
border: 0.5px solid transparent; border: 0.5px solid transparent;
.iconfont, .iconfont,
@@ -205,4 +237,24 @@ const StyledLink = styled.div`
} }
` `
const AppsContainer = styled.div`
display: flex;
flex: 1;
flex-direction: column;
align-items: center;
overflow-y: auto;
overflow-x: hidden;
margin-bottom: 10px;
-webkit-app-region: none;
&::-webkit-scrollbar {
display: none;
}
`
const Divider = styled.div`
width: 50%;
margin: 8px 0;
border-bottom: 0.5px solid var(--color-border);
`
export default Sidebar export default Sidebar
+71 -37
View File
@@ -1,38 +1,43 @@
import BaiduAiAppLogo from '@renderer/assets/images/apps/baidu-ai.png' import BaiduAiAppLogo from '@renderer/assets/images/apps/baidu-ai.png?url'
import BaicuanAppLogo from '@renderer/assets/images/apps/baixiaoying.webp' import BaicuanAppLogo from '@renderer/assets/images/apps/baixiaoying.webp?url'
import BoltAppLogo from '@renderer/assets/images/apps/bolt.svg' import BoltAppLogo from '@renderer/assets/images/apps/bolt.svg?url'
import DevvAppLogo from '@renderer/assets/images/apps/devv.png' import DevvAppLogo from '@renderer/assets/images/apps/devv.png?url'
import DoubaoAppLogo from '@renderer/assets/images/apps/doubao.png' import DoubaoAppLogo from '@renderer/assets/images/apps/doubao.png?url'
import DuckDuckGoAppLogo from '@renderer/assets/images/apps/duckduckgo.webp' import DuckDuckGoAppLogo from '@renderer/assets/images/apps/duckduckgo.webp?url'
import FeloAppLogo from '@renderer/assets/images/apps/felo.png' import FeloAppLogo from '@renderer/assets/images/apps/felo.png?url'
import GeminiAppLogo from '@renderer/assets/images/apps/gemini.png' import FlowithAppLogo from '@renderer/assets/images/apps/flowith.svg?url'
import GithubCopilotLogo from '@renderer/assets/images/apps/github-copilot.webp' import GeminiAppLogo from '@renderer/assets/images/apps/gemini.png?url'
import HuggingChatLogo from '@renderer/assets/images/apps/huggingchat.svg' import GensparkLogo from '@renderer/assets/images/apps/genspark.jpg?url'
import KimiAppLogo from '@renderer/assets/images/apps/kimi.jpg' import GithubCopilotLogo from '@renderer/assets/images/apps/github-copilot.webp?url'
import MetasoAppLogo from '@renderer/assets/images/apps/metaso.webp' import GrokAppLogo from '@renderer/assets/images/apps/grok.png?url'
import NamiAiSearchLogo from '@renderer/assets/images/apps/nm.webp' import HikaLogo from '@renderer/assets/images/apps/hika.webp?url'
import PerplexityAppLogo from '@renderer/assets/images/apps/perplexity.webp' import HuggingChatLogo from '@renderer/assets/images/apps/huggingchat.svg?url'
import PoeAppLogo from '@renderer/assets/images/apps/poe.webp' import KimiAppLogo from '@renderer/assets/images/apps/kimi.jpg?url'
import ZhipuProviderLogo from '@renderer/assets/images/apps/qingyan.png' import MetasoAppLogo from '@renderer/assets/images/apps/metaso.webp?url'
import SensetimeAppLogo from '@renderer/assets/images/apps/sensetime.png' import NamiAiSearchLogo from '@renderer/assets/images/apps/nm.webp?url'
import SparkDeskAppLogo from '@renderer/assets/images/apps/sparkdesk.png' import PerplexityAppLogo from '@renderer/assets/images/apps/perplexity.webp?url'
import ThinkAnyLogo from '@renderer/assets/images/apps/thinkany.webp' import PoeAppLogo from '@renderer/assets/images/apps/poe.webp?url'
import TiangongAiLogo from '@renderer/assets/images/apps/tiangong.png' import ZhipuProviderLogo from '@renderer/assets/images/apps/qingyan.png?url'
import WanZhiAppLogo from '@renderer/assets/images/apps/wanzhi.jpg' import QwenlmAppLogo from '@renderer/assets/images/apps/qwenlm.webp?url'
import TencentYuanbaoAppLogo from '@renderer/assets/images/apps/yuanbao.png' import SensetimeAppLogo from '@renderer/assets/images/apps/sensetime.png?url'
import YuewenAppLogo from '@renderer/assets/images/apps/yuewen.png' import SparkDeskAppLogo from '@renderer/assets/images/apps/sparkdesk.png?url'
import ZhihuAppLogo from '@renderer/assets/images/apps/zhihu.png' import ThinkAnyLogo from '@renderer/assets/images/apps/thinkany.webp?url'
import ClaudeAppLogo from '@renderer/assets/images/models/claude.png' import TiangongAiLogo from '@renderer/assets/images/apps/tiangong.png?url'
import HailuoModelLogo from '@renderer/assets/images/models/hailuo.png' import WanZhiAppLogo from '@renderer/assets/images/apps/wanzhi.jpg?url'
import QwenModelLogo from '@renderer/assets/images/models/qwen.png' import TencentYuanbaoAppLogo from '@renderer/assets/images/apps/yuanbao.png?url'
import DeepSeekProviderLogo from '@renderer/assets/images/providers/deepseek.png' import YuewenAppLogo from '@renderer/assets/images/apps/yuewen.png?url'
import GroqProviderLogo from '@renderer/assets/images/providers/groq.png' import ZhihuAppLogo from '@renderer/assets/images/apps/zhihu.png?url'
import OpenAiProviderLogo from '@renderer/assets/images/providers/openai.png' import ClaudeAppLogo from '@renderer/assets/images/models/claude.png?url'
import SiliconFlowProviderLogo from '@renderer/assets/images/providers/silicon.png' import HailuoModelLogo from '@renderer/assets/images/models/hailuo.png?url'
import QwenModelLogo from '@renderer/assets/images/models/qwen.png?url'
import DeepSeekProviderLogo from '@renderer/assets/images/providers/deepseek.png?url'
import GroqProviderLogo from '@renderer/assets/images/providers/groq.png?url'
import OpenAiProviderLogo from '@renderer/assets/images/providers/openai.png?url'
import SiliconFlowProviderLogo from '@renderer/assets/images/providers/silicon.png?url'
import MinApp from '@renderer/components/MinApp' import MinApp from '@renderer/components/MinApp'
import { MinAppType } from '@renderer/types' import { MinAppType } from '@renderer/types'
const _apps: MinAppType[] = [ export const DEFAULT_MIN_APPS: MinAppType[] = [
{ {
id: 'openai', id: 'openai',
name: 'ChatGPT', name: 'ChatGPT',
@@ -225,19 +230,48 @@ const _apps: MinAppType[] = [
url: 'https://thinkany.ai/', url: 'https://thinkany.ai/',
bodered: true bodered: true
}, },
{
id: 'hika',
name: 'Hika',
logo: HikaLogo,
url: 'https://hika.fyi/',
bodered: true
},
{ {
id: 'github-copilot', id: 'github-copilot',
name: 'GitHub Copilot', name: 'GitHub Copilot',
logo: GithubCopilotLogo, logo: GithubCopilotLogo,
url: 'https://github.com/copilot' url: 'https://github.com/copilot'
},
{
id: 'genspark',
name: 'Genspark',
logo: GensparkLogo,
url: 'https://www.genspark.ai/'
},
{
id: 'grok',
name: 'Grok',
logo: GrokAppLogo,
url: 'https://grok.com',
bodered: true
},
{
id: 'qwenlm',
name: 'QwenLM',
logo: QwenlmAppLogo,
url: 'https://qwenlm.ai/'
},
{
id: 'flowith',
name: 'Flowith',
logo: FlowithAppLogo,
url: 'https://www.flowith.io/',
bodered: true
} }
] ]
export function getAllMinApps() {
return _apps as MinAppType[]
}
export function startMinAppById(id: string) { export function startMinAppById(id: string) {
const app = getAllMinApps().find((app) => app?.id === id) const app = DEFAULT_MIN_APPS.find((app) => app?.id === id)
app && MinApp.start(app) app && MinApp.start(app)
} }
+173 -22
View File
@@ -10,6 +10,7 @@ import AisingaporeModelLogo from '@renderer/assets/images/models/aisingapore.png
import AisingaporeModelLogoDark from '@renderer/assets/images/models/aisingapore_dark.png' import AisingaporeModelLogoDark from '@renderer/assets/images/models/aisingapore_dark.png'
import BaichuanModelLogo from '@renderer/assets/images/models/baichuan.png' import BaichuanModelLogo from '@renderer/assets/images/models/baichuan.png'
import BaichuanModelLogoDark from '@renderer/assets/images/models/baichuan_dark.png' import BaichuanModelLogoDark from '@renderer/assets/images/models/baichuan_dark.png'
import BgeModelLogo from '@renderer/assets/images/models/bge.webp'
import BigcodeModelLogo from '@renderer/assets/images/models/bigcode.webp' import BigcodeModelLogo from '@renderer/assets/images/models/bigcode.webp'
import BigcodeModelLogoDark from '@renderer/assets/images/models/bigcode_dark.webp' import BigcodeModelLogoDark from '@renderer/assets/images/models/bigcode_dark.webp'
import ChatGLMModelLogo from '@renderer/assets/images/models/chatglm.png' import ChatGLMModelLogo from '@renderer/assets/images/models/chatglm.png'
@@ -125,6 +126,8 @@ import { getProviderByModel } from '@renderer/services/AssistantService'
import { Model } from '@renderer/types' import { Model } from '@renderer/types'
import OpenAI from 'openai' import OpenAI from 'openai'
import { getWebSearchTools } from './tools'
const visionAllowedModels = [ const visionAllowedModels = [
'llava', 'llava',
'moondream', 'moondream',
@@ -249,7 +252,8 @@ export function getModelLogo(modelId: string) {
rakutenai: isLight ? RakutenaiModelLogo : RakutenaiModelLogoDark, rakutenai: isLight ? RakutenaiModelLogo : RakutenaiModelLogoDark,
ibm: isLight ? IbmModelLogo : IbmModelLogoDark, ibm: isLight ? IbmModelLogo : IbmModelLogoDark,
'google/': isLight ? GoogleModelLogo : GoogleModelLogoDark, 'google/': isLight ? GoogleModelLogo : GoogleModelLogoDark,
hugging: isLight ? HuggingfaceModelLogo : HuggingfaceModelLogoDark hugging: isLight ? HuggingfaceModelLogo : HuggingfaceModelLogoDark,
'bge-': BgeModelLogo
} }
for (const key in logoMap) { for (const key in logoMap) {
@@ -262,6 +266,94 @@ export function getModelLogo(modelId: string) {
} }
export const SYSTEM_MODELS: Record<string, Model[]> = { export const SYSTEM_MODELS: Record<string, Model[]> = {
qwenlm: [
{
id: 'qwen-plus-latest',
provider: 'qwenlm',
name: 'Qwen2.5-Plus',
group: 'Qwen 2.5'
},
{
id: 'qvq-72b-preview',
provider: 'qwenlm',
name: 'QVQ-72B-Preview',
group: 'QVQ'
},
{
id: 'qwq-32b-preview',
provider: 'qwenlm',
name: 'QwQ-32B-Preview',
group: 'QVQ'
},
{
id: 'qwen2.5-coder-32b-instruct',
provider: 'qwenlm',
name: 'Qwen2.5-Coder-32B-Instruct',
group: 'Qwen 2.5'
},
{
id: 'qwen-vl-max-latest',
provider: 'qwenlm',
name: 'Qwen2-VL-Max',
group: 'Qwen 2'
},
{
id: 'qwen-turbo-latest',
provider: 'qwenlm',
name: 'Qwen2.5-Turbo',
group: 'Qwen 2.5'
},
{
id: 'qwen2.5-72b-instruct',
provider: 'qwenlm',
name: 'Qwen2.5-72B-Instruct',
group: 'Qwen 2.5'
},
{
id: 'qwen2.5-32b-instruct',
provider: 'qwenlm',
name: 'Qwen2.5-32B-Instruct',
group: 'Qwen 2.5'
}
],
aihubmix: [
{
id: 'gpt-4o',
provider: 'aihubmix',
name: 'GPT-4o',
group: 'GPT-4o'
},
{
id: 'claude-3-5-sonnet-latest',
provider: 'aihubmix',
name: 'Claude 3.5 Sonnet',
group: 'Claude 3.5'
},
{
id: 'gemini-2.0-flash-exp-search',
provider: 'aihubmix',
name: 'Gemini 2.0 Flash Exp Search',
group: 'Gemini 2.0'
},
{
id: 'deepseek-chat',
provider: 'aihubmix',
name: 'DeepSeek Chat',
group: 'DeepSeek Chat'
},
{
id: 'aihubmix-Llama-3-3-70B-Instruct',
provider: 'aihubmix',
name: 'Llama-3.3-70b',
group: 'Llama 3.3'
},
{
id: 'Qwen/QVQ-72B-Preview',
provider: 'aihubmix',
name: 'Qwen/QVQ-72B',
group: 'Qwen'
}
],
ollama: [], ollama: [],
silicon: [ silicon: [
{ {
@@ -274,7 +366,7 @@ export const SYSTEM_MODELS: Record<string, Model[]> = {
id: 'Qwen/Qwen2.5-7B-Instruct', id: 'Qwen/Qwen2.5-7B-Instruct',
provider: 'silicon', provider: 'silicon',
name: 'Qwen2.5-7B-Instruct', name: 'Qwen2.5-7B-Instruct',
group: 'Qwen2.5' group: 'Qwen'
}, },
{ {
id: 'meta-llama/Llama-3.3-70B-Instruct', id: 'meta-llama/Llama-3.3-70B-Instruct',
@@ -351,10 +443,10 @@ export const SYSTEM_MODELS: Record<string, Model[]> = {
group: 'DeepSeek Chat' group: 'DeepSeek Chat'
}, },
{ {
id: 'deepseek-coder', id: 'deepseek-reasoner',
provider: 'deepseek', provider: 'deepseek',
name: 'DeepSeek Coder', name: 'DeepSeek Reasoner',
group: 'DeepSeek Coder' group: 'DeepSeek Reasoner'
} }
], ],
together: [ together: [
@@ -521,9 +613,21 @@ export const SYSTEM_MODELS: Record<string, Model[]> = {
], ],
zhipu: [ zhipu: [
{ {
id: 'glm-4', id: 'glm-zero-preview',
provider: 'zhipu', provider: 'zhipu',
name: 'GLM-4', name: 'GLM-Zero-Preview',
group: 'GLM-Zero'
},
{
id: 'glm-4-0520',
provider: 'zhipu',
name: 'GLM-4-0520',
group: 'GLM-4'
},
{
id: 'glm-4-long',
provider: 'zhipu',
name: 'GLM-4-Long',
group: 'GLM-4' group: 'GLM-4'
}, },
{ {
@@ -550,6 +654,12 @@ export const SYSTEM_MODELS: Record<string, Model[]> = {
name: 'GLM-4-Flash', name: 'GLM-4-Flash',
group: 'GLM-4' group: 'GLM-4'
}, },
{
id: 'glm-4-flashx',
provider: 'zhipu',
name: 'GLM-4-FlashX',
group: 'GLM-4'
},
{ {
id: 'glm-4v', id: 'glm-4v',
provider: 'zhipu', provider: 'zhipu',
@@ -567,6 +677,12 @@ export const SYSTEM_MODELS: Record<string, Model[]> = {
provider: 'zhipu', provider: 'zhipu',
name: 'GLM-4-AllTools', name: 'GLM-4-AllTools',
group: 'GLM-4-AllTools' group: 'GLM-4-AllTools'
},
{
id: 'embedding-3',
provider: 'zhipu',
name: 'Embedding-3',
group: 'Embedding'
} }
], ],
moonshot: [ moonshot: [
@@ -644,6 +760,12 @@ export const SYSTEM_MODELS: Record<string, Model[]> = {
provider: 'minimax', provider: 'minimax',
name: 'abab5.5s', name: 'abab5.5s',
group: 'abab5' group: 'abab5'
},
{
id: 'minimax-text-01',
provider: 'minimax',
name: 'minimax-01',
group: 'minimax-01'
} }
], ],
hyperbolic: [ hyperbolic: [
@@ -750,20 +872,6 @@ export const SYSTEM_MODELS: Record<string, Model[]> = {
group: 'Jina Embeddings V3' group: 'Jina Embeddings V3'
} }
], ],
aihubmix: [
{
id: 'gpt-4o-mini',
provider: 'aihubmix',
name: 'GPT-4o Mini',
group: 'GPT-4o'
},
{
id: 'aihubmix-Llama-3-70B-Instruct',
provider: 'aihubmix',
name: 'Llama 3 70B Instruct',
group: 'Llama3'
}
],
fireworks: [ fireworks: [
{ {
id: 'accounts/fireworks/models/mythomax-l2-13b', id: 'accounts/fireworks/models/mythomax-l2-13b',
@@ -961,6 +1069,11 @@ export const TEXT_TO_IMAGES_MODELS = [
} }
] ]
export const TEXT_TO_IMAGES_MODELS_SUPPORT_IMAGE_ENHANCEMENT = [
'stabilityai/stable-diffusion-2-1',
'stabilityai/stable-diffusion-xl-base-1.0'
]
export function isTextToImageModel(model: Model): boolean { export function isTextToImageModel(model: Model): boolean {
return TEXT_TO_IMAGE_REGEX.test(model.id) return TEXT_TO_IMAGE_REGEX.test(model.id)
} }
@@ -1004,5 +1117,43 @@ export function isWebSearchModel(model: Model): boolean {
return false return false
} }
return (provider.id === 'gemini' || provider?.type === 'gemini') && model?.id === 'gemini-2.0-flash-exp' if (provider?.type === 'openai') {
if (model?.id?.includes('gemini-2.0-flash-exp')) {
return true
}
}
if (provider.id === 'gemini' || provider?.type === 'gemini') {
return model?.id === 'gemini-2.0-flash-exp'
}
if (provider.id === 'hunyuan') {
return model?.id !== 'hunyuan-lite'
}
if (provider.id === 'aihubmix') {
return model?.id === 'gemini-2.0-flash-exp-search'
}
if (provider.id === 'zhipu') {
return model?.id?.startsWith('glm-4-')
}
return false
}
export function getOpenAIWebSearchParams(model: Model): Record<string, any> {
if (isWebSearchModel(model)) {
const webSearchTools = getWebSearchTools(model)
if (model.provider === 'hunyuan') {
return { enable_enhancement: true }
}
return {
tools: webSearchTools
}
}
return {}
} }
+1 -1
View File
@@ -45,7 +45,7 @@ export const AGENT_PROMPT = `
` `
export const SUMMARIZE_PROMPT = export const SUMMARIZE_PROMPT =
'你是一名擅长会话的助理,你需要将用户的会话总结为 10 个字以内的标题,不要使用标点符号和其他特殊符号' '你是一名擅长会话的助理,你需要将用户的会话总结为 10 个字以内的标题,标题语言与用户的首要语言一致,不要使用标点符号和其他特殊符号'
export const TRANSLATE_PROMPT = 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.' '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.'
+15 -1
View File
@@ -23,6 +23,7 @@ import OcoolAiProviderLogo from '@renderer/assets/images/providers/ocoolai.png'
import OllamaProviderLogo from '@renderer/assets/images/providers/ollama.png' import OllamaProviderLogo from '@renderer/assets/images/providers/ollama.png'
import OpenAiProviderLogo from '@renderer/assets/images/providers/openai.png' import OpenAiProviderLogo from '@renderer/assets/images/providers/openai.png'
import OpenRouterProviderLogo from '@renderer/assets/images/providers/openrouter.png' import OpenRouterProviderLogo from '@renderer/assets/images/providers/openrouter.png'
import QwenLMProviderLogo from '@renderer/assets/images/providers/qwenlm.png'
import SiliconFlowProviderLogo from '@renderer/assets/images/providers/silicon.png' import SiliconFlowProviderLogo from '@renderer/assets/images/providers/silicon.png'
import StepProviderLogo from '@renderer/assets/images/providers/step.png' import StepProviderLogo from '@renderer/assets/images/providers/step.png'
import TogetherProviderLogo from '@renderer/assets/images/providers/together.png' import TogetherProviderLogo from '@renderer/assets/images/providers/together.png'
@@ -91,6 +92,8 @@ export function getProviderLogo(providerId: string) {
return MistralProviderLogo return MistralProviderLogo
case 'jina': case 'jina':
return JinaProviderLogo return JinaProviderLogo
case 'qwenlm':
return QwenLMProviderLogo
default: default:
return undefined return undefined
} }
@@ -402,7 +405,7 @@ export const PROVIDER_CONFIG = {
url: 'https://integrate.api.nvidia.com' url: 'https://integrate.api.nvidia.com'
}, },
websites: { websites: {
official: 'https://ai.360.com/', official: 'https://build.nvidia.com/explore/discover',
apiKey: 'https://build.nvidia.com/meta/llama-3_1-405b-instruct', apiKey: 'https://build.nvidia.com/meta/llama-3_1-405b-instruct',
docs: 'https://docs.api.nvidia.com/nim/reference/llm-apis', docs: 'https://docs.api.nvidia.com/nim/reference/llm-apis',
models: 'https://build.nvidia.com/nim' models: 'https://build.nvidia.com/nim'
@@ -418,5 +421,16 @@ export const PROVIDER_CONFIG = {
docs: 'https://learn.microsoft.com/en-us/azure/ai-services/openai/', docs: 'https://learn.microsoft.com/en-us/azure/ai-services/openai/',
models: 'https://learn.microsoft.com/en-us/azure/ai-services/openai/concepts/models' models: 'https://learn.microsoft.com/en-us/azure/ai-services/openai/concepts/models'
} }
},
qwenlm: {
api: {
url: 'https://chat.qwenlm.ai/api/'
},
websites: {
official: 'https://chat.qwenlm.ai',
apiKey: 'https://chat.qwenlm.ai',
docs: 'https://chat.qwenlm.ai',
models: 'https://chat.qwenlm.ai'
}
} }
} }
+32
View File
@@ -0,0 +1,32 @@
import { Model } from '@renderer/types'
import { ChatCompletionTool } from 'openai/resources'
export function getWebSearchTools(model: Model): ChatCompletionTool[] {
if (model?.provider === 'zhipu') {
if (model.id === 'glm-4-alltools') {
return [
{
type: 'web_browser'
} as unknown as ChatCompletionTool
]
}
return [
{
type: 'web_search',
web_search: {
enable: true,
search_result: true
}
} as unknown as ChatCompletionTool
]
}
return [
{
type: 'function',
function: {
name: 'googleSearch'
}
}
]
}
+59
View File
@@ -0,0 +1,59 @@
import i18n from '@renderer/i18n'
export const TranslateLanguageOptions = [
{
value: 'english',
label: i18n.t('languages.english'),
emoji: '🇬🇧'
},
{
value: 'chinese',
label: i18n.t('languages.chinese'),
emoji: '🇨🇳'
},
{
value: 'chinese-traditional',
label: i18n.t('languages.chinese-traditional'),
emoji: '🇭🇰'
},
{
value: 'japanese',
label: i18n.t('languages.japanese'),
emoji: '🇯🇵'
},
{
value: 'korean',
label: i18n.t('languages.korean'),
emoji: '🇰🇷'
},
{
value: 'russian',
label: i18n.t('languages.russian'),
emoji: '🇷🇺'
},
{
value: 'spanish',
label: i18n.t('languages.spanish'),
emoji: '🇪🇸'
},
{
value: 'french',
label: i18n.t('languages.french'),
emoji: '🇫🇷'
},
{
value: 'italian',
label: i18n.t('languages.italian'),
emoji: '🇮🇹'
},
{
value: 'portuguese',
label: i18n.t('languages.portuguese'),
emoji: '🇵🇹'
},
{
value: 'arabic',
label: i18n.t('languages.arabic'),
emoji: '🇸🇦'
}
]
@@ -54,13 +54,12 @@ export const SyntaxHighlighterProvider: React.FC<PropsWithChildren> = ({ childre
const codeToHtml = async (code: string, language: string) => { const codeToHtml = async (code: string, language: string) => {
if (!highlighter) return '' if (!highlighter) return ''
const escapedCode = code.replace(/[<>]/g, (char) => ({ '<': '&lt;', '>': '&gt;' })[char]!) const escapedCode = code?.replace(/[<>]/g, (char) => ({ '<': '&lt;', '>': '&gt;' })[char]!)
try { try {
if (!highlighter.getLoadedLanguages().includes(language as BundledLanguage)) { if (!highlighter.getLoadedLanguages().includes(language as BundledLanguage)) {
if (language in bundledLanguages || language === 'text') { if (language in bundledLanguages || language === 'text') {
await highlighter.loadLanguage(language as BundledLanguage) await highlighter.loadLanguage(language as BundledLanguage)
console.log(`Loaded language: ${language}`)
} else { } else {
return `<pre style="padding: 10px"><code>${escapedCode}</code></pre>` return `<pre style="padding: 10px"><code>${escapedCode}</code></pre>`
} }
+11 -4
View File
@@ -1,6 +1,7 @@
import { isMac } from '@renderer/config/constant' import { isMac } from '@renderer/config/constant'
import { useSettings } from '@renderer/hooks/useSettings' import { useSettings } from '@renderer/hooks/useSettings'
import { ThemeMode } from '@renderer/types' import { ThemeMode } from '@renderer/types'
import { isMiniWindow } from '@renderer/utils'
import React, { createContext, PropsWithChildren, useContext, useEffect, useState } from 'react' import React, { createContext, PropsWithChildren, useContext, useEffect, useState } from 'react'
interface ThemeContextType { interface ThemeContextType {
@@ -13,7 +14,11 @@ const ThemeContext = createContext<ThemeContextType>({
toggleTheme: () => {} toggleTheme: () => {}
}) })
export const ThemeProvider: React.FC<PropsWithChildren> = ({ children }) => { interface ThemeProviderProps extends PropsWithChildren {
defaultTheme?: ThemeMode
}
export const ThemeProvider: React.FC<ThemeProviderProps> = ({ children, defaultTheme }) => {
const { theme, setTheme } = useSettings() const { theme, setTheme } = useSettings()
const [_theme, _setTheme] = useState(theme) const [_theme, _setTheme] = useState(theme)
@@ -22,7 +27,7 @@ export const ThemeProvider: React.FC<PropsWithChildren> = ({ children }) => {
} }
useEffect((): any => { useEffect((): any => {
if (theme === ThemeMode.auto) { if (theme === ThemeMode.auto || defaultTheme === ThemeMode.auto) {
const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)') const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)')
_setTheme(mediaQuery.matches ? ThemeMode.dark : ThemeMode.light) _setTheme(mediaQuery.matches ? ThemeMode.dark : ThemeMode.light)
const handleChange = (e: MediaQueryListEvent) => _setTheme(e.matches ? ThemeMode.dark : ThemeMode.light) const handleChange = (e: MediaQueryListEvent) => _setTheme(e.matches ? ThemeMode.dark : ThemeMode.light)
@@ -31,11 +36,13 @@ export const ThemeProvider: React.FC<PropsWithChildren> = ({ children }) => {
} else { } else {
_setTheme(theme) _setTheme(theme)
} }
}, [theme]) }, [defaultTheme, theme])
useEffect(() => { useEffect(() => {
document.body.setAttribute('theme-mode', _theme) document.body.setAttribute('theme-mode', _theme)
window.api?.setTheme(_theme === ThemeMode.dark ? 'dark' : 'light') if (!isMiniWindow()) {
window.api?.setTheme(_theme === ThemeMode.dark ? 'dark' : 'light')
}
}, [_theme]) }, [_theme])
useEffect(() => { useEffect(() => {
+3 -16
View File
@@ -2,9 +2,8 @@ import { isMac } from '@renderer/config/constant'
import { isLocalAi } from '@renderer/config/env' import { isLocalAi } from '@renderer/config/env'
import db from '@renderer/databases' import db from '@renderer/databases'
import i18n from '@renderer/i18n' import i18n from '@renderer/i18n'
import { startAutoSync, stopAutoSync } from '@renderer/services/BackupService'
import { useAppDispatch } from '@renderer/store' import { useAppDispatch } from '@renderer/store'
import { setAvatar, setFilesPath, setUpdateState } from '@renderer/store/runtime' import { setAvatar, setFilesPath, setResourcesPath, setUpdateState } from '@renderer/store/runtime'
import { delay, runAsyncFunction } from '@renderer/utils' import { delay, runAsyncFunction } from '@renderer/utils'
import { useLiveQuery } from 'dexie-react-hooks' import { useLiveQuery } from 'dexie-react-hooks'
import { useEffect } from 'react' import { useEffect } from 'react'
@@ -16,16 +15,7 @@ import useUpdateHandler from './useUpdateHandler'
export function useAppInit() { export function useAppInit() {
const dispatch = useAppDispatch() const dispatch = useAppDispatch()
const { const { proxyUrl, language, windowStyle, manualUpdateCheck, proxyMode, customCss } = useSettings()
proxyUrl,
language,
windowStyle,
manualUpdateCheck,
proxyMode,
webdavAutoSync,
webdavSyncInterval,
customCss
} = useSettings()
const { minappShow } = useRuntime() const { minappShow } = useRuntime()
const { setDefaultModel, setTopicNamingModel, setTranslateModel } = useDefaultModel() const { setDefaultModel, setTopicNamingModel, setTranslateModel } = useDefaultModel()
const avatar = useLiveQuery(() => db.settings.get('image://avatar')) const avatar = useLiveQuery(() => db.settings.get('image://avatar'))
@@ -81,13 +71,10 @@ export function useAppInit() {
// set files path // set files path
window.api.getAppInfo().then((info) => { window.api.getAppInfo().then((info) => {
dispatch(setFilesPath(info.filesPath)) dispatch(setFilesPath(info.filesPath))
dispatch(setResourcesPath(info.resourcesPath))
}) })
}, [dispatch]) }, [dispatch])
useEffect(() => {
webdavAutoSync ? startAutoSync() : stopAutoSync()
}, [webdavAutoSync, webdavSyncInterval])
useEffect(() => { useEffect(() => {
import('@renderer/queue/KnowledgeQueue') import('@renderer/queue/KnowledgeQueue')
}, []) }, [])
+30 -1
View File
@@ -15,6 +15,7 @@ import {
renameBase, renameBase,
updateBase, updateBase,
updateBases, updateBases,
updateItem as updateItemAction,
updateItemProcessingStatus, updateItemProcessingStatus,
updateNotes updateNotes
} from '@renderer/store/knowledge' } from '@renderer/store/knowledge'
@@ -117,7 +118,8 @@ export const useKnowledge = (baseId: string) => {
await db.knowledge_notes.put(updatedNote) await db.knowledge_notes.put(updatedNote)
dispatch(updateNotes({ baseId, item: updatedNote })) dispatch(updateNotes({ baseId, item: updatedNote }))
} }
setTimeout(() => KnowledgeQueue.checkAllBases(), 0) const noteItem = base?.items.find((item) => item.id === noteId)
noteItem && refreshItem(noteItem)
} }
// 获取笔记内容 // 获取笔记内容
@@ -125,6 +127,10 @@ export const useKnowledge = (baseId: string) => {
return await db.knowledge_notes.get(noteId) return await db.knowledge_notes.get(noteId)
} }
const updateItem = (item: KnowledgeItem) => {
dispatch(updateItemAction({ baseId, item }))
}
// 移除项目 // 移除项目
const removeItem = async (item: KnowledgeItem) => { const removeItem = async (item: KnowledgeItem) => {
dispatch(removeItemAction({ baseId, item })) dispatch(removeItemAction({ baseId, item }))
@@ -138,6 +144,27 @@ export const useKnowledge = (baseId: string) => {
} }
} }
// 刷新项目
const refreshItem = async (item: KnowledgeItem) => {
const status = getProcessingStatus(item.id)
if (status === 'pending' || status === 'processing') {
return
}
if (base && item.uniqueId) {
await window.api.knowledgeBase.remove({ uniqueId: item.uniqueId, base: getKnowledgeBaseParams(base) })
updateItem({
...item,
processingStatus: 'pending',
processingProgress: 0,
processingError: '',
uniqueId: undefined
})
setTimeout(() => KnowledgeQueue.checkAllBases(), 0)
}
}
// 更新处理状态 // 更新处理状态
const updateItemStatus = (itemId: string, status: ProcessingStatus, progress?: number, error?: string) => { const updateItemStatus = (itemId: string, status: ProcessingStatus, progress?: number, error?: string) => {
dispatch( dispatch(
@@ -238,7 +265,9 @@ export const useKnowledge = (baseId: string) => {
addNote, addNote,
updateNoteContent, updateNoteContent,
getNoteContent, getNoteContent,
updateItem,
updateItemStatus, updateItemStatus,
refreshItem,
getProcessingStatus, getProcessingStatus,
getProcessingItemsByType, getProcessingItemsByType,
clearCompleted, clearCompleted,
+28
View File
@@ -37,4 +37,32 @@ export const useMermaid = () => {
setTimeout(renderMermaid, 100) setTimeout(renderMermaid, 100)
}, [generating]) }, [generating])
useEffect(() => {
const handleWheel = (e: WheelEvent) => {
if (e.ctrlKey || e.metaKey) {
e.preventDefault()
const mermaidElement = (e.target as HTMLElement).closest('.mermaid')
if (!mermaidElement) return
const svg = mermaidElement.querySelector('svg')
if (!svg) return
const currentScale = parseFloat(svg.style.transform?.match(/scale\((.*?)\)/)?.[1] || '1')
const delta = e.deltaY < 0 ? 0.1 : -0.1
const newScale = Math.max(0.1, Math.min(3, currentScale + delta))
const container = svg.parentElement
if (container) {
container.style.overflow = 'auto'
container.style.position = 'relative'
svg.style.transformOrigin = 'top left'
svg.style.transform = `scale(${newScale})`
}
}
}
document.addEventListener('wheel', handleWheel, { passive: false })
return () => document.removeEventListener('wheel', handleWheel)
}, [])
} }
+24
View File
@@ -0,0 +1,24 @@
import { DEFAULT_MIN_APPS } from '@renderer/config/minapps'
import { RootState, useAppDispatch, useAppSelector } from '@renderer/store'
import { setDisabledMinApps, setMinApps, setPinnedMinApps } from '@renderer/store/minapps'
import { MinAppType } from '@renderer/types'
export const useMinapps = () => {
const { enabled, disabled, pinned } = useAppSelector((state: RootState) => state.minapps)
const dispatch = useAppDispatch()
return {
minapps: enabled.map((app) => DEFAULT_MIN_APPS.find((item) => item.id === app.id) || app),
disabled: disabled.map((app) => DEFAULT_MIN_APPS.find((item) => item.id === app.id) || app),
pinned: pinned.map((app) => DEFAULT_MIN_APPS.find((item) => item.id === app.id) || app),
updateMinapps: (minapps: MinAppType[]) => {
dispatch(setMinApps(minapps))
},
updateDisabledMinapps: (minapps: MinAppType[]) => {
dispatch(setDisabledMinApps(minapps))
},
updatePinnedMinapps: (minapps: MinAppType[]) => {
dispatch(setPinnedMinApps(minapps))
}
}
}
+2 -1
View File
@@ -14,6 +14,7 @@ export function usePaintings() {
paintings, paintings,
addPainting: () => { addPainting: () => {
const newPainting: Painting = { const newPainting: Painting = {
model: TEXT_TO_IMAGES_MODELS[0].id,
id: uuid(), id: uuid(),
urls: [], urls: [],
files: [], files: [],
@@ -24,7 +25,7 @@ export function usePaintings() {
seed: generateRandomSeed(), seed: generateRandomSeed(),
steps: 25, steps: 25,
guidanceScale: 4.5, guidanceScale: 4.5,
model: TEXT_TO_IMAGES_MODELS[0].id promptEnhancement: true
} }
dispatch(addPainting(newPainting)) dispatch(addPainting(newPainting))
return newPainting return newPainting
+13 -1
View File
@@ -1,5 +1,17 @@
import { useAppSelector } from '@renderer/store' import i18n from '@renderer/i18n'
import store, { useAppSelector } from '@renderer/store'
export function useRuntime() { export function useRuntime() {
return useAppSelector((state) => state.runtime) return useAppSelector((state) => state.runtime)
} }
export function modelGenerating() {
const generating = store.getState().runtime.generating
if (generating) {
window.message.warning({ content: i18n.t('message.switch.disabled'), key: 'model-generating' })
return Promise.reject()
}
return Promise.resolve()
}
+18 -2
View File
@@ -1,13 +1,15 @@
import { useAppDispatch, useAppSelector } from '@renderer/store' import store, { useAppDispatch, useAppSelector } from '@renderer/store'
import { import {
SendMessageShortcut, SendMessageShortcut,
setSendMessageShortcut as _setSendMessageShortcut, setSendMessageShortcut as _setSendMessageShortcut,
setSidebarIcons,
setTheme, setTheme,
SettingsState,
setTopicPosition, setTopicPosition,
setTray, setTray,
setWindowStyle setWindowStyle
} from '@renderer/store/settings' } from '@renderer/store/settings'
import { ThemeMode } from '@renderer/types' import { SidebarIcon, ThemeMode } from '@renderer/types'
export function useSettings() { export function useSettings() {
const settings = useAppSelector((state) => state.settings) const settings = useAppSelector((state) => state.settings)
@@ -20,6 +22,7 @@ export function useSettings() {
}, },
setTray(isActive: boolean) { setTray(isActive: boolean) {
dispatch(setTray(isActive)) dispatch(setTray(isActive))
window.api.setTray(isActive)
}, },
setTheme(theme: ThemeMode) { setTheme(theme: ThemeMode) {
dispatch(setTheme(theme)) dispatch(setTheme(theme))
@@ -29,6 +32,15 @@ export function useSettings() {
}, },
setTopicPosition(topicPosition: 'left' | 'right') { setTopicPosition(topicPosition: 'left' | 'right') {
dispatch(setTopicPosition(topicPosition)) dispatch(setTopicPosition(topicPosition))
},
updateSidebarIcons(icons: { visible: SidebarIcon[]; disabled: SidebarIcon[] }) {
dispatch(setSidebarIcons(icons))
},
updateSidebarVisibleIcons(icons: SidebarIcon[]) {
dispatch(setSidebarIcons({ visible: icons }))
},
updateSidebarDisabledIcons(icons: SidebarIcon[]) {
dispatch(setSidebarIcons({ disabled: icons }))
} }
} }
} }
@@ -41,3 +53,7 @@ export function useMessageStyle() {
isBubbleStyle isBubbleStyle
} }
} }
export const getStoreSetting = (key: keyof SettingsState) => {
return store.getState().settings[key]
}
+2 -1
View File
@@ -1,5 +1,6 @@
import { isMac, isWindows } from '@renderer/config/constant' import { isMac, isWindows } from '@renderer/config/constant'
import { useAppSelector } from '@renderer/store' import { useAppSelector } from '@renderer/store'
import { orderBy } from 'lodash'
import { useCallback } from 'react' import { useCallback } from 'react'
import { useHotkeys } from 'react-hotkeys-hook' import { useHotkeys } from 'react-hotkeys-hook'
@@ -58,7 +59,7 @@ export const useShortcut = (
export function useShortcuts() { export function useShortcuts() {
const shortcuts = useAppSelector((state) => state.shortcuts.shortcuts) const shortcuts = useAppSelector((state) => state.shortcuts.shortcuts)
return { shortcuts } return { shortcuts: orderBy(shortcuts, 'system', 'desc') }
} }
export function useShortcutDisplay(key: string) { export function useShortcutDisplay(key: string) {
@@ -9,7 +9,10 @@ export default function useUpdateHandler() {
const { t } = useTranslation() const { t } = useTranslation()
useEffect(() => { useEffect(() => {
if (!window.electron) return
const ipcRenderer = window.electron.ipcRenderer const ipcRenderer = window.electron.ipcRenderer
const removers = [ const removers = [
ipcRenderer.on('update-not-available', () => { ipcRenderer.on('update-not-available', () => {
dispatch(setUpdateState({ checking: false })) dispatch(setUpdateState({ checking: false }))
+117 -17
View File
@@ -64,14 +64,14 @@
"default.description": "Hello, I'm Default Assistant. You can start chatting with me right away", "default.description": "Hello, I'm Default Assistant. You can start chatting with me right away",
"default.name": "⭐️ Default Assistant", "default.name": "⭐️ Default Assistant",
"default.topic.name": "Default Topic", "default.topic.name": "Default Topic",
"input.clear": "Clear", "input.clear": "Clear {{Command}}",
"input.clear.content": "Do you want to clear all messages of the current topic?", "input.clear.content": "Do you want to clear all messages of the current topic?",
"input.clear.title": "Clear all messages?", "input.clear.title": "Clear all messages?",
"input.collapse": "Collapse", "input.collapse": "Collapse",
"input.context_count.tip": "Context Count", "input.context_count.tip": "Context Count",
"input.estimated_tokens.tip": "Estimated tokens", "input.estimated_tokens.tip": "Estimated tokens",
"input.expand": "Expand", "input.expand": "Expand",
"input.new.context": "Clear Context", "input.new.context": "Clear Context {{Command}}",
"input.new_topic": "New Topic {{Command}}", "input.new_topic": "New Topic {{Command}}",
"input.pause": "Pause", "input.pause": "Pause",
"input.placeholder": "Type your message here...", "input.placeholder": "Type your message here...",
@@ -86,6 +86,7 @@
"message.new.branch.created": "New Branch Created", "message.new.branch.created": "New Branch Created",
"message.regenerate.model": "Switch Model", "message.regenerate.model": "Switch Model",
"message.new.context": "New Context", "message.new.context": "New Context",
"message.useful": "Helpful",
"save": "Save", "save": "Save",
"settings.code_collapsible": "Code block collapsible", "settings.code_collapsible": "Code block collapsible",
"settings.context_count": "Context", "settings.context_count": "Context",
@@ -112,7 +113,10 @@
"topics.list": "Topic List", "topics.list": "Topic List",
"topics.move_to": "Move to", "topics.move_to": "Move to",
"topics.title": "Topics", "topics.title": "Topics",
"translate": "Translate" "translate": "Translate",
"resend": "Resend",
"thinking": "Thinking",
"deeply_thought": "Deeply thought ({{secounds}} seconds)"
}, },
"common": { "common": {
"and": "and", "and": "and",
@@ -182,8 +186,14 @@
"name": "Name", "name": "Name",
"open": "Open", "open": "Open",
"size": "Size", "size": "Size",
"type": "Type",
"text": "Text", "text": "Text",
"title": "Files" "title": "Files",
"edit": "Edit",
"delete": "Delete",
"delete.title": "Delete File",
"delete.content": "Deleting a file will delete its reference from all messages. Are you sure you want to delete this file?",
"delete.paintings.warning": "Image contains this file, deletion is not possible"
}, },
"history": { "history": {
"continue_chat": "Continue Chatting", "continue_chat": "Continue Chatting",
@@ -211,6 +221,10 @@
"png": "Download PNG", "png": "Download PNG",
"svg": "Download SVG" "svg": "Download SVG"
}, },
"resize": {
"zoom-in": "Zoom In",
"zoom-out": "Zoom Out"
},
"tabs": { "tabs": {
"preview": "Preview", "preview": "Preview",
"source": "Source" "source": "Source"
@@ -220,6 +234,7 @@
"message": { "message": {
"api.connection.failed": "Connection failed", "api.connection.failed": "Connection failed",
"api.connection.success": "Connection successful", "api.connection.success": "Connection successful",
"api.check.model.title": "Select the model to use for detection",
"assistant.added.content": "Assistant added successfully", "assistant.added.content": "Assistant added successfully",
"backup.failed": "Backup failed", "backup.failed": "Backup failed",
"backup.success": "Backup successful", "backup.success": "Backup successful",
@@ -230,6 +245,7 @@
"error.enter.api.key": "Please enter your API key first", "error.enter.api.key": "Please enter your API key first",
"error.enter.model": "Please select a model first", "error.enter.model": "Please select a model first",
"error.enter.name": "Please enter the name of the knowledge base", "error.enter.name": "Please enter the name of the knowledge base",
"error.chunk_overlap_too_large": "Chunk overlap cannot be greater than chunk size",
"error.invalid.proxy.url": "Invalid proxy URL", "error.invalid.proxy.url": "Invalid proxy URL",
"error.invalid.webdav": "Invalid WebDAV settings", "error.invalid.webdav": "Invalid WebDAV settings",
"message.code_style": "Code style", "message.code_style": "Code style",
@@ -238,22 +254,30 @@
"message.style": "Message style", "message.style": "Message style",
"message.style.bubble": "Bubble", "message.style.bubble": "Bubble",
"message.style.plain": "Plain", "message.style.plain": "Plain",
"message.multi_model_style": "Multi-model answer style",
"message.multi_model_style.horizontal": "Horizontal",
"message.multi_model_style.vertical": "Vertical",
"message.multi_model_style.fold": "Fold",
"reset.confirm.content": "Are you sure you want to clear all data?", "reset.confirm.content": "Are you sure you want to clear all data?",
"reset.double.confirm.content": "All data will be lost, do you want to continue?", "reset.double.confirm.content": "All data will be lost, do you want to continue?",
"reset.double.confirm.title": "DATA LOST !!!", "reset.double.confirm.title": "DATA LOST !!!",
"restore.success": "Restored successfully", "restore.success": "Restored successfully",
"save.success.title": "Saved successfully", "save.success.title": "Saved successfully",
"switch.disabled": "Switching is disabled while the assistant is generating", "switch.disabled": "Please wait for the current reply to complete",
"topic.added": "New topic added", "topic.added": "New topic added",
"upgrade.success.button": "Restart", "upgrade.success.button": "Restart",
"upgrade.success.content": "Please restart the application to complete the upgrade", "upgrade.success.content": "Please restart the application to complete the upgrade",
"upgrade.success.title": "Upgrade successfully", "upgrade.success.title": "Upgrade successfully",
"regenerate.confirm": "Regenerating will replace current message", "regenerate.confirm": "Regenerating will replace current message",
"copy.success": "Copied!", "copy.success": "Copied!",
"error.get_embedding_dimensions": "Failed to get embedding dimensions" "error.get_embedding_dimensions": "Failed to get embedding dimensions",
"group.delete.title": "Delete Group Message",
"group.delete.content": "Deleting a group message will delete the user's question and all assistant's answers"
}, },
"minapp": { "minapp": {
"title": "MinApp" "title": "MinApp",
"sidebar.add.title": "Add to sidebar",
"sidebar.remove.title": "Remove from sidebar"
}, },
"ollama": { "ollama": {
"keep_alive_time.description": "The time in minutes to keep the connection alive, default is 5 minutes.", "keep_alive_time.description": "The time in minutes to keep the connection alive, default is 5 minutes.",
@@ -278,7 +302,9 @@
"regenerate.confirm": "This will replace your existing generated images. Do you want to continue?", "regenerate.confirm": "This will replace your existing generated images. Do you want to continue?",
"seed": "Seed", "seed": "Seed",
"seed_tip": "The same seed and prompt can produce similar images", "seed_tip": "The same seed and prompt can produce similar images",
"title": "Images" "title": "Images",
"prompt_enhancement": "Prompt Enhancement",
"prompt_enhancement_tip": "Rewrite prompts into detailed, model-friendly versions when switched on"
}, },
"provider": { "provider": {
"aihubmix": "AiHubMix", "aihubmix": "AiHubMix",
@@ -310,7 +336,8 @@
"together": "Together", "together": "Together",
"yi": "Yi", "yi": "Yi",
"zhinao": "360AI", "zhinao": "360AI",
"zhipu": "ZHIPU AI" "zhipu": "ZHIPU AI",
"qwenlm": "QwenLM"
}, },
"settings": { "settings": {
"about": "About & Feedback", "about": "About & Feedback",
@@ -357,11 +384,25 @@
"webdav.password": "WebDAV Password", "webdav.password": "WebDAV Password",
"webdav.path": "WebDAV Path", "webdav.path": "WebDAV Path",
"webdav.path.placeholder": "/backup", "webdav.path.placeholder": "/backup",
"webdav.autoSync": "Auto Sync", "webdav.autoSync": "Auto Backup",
"webdav.minute": "Minute",
"webdav.minutes": "Minutes", "webdav.minutes": "Minutes",
"webdav.hour": "Hour",
"webdav.hours": "Hours",
"webdav.restore.button": "Restore from WebDAV", "webdav.restore.button": "Restore from WebDAV",
"webdav.title": "WebDAV", "webdav.title": "WebDAV",
"webdav.user": "WebDAV User" "webdav.user": "WebDAV User",
"webdav.syncStatus": "Backup Status",
"webdav.autoSync.off": "Off",
"webdav.noSync": "Waiting for next backup",
"webdav.syncError": "Backup Error",
"webdav.lastSync": "Last Backup"
},
"quickAssistant": {
"title": "Quick Assistant",
"click_tray_to_show": "Click the tray icon to start",
"enable_quick_assistant": "Enable Quick Assistant",
"use_shortcut_to_show": "Right-click the tray icon or use shortcuts to start"
}, },
"display.title": "Display Settings", "display.title": "Display Settings",
"font_size.title": "Message font size", "font_size.title": "Message font size",
@@ -377,9 +418,21 @@
"general.user_name.placeholder": "Enter your name", "general.user_name.placeholder": "Enter your name",
"general.view_webdav_settings": "View WebDAV settings", "general.view_webdav_settings": "View WebDAV settings",
"general.display.title": "Display Settings", "general.display.title": "Display Settings",
"display.sidebar.translate.icon": "Show Translate icon",
"display.sidebar.painting.icon": "Show Painting icon",
"display.sidebar.minapp.icon": "Show MinApp icon", "display.sidebar.minapp.icon": "Show MinApp icon",
"display.sidebar.knowledge.icon": "Show Knowledge icon",
"display.sidebar.files.icon": "Show Files icon", "display.sidebar.files.icon": "Show Files icon",
"display.sidebar.title": "Sidebar Settings", "display.sidebar.title": "Sidebar Settings",
"display.sidebar.visible": "Show icons",
"display.sidebar.disabled": "Hide icons",
"display.sidebar.chat.hiddenMessage": "Assistants are basic functions, not supported for hiding",
"display.sidebar.empty": "Drag the hidden feature from the left side here",
"display.minApp.title": "MinApp Settings",
"display.minApp.visible": "Visible MinApp",
"display.minApp.disabled": "Hidden MinApp",
"display.minApp.empty": "Drag minApp from the left to hide them here",
"": "MinApp that have been added to the sidebar do not support hiding. If you want to hide them, please remove them from the sidebar first.",
"display.topic.title": "Topic Settings", "display.topic.title": "Topic Settings",
"display.custom.css": "Custom CSS", "display.custom.css": "Custom CSS",
"display.custom.css.placeholder": "/* Put custom CSS here */", "display.custom.css.placeholder": "/* Put custom CSS here */",
@@ -390,7 +443,7 @@
"messages.input.show_estimated_tokens": "Show estimated tokens", "messages.input.show_estimated_tokens": "Show estimated tokens",
"messages.metrics": "{{time_first_token_millsec}}ms to first token | {{token_speed}} tok/sec", "messages.metrics": "{{time_first_token_millsec}}ms to first token | {{token_speed}} tok/sec",
"messages.input.title": "Input Settings", "messages.input.title": "Input Settings",
"messages.markdown_rendering_input_message": "Markdown render input msg", "messages.markdown_rendering_input_message": "Markdown render input message",
"messages.math_engine": "Math render engine", "messages.math_engine": "Math render engine",
"messages.model.title": "Model Settings", "messages.model.title": "Model Settings",
"messages.title": "Message Settings", "messages.title": "Message Settings",
@@ -417,6 +470,7 @@
"models.translate_model_prompt_title": "Translate Model Prompt", "models.translate_model_prompt_title": "Translate Model Prompt",
"models.topic_naming_model_setting_title": "Topic Naming Model Settings", "models.topic_naming_model_setting_title": "Topic Naming Model Settings",
"models.enable_topic_naming": "Topic Auto Naming", "models.enable_topic_naming": "Topic Auto Naming",
"models.topic_naming_prompt": "Topic Naming Prompt",
"provider": { "provider": {
"add.name": "Provider Name", "add.name": "Provider Name",
"add.name.placeholder": "Example: OpenAI", "add.name.placeholder": "Example: OpenAI",
@@ -483,7 +537,11 @@
"clear_shortcut": "Clear Shortcut", "clear_shortcut": "Clear Shortcut",
"toggle_show_assistants": "Toggle Assistants", "toggle_show_assistants": "Toggle Assistants",
"toggle_show_topics": "Toggle Topics", "toggle_show_topics": "Toggle Topics",
"copy_last_message": "Copy Last Message" "copy_last_message": "Copy Last Message",
"search_message": "Search Message",
"mini_window": "Quick Assistant",
"clear_topic": "Clear Messages",
"toggle_new_context": "Clear Context"
}, },
"theme.auto": "Auto", "theme.auto": "Auto",
"theme.dark": "Dark", "theme.dark": "Dark",
@@ -516,7 +574,8 @@
}, },
"tray": { "tray": {
"quit": "Quit", "quit": "Quit",
"show_window": "Show Window" "show_window": "Show Window",
"show_mini_window": "Quick Assistant"
}, },
"words": { "words": {
"knowledgeGraph": "Knowledge Graph", "knowledgeGraph": "Knowledge Graph",
@@ -524,7 +583,7 @@
"show_window": "Show Window", "show_window": "Show Window",
"quit": "Quit" "quit": "Quit"
}, },
"knowledge_base": { "knowledge": {
"title": "Knowledge Base", "title": "Knowledge Base",
"search": "Search knowledge base", "search": "Search knowledge base",
"empty": "No knowledge base found", "empty": "No knowledge base found",
@@ -566,7 +625,11 @@
"directory_placeholder": "Enter Directory Path", "directory_placeholder": "Enter Directory Path",
"model_info": "Model Info", "model_info": "Model Info",
"not_support": "Knowledge base database engine updated, the knowledge base will no longer be supported, please create a new knowledge base", "not_support": "Knowledge base database engine updated, the knowledge base will no longer be supported, please create a new knowledge base",
"source": "Source" "no_provider": "Knowledge base model provider is not set, the knowledge base will no longer be supported, please create a new knowledge base",
"source": "Source",
"chunk_size": "Chunk Size",
"chunk_overlap": "Chunk Overlap",
"not_set": "Not Set"
}, },
"models": { "models": {
"pinned": "Pinned", "pinned": "Pinned",
@@ -585,7 +648,44 @@
"embedding": "Embedding", "embedding": "Embedding",
"embedding_model": "Embedding Model", "embedding_model": "Embedding Model",
"embedding_model_tooltip": "Add in Settings->Model Provider->Manage", "embedding_model_tooltip": "Add in Settings->Model Provider->Manage",
"dimensions": "Dimensions {{dimensions}}" "dimensions": "Dimensions {{dimensions}}",
"custom_parameters": "Custom Parameters",
"add_parameter": "Add Parameter",
"parameter_name": "Parameter Name",
"parameter_type": {
"string": "Text",
"number": "Number",
"boolean": "Boolean",
"json": "JSON"
}
},
"prompts": {
"title": "You are an assistant who is good at conversation. You need to summarize the user's conversation into a title of 10 characters or less, ensuring it matches the user's primary language without using punctuation or other special symbols.",
"explanation": "Explain this concept to me",
"summarize": "Summarize this text"
},
"miniwindow": {
"feature": {
"chat": "Answer this question",
"translate": "Text translation",
"summary": "Content summary",
"explanation": "Explanation"
},
"clipboard": {
"empty": "Clipboard is empty"
},
"input": {
"placeholder": {
"title": "What do you want to do with this text?",
"empty": "Ask {{model}} for help..."
}
},
"footer": {
"esc": "Press ESC {{action}}",
"esc_close": "close the window",
"esc_back": "back",
"copy_last_message": "Press C to copy"
}
} }
} }
} }
+118 -16
View File
@@ -64,14 +64,14 @@
"default.description": "こんにちは、私はデフォルトのアシスタントです。すぐにチャットを始められます。", "default.description": "こんにちは、私はデフォルトのアシスタントです。すぐにチャットを始められます。",
"default.name": "⭐️ デフォルトアシスタント", "default.name": "⭐️ デフォルトアシスタント",
"default.topic.name": "デフォルトトピック", "default.topic.name": "デフォルトトピック",
"input.clear": "クリア", "input.clear": "クリア {{Command}}",
"input.clear.content": "現在のトピックのすべてのメッセージをクリアしますか?", "input.clear.content": "現在のトピックのすべてのメッセージをクリアしますか?",
"input.clear.title": "すべてのメッセージをクリアしますか?", "input.clear.title": "すべてのメッセージをクリアしますか?",
"input.collapse": "折りたたむ", "input.collapse": "折りたたむ",
"input.context_count.tip": "コンテキスト数", "input.context_count.tip": "コンテキスト数",
"input.estimated_tokens.tip": "推定トークン数", "input.estimated_tokens.tip": "推定トークン数",
"input.expand": "展開", "input.expand": "展開",
"input.new.context": "コンテキストをクリア", "input.new.context": "コンテキストをクリア {{Command}}",
"input.new_topic": "新しいトピック {{Command}}", "input.new_topic": "新しいトピック {{Command}}",
"input.pause": "一時停止", "input.pause": "一時停止",
"input.placeholder": "ここにメッセージを入力...", "input.placeholder": "ここにメッセージを入力...",
@@ -86,6 +86,7 @@
"message.new.branch.created": "新しいブランチが作成されました", "message.new.branch.created": "新しいブランチが作成されました",
"message.regenerate.model": "モデルを切り替え", "message.regenerate.model": "モデルを切り替え",
"message.new.context": "新しいコンテキスト", "message.new.context": "新しいコンテキスト",
"message.useful": "役立つ",
"save": "保存", "save": "保存",
"settings.code_collapsible": "コードブロックを折りたたむ", "settings.code_collapsible": "コードブロックを折りたたむ",
"settings.context_count": "コンテキスト", "settings.context_count": "コンテキスト",
@@ -112,7 +113,10 @@
"topics.list": "トピックリスト", "topics.list": "トピックリスト",
"topics.move_to": "移動先", "topics.move_to": "移動先",
"topics.title": "トピック", "topics.title": "トピック",
"translate": "翻訳" "translate": "翻訳",
"resend": "再送信",
"thinking": "思考中...",
"deeply_thought": "深く考えています({{secounds}} 秒)"
}, },
"common": { "common": {
"and": "と", "and": "と",
@@ -182,8 +186,14 @@
"name": "名前", "name": "名前",
"open": "開く", "open": "開く",
"size": "サイズ", "size": "サイズ",
"type": "タイプ",
"text": "テキスト", "text": "テキスト",
"title": "ファイル" "title": "ファイル",
"edit": "編集",
"delete": "削除",
"delete.title": "ファイルを削除",
"delete.content": "ファイルを削除すると、ファイルがすべてのメッセージで参照されることを削除します。このファイルを削除してもよろしいですか?",
"delete.paintings.warning": "画像に含まれているため、削除できません"
}, },
"history": { "history": {
"continue_chat": "チャットを続ける", "continue_chat": "チャットを続ける",
@@ -211,6 +221,10 @@
"png": "PNGをダウンロード", "png": "PNGをダウンロード",
"svg": "SVGをダウンロード" "svg": "SVGをダウンロード"
}, },
"resize": {
"zoom-in": "拡大する",
"zoom-out": "ズームアウト"
},
"tabs": { "tabs": {
"preview": "プレビュー", "preview": "プレビュー",
"source": "ソース" "source": "ソース"
@@ -220,6 +234,7 @@
"message": { "message": {
"api.connection.failed": "接続に失敗しました", "api.connection.failed": "接続に失敗しました",
"api.connection.success": "接続に成功しました", "api.connection.success": "接続に成功しました",
"api.check.model.title": "検出に使用するモデルを選択してください",
"assistant.added.content": "アシスタントが追加されました", "assistant.added.content": "アシスタントが追加されました",
"backup.failed": "バックアップに失敗しました", "backup.failed": "バックアップに失敗しました",
"backup.success": "バックアップに成功しました", "backup.success": "バックアップに成功しました",
@@ -229,6 +244,7 @@
"error.enter.api.host": "APIホストを入力してください", "error.enter.api.host": "APIホストを入力してください",
"error.enter.api.key": "APIキーを入力してください", "error.enter.api.key": "APIキーを入力してください",
"error.enter.model": "モデルを選択してください", "error.enter.model": "モデルを選択してください",
"error.chunk_overlap_too_large": "チャンクの重なりは、チャンクサイズを超えることはできません",
"error.invalid.proxy.url": "無効なプロキシURL", "error.invalid.proxy.url": "無効なプロキシURL",
"error.invalid.webdav": "無効なWebDAV設定", "error.invalid.webdav": "無効なWebDAV設定",
"message.code_style": "コードスタイル", "message.code_style": "コードスタイル",
@@ -237,21 +253,30 @@
"message.style": "メッセージスタイル", "message.style": "メッセージスタイル",
"message.style.bubble": "バブル", "message.style.bubble": "バブル",
"message.style.plain": "プレーン", "message.style.plain": "プレーン",
"message.multi_model_style": "複数モデル回答スタイル",
"message.multi_model_style.horizontal": "水平",
"message.multi_model_style.vertical": "垂直",
"message.multi_model_style.fold": "折りたたむ",
"reset.confirm.content": "すべてのデータをリセットしてもよろしいですか?", "reset.confirm.content": "すべてのデータをリセットしてもよろしいですか?",
"reset.double.confirm.content": "すべてのデータが失われます。続行しますか?", "reset.double.confirm.content": "すべてのデータが失われます。続行しますか?",
"reset.double.confirm.title": "データが失われます!!!", "reset.double.confirm.title": "データが失われます!!!",
"restore.success": "復元に成功しました", "restore.success": "復元に成功しました",
"save.success.title": "保存に成功しました", "save.success.title": "保存に成功しました",
"switch.disabled": "アシスタントが生成中は切り替え無効す", "switch.disabled": "現在の応答が完了するまで切り替え無効にします",
"topic.added": "新しいトピックが追加されました", "topic.added": "新しいトピックが追加されました",
"upgrade.success.button": "再起動", "upgrade.success.button": "再起動",
"upgrade.success.content": "アップグレードを完了するためにアプリケーションを再起動してください", "upgrade.success.content": "アップグレードを完了するためにアプリケーションを再起動してください",
"upgrade.success.title": "アップグレードに成功しました", "upgrade.success.title": "アップグレードに成功しました",
"regenerate.confirm": "再生成すると現在のメッセージが置き換えられます", "regenerate.confirm": "再生成すると現在のメッセージが置き換えられます",
"copy.success": "コピーしました!" "copy.success": "コピーしました!",
"error.get_embedding_dimensions": "埋込み次元を取得できませんでした",
"group.delete.title": "分組メッセージを削除",
"group.delete.content": "分組メッセージを削除するとユーザーの質問と助け手の回答がすべて削除されます"
}, },
"minapp": { "minapp": {
"title": "ミニアプリ" "title": "ミニアプリ",
"sidebar.add.title": "サイドバーに追加",
"sidebar.remove.title": "サイドバーから削除"
}, },
"ollama": { "ollama": {
"keep_alive_time.description": "モデルがメモリに保持される時間(デフォルト:5分)", "keep_alive_time.description": "モデルがメモリに保持される時間(デフォルト:5分)",
@@ -276,7 +301,9 @@
"regenerate.confirm": "これにより、既存の生成画像が置き換えられます。続行しますか?", "regenerate.confirm": "これにより、既存の生成画像が置き換えられます。続行しますか?",
"seed": "シード", "seed": "シード",
"seed_tip": "同じシードとプロンプトで似た画像を生成できます", "seed_tip": "同じシードとプロンプトで似た画像を生成できます",
"title": "画像" "title": "画像",
"prompt_enhancement": "プロンプト強化",
"prompt_enhancement_tip": "オンにすると、プロンプトを詳細でモデルに適したバージョンに書き直します"
}, },
"provider": { "provider": {
"aihubmix": "AiHubMix", "aihubmix": "AiHubMix",
@@ -308,7 +335,8 @@
"together": "Together", "together": "Together",
"yi": "零一万物", "yi": "零一万物",
"zhinao": "360智脳", "zhinao": "360智脳",
"zhipu": "智譜AI" "zhipu": "智譜AI",
"qwenlm": "QwenLM"
}, },
"settings": { "settings": {
"about": "について", "about": "について",
@@ -355,11 +383,23 @@
"webdav.password": "WebDAVパスワード", "webdav.password": "WebDAVパスワード",
"webdav.path": "WebDAVパス", "webdav.path": "WebDAVパス",
"webdav.path.placeholder": "/backup", "webdav.path.placeholder": "/backup",
"webdav.autoSync": "自動同期", "webdav.autoSync": "自動バックアップ",
"webdav.minutes": "分", "webdav.minutes": "分",
"webdav.hours": "時間",
"webdav.restore.button": "WebDAVから復元", "webdav.restore.button": "WebDAVから復元",
"webdav.title": "WebDAV", "webdav.title": "WebDAV",
"webdav.user": "WebDAVユーザー" "webdav.user": "WebDAVユーザー",
"webdav.syncStatus": "バックアップ状態",
"webdav.autoSync.off": "オフ",
"webdav.noSync": "次回のバックアップを待っています",
"webdav.syncError": "バックアップエラー",
"webdav.lastSync": "最終同期"
},
"quickAssistant": {
"title": "クイックアシスタント",
"click_tray_to_show": "トレイアイコンをクリックして起動",
"enable_quick_assistant": "クイックアシスタントを有効にする",
"use_shortcut_to_show": "トレイアイコンを右クリックするか、ショートカットキーで起動できます"
}, },
"display.title": "表示設定", "display.title": "表示設定",
"font_size.title": "メッセージのフォントサイズ", "font_size.title": "メッセージのフォントサイズ",
@@ -375,12 +415,23 @@
"general.user_name.placeholder": "ユーザー名を入力", "general.user_name.placeholder": "ユーザー名を入力",
"general.view_webdav_settings": "WebDAV設定を表示", "general.view_webdav_settings": "WebDAV設定を表示",
"general.display.title": "表示設定", "general.display.title": "表示設定",
"display.sidebar.translate.icon": "翻訳のアイコンを表示",
"display.sidebar.painting.icon": "絵画のアイコンを表示",
"display.sidebar.minapp.icon": "ミニアプリのアイコンを表示", "display.sidebar.minapp.icon": "ミニアプリのアイコンを表示",
"display.sidebar.knowledge.icon": "ナレッジのアイコンを表示",
"display.sidebar.files.icon": "ファイルのアイコンを表示", "display.sidebar.files.icon": "ファイルのアイコンを表示",
"display.sidebar.title": "サイドバー設定", "display.sidebar.title": "サイドバー設定",
"display.sidebar.visible": "アイコンを表示",
"display.sidebar.disabled": "アイコンを非表示",
"display.sidebar.chat.hiddenMessage": "アシスタントは基本的な機能であり、非表示はサポートされていません",
"display.sidebar.empty": "非表示にする機能を左側からここにドラッグ",
"display.topic.title": "トピック設定", "display.topic.title": "トピック設定",
"display.custom.css": "カスタムCSS", "display.custom.css": "カスタムCSS",
"display.custom.css.placeholder": "/* ここにカスタムCSSを入力 */", "display.custom.css.placeholder": "/* ここにカスタムCSSを入力 */",
"display.minApp.title": "ミニプログラム表示設定",
"display.minApp.visible": "表示中ミニプログラム",
"display.minApp.disabled": "非表示ミニプログラム",
"display.minApp.empty": "非表示にしたいアプレットを左からここまでドラッグします",
"input.auto_translate_with_space": "スペースを3回押して翻訳", "input.auto_translate_with_space": "スペースを3回押して翻訳",
"messages.divider": "メッセージ間に区切り線を表示", "messages.divider": "メッセージ間に区切り線を表示",
"messages.input.paste_long_text_as_file": "長いテキストをファイルとして貼り付け", "messages.input.paste_long_text_as_file": "長いテキストをファイルとして貼り付け",
@@ -415,6 +466,7 @@
"models.translate_model_prompt_title": "翻訳モデルのプロンプト", "models.translate_model_prompt_title": "翻訳モデルのプロンプト",
"models.topic_naming_model_setting_title": "トピック命名モデルの設定", "models.topic_naming_model_setting_title": "トピック命名モデルの設定",
"models.enable_topic_naming": "トピックの自動命名", "models.enable_topic_naming": "トピックの自動命名",
"models.topic_naming_prompt": "トピック命名プロンプト",
"provider": { "provider": {
"add.name": "プロバイダー名", "add.name": "プロバイダー名",
"add.name.placeholder": "例:OpenAI", "add.name.placeholder": "例:OpenAI",
@@ -469,7 +521,11 @@
"clear_shortcut": "ショートカットをクリア", "clear_shortcut": "ショートカットをクリア",
"toggle_show_assistants": "アシスタントの表示を切り替え", "toggle_show_assistants": "アシスタントの表示を切り替え",
"toggle_show_topics": "トピックの表示を切り替え", "toggle_show_topics": "トピックの表示を切り替え",
"copy_last_message": "最後のメッセージをコピー" "copy_last_message": "最後のメッセージをコピー",
"search_message": "メッセージを検索",
"mini_window": "クイックアシスタント",
"clear_topic": "メッセージを消去",
"toggle_new_context": "コンテキストをクリア"
}, },
"theme.auto": "自動", "theme.auto": "自動",
"theme.dark": "ダークテーマ", "theme.dark": "ダークテーマ",
@@ -502,7 +558,8 @@
}, },
"tray": { "tray": {
"quit": "終了", "quit": "終了",
"show_window": "ウィンドウを表示" "show_window": "ウィンドウを表示",
"show_mini_window": "クイックアシスタント"
}, },
"words": { "words": {
"knowledgeGraph": "ナレッジグラフ", "knowledgeGraph": "ナレッジグラフ",
@@ -510,7 +567,7 @@
"show_window": "ウィンドウを表示", "show_window": "ウィンドウを表示",
"quit": "終了" "quit": "終了"
}, },
"knowledge_base": { "knowledge": {
"title": "ナレッジベース", "title": "ナレッジベース",
"search": "ナレッジベースを検索", "search": "ナレッジベースを検索",
"empty": "ナレッジベースが見つかりません", "empty": "ナレッジベースが見つかりません",
@@ -549,7 +606,14 @@
"sitemap_placeholder": "サイトマップURLを入力", "sitemap_placeholder": "サイトマップURLを入力",
"directories": "ディレクトリ", "directories": "ディレクトリ",
"add_directory": "ディレクトリを追加", "add_directory": "ディレクトリを追加",
"directory_placeholder": "ディレクトリパスを入力" "directory_placeholder": "ディレクトリパスを入力",
"model_info": "モデル情報",
"not_support": "ナレッジベースデータベースエンジンが更新されました。このナレッジベースはもうサポートされていません。新しいナレッジベースを作成してください",
"no_provider": "ナレッジベースモデルプロバイダーが設定されていません。ナレッジベースはもうサポートされていません。新しいナレッジベースを作成してください",
"source": "ソース",
"chunk_size": "チャンクサイズ",
"chunk_overlap": "チャンクの重なり",
"not_set": "未設定"
}, },
"models": { "models": {
"pinned": "固定済み", "pinned": "固定済み",
@@ -567,7 +631,45 @@
"free": "無料モデル", "free": "無料モデル",
"embedding": "埋め込みモデル", "embedding": "埋め込みモデル",
"embedding_model": "埋め込みモデル", "embedding_model": "埋め込みモデル",
"embedding_model_tooltip": "設定->モデルサービス->管理で追加" "embedding_model_tooltip": "設定->モデルサービス->管理で追加",
"dimensions": "{{dimensions}} 次元",
"custom_parameters": "カスタムパラメータ",
"add_parameter": "パラメータを追加",
"parameter_name": "パラメータ名",
"parameter_type": {
"string": "テキスト",
"number": "数値",
"boolean": "真偽値",
"json": "JSON"
}
},
"prompts": {
"title": "あなたは会話を得意とするアシスタントです。ユーザーの会話を10文字以内のタイトルに要約し、ユーザーの主言語と一致していることを確認してください。句読点や特殊記号は使用しないでください。",
"explanation": "この概念を説明してください",
"summarize": "このテキストを要約してください"
},
"miniwindow": {
"feature": {
"chat": "この質問に回答",
"translate": "テキスト翻訳",
"summary": "内容要約",
"explanation": "説明"
},
"clipboard": {
"empty": "クリップボードが空です"
},
"input": {
"placeholder": {
"title": "下のテキストに対して何をしますか?",
"empty": "{{model}} に質問してください..."
}
},
"footer": {
"esc": "ESC キーを押して{{action}}",
"esc_close": "ウィンドウを閉じる",
"esc_back": "戻る",
"copy_last_message": "C キーを押してコピー"
}
} }
} }
} }
+113 -16
View File
@@ -64,14 +64,14 @@
"default.description": "Привет, я Ассистент по умолчанию. Вы можете начать общаться со мной прямо сейчас", "default.description": "Привет, я Ассистент по умолчанию. Вы можете начать общаться со мной прямо сейчас",
"default.name": "⭐️ Ассистент по умолчанию", "default.name": "⭐️ Ассистент по умолчанию",
"default.topic.name": "Топик по умолчанию", "default.topic.name": "Топик по умолчанию",
"input.clear": "Очистить", "input.clear": "Очистить {{Command}}",
"input.clear.content": "Хотите очистить все сообщения текущего топика?", "input.clear.content": "Хотите очистить все сообщения текущего топика?",
"input.clear.title": "Очистить все сообщения?", "input.clear.title": "Очистить все сообщения?",
"input.collapse": "Свернуть", "input.collapse": "Свернуть",
"input.context_count.tip": "Количество контекстов", "input.context_count.tip": "Количество контекстов",
"input.estimated_tokens.tip": "Затраты токенов", "input.estimated_tokens.tip": "Затраты токенов",
"input.expand": "Развернуть", "input.expand": "Развернуть",
"input.new.context": "Очистить контекст", "input.new.context": "Очистить контекст {{Command}}",
"input.new_topic": "Новый топик {{Command}}", "input.new_topic": "Новый топик {{Command}}",
"input.pause": "Остановить", "input.pause": "Остановить",
"input.placeholder": "Введите ваше сообщение здесь...", "input.placeholder": "Введите ваше сообщение здесь...",
@@ -86,6 +86,7 @@
"message.new.branch.created": "Новая ветка создана", "message.new.branch.created": "Новая ветка создана",
"message.regenerate.model": "Переключить модель", "message.regenerate.model": "Переключить модель",
"message.new.context": "Новый контекст", "message.new.context": "Новый контекст",
"message.useful": "Полезно",
"save": "Сохранить", "save": "Сохранить",
"settings.code_collapsible": "Блок кода свернут", "settings.code_collapsible": "Блок кода свернут",
"settings.context_count": "Контекст", "settings.context_count": "Контекст",
@@ -112,7 +113,10 @@
"topics.list": "Список топиков", "topics.list": "Список топиков",
"topics.move_to": "Переместить в", "topics.move_to": "Переместить в",
"topics.title": "Топики", "topics.title": "Топики",
"translate": "Перевести" "translate": "Перевести",
"resend": "Переотправить",
"thinking": "Мыслим",
"deeply_thought": "Мыслим ({{secounds}} секунд)"
}, },
"common": { "common": {
"and": "и", "and": "и",
@@ -182,8 +186,14 @@
"name": "Имя", "name": "Имя",
"open": "Открыть", "open": "Открыть",
"size": "Размер", "size": "Размер",
"type": "Тип",
"text": "Текст", "text": "Текст",
"title": "Файлы" "title": "Файлы",
"edit": "Редактировать",
"delete": "Удалить",
"delete.title": "Удалить файл",
"delete.content": "Удаление файла удалит его из всех сообщений, вы уверены, что хотите удалить этот файл?",
"delete.paintings.warning": "В изображениях содержится этот файл, удаление невозможно"
}, },
"history": { "history": {
"continue_chat": "Продолжить чат", "continue_chat": "Продолжить чат",
@@ -211,6 +221,10 @@
"png": "Скачать PNG", "png": "Скачать PNG",
"svg": "Скачать SVG" "svg": "Скачать SVG"
}, },
"resize": {
"zoom-in": "Yвеличить",
"zoom-out": "Yменьшить масштаб"
},
"tabs": { "tabs": {
"preview": "Предпросмотр", "preview": "Предпросмотр",
"source": "Исходный код" "source": "Исходный код"
@@ -220,6 +234,7 @@
"message": { "message": {
"api.connection.failed": "Соединение не удалось", "api.connection.failed": "Соединение не удалось",
"api.connection.success": "Соединение успешно", "api.connection.success": "Соединение успешно",
"api.check.model.title": "Выберите модель для проверки",
"assistant.added.content": "Ассистент успешно добавлен", "assistant.added.content": "Ассистент успешно добавлен",
"backup.failed": "Создание резервной копии не удалось", "backup.failed": "Создание резервной копии не удалось",
"backup.success": "Резервная копия успешно создана", "backup.success": "Резервная копия успешно создана",
@@ -230,6 +245,7 @@
"error.enter.api.key": "Пожалуйста, введите ваш API ключ", "error.enter.api.key": "Пожалуйста, введите ваш API ключ",
"error.enter.model": "Пожалуйста, выберите модель", "error.enter.model": "Пожалуйста, выберите модель",
"error.enter.name": "Пожалуйста, введите название базы знаний", "error.enter.name": "Пожалуйста, введите название базы знаний",
"error.chunk_overlap_too_large": "Перекрытие фрагментов не может быть больше размера фрагмента.",
"error.invalid.proxy.url": "Неверный URL прокси", "error.invalid.proxy.url": "Неверный URL прокси",
"error.invalid.webdav": "Неверные настройки WebDAV", "error.invalid.webdav": "Неверные настройки WebDAV",
"message.code_style": "Стиль кода", "message.code_style": "Стиль кода",
@@ -238,22 +254,30 @@
"message.style": "Стиль сообщения", "message.style": "Стиль сообщения",
"message.style.bubble": "Пузырь", "message.style.bubble": "Пузырь",
"message.style.plain": "Простой", "message.style.plain": "Простой",
"message.multi_model_style": "Стиль ответов от нескольких моделей",
"message.multi_model_style.horizontal": "Горизонтальный",
"message.multi_model_style.vertical": "Вертикальный",
"message.multi_model_style.fold": "Свернуть",
"reset.confirm.content": "Вы уверены, что хотите очистить все данные?", "reset.confirm.content": "Вы уверены, что хотите очистить все данные?",
"reset.double.confirm.content": "Все данные будут утеряны, хотите продолжить?", "reset.double.confirm.content": "Все данные будут утеряны, хотите продолжить?",
"reset.double.confirm.title": "ДАННЫЕ БУДУТ УТЕРЯНЫ !!!", "reset.double.confirm.title": "ДАННЫЕ БУДУТ УТЕРЯНЫ !!!",
"restore.success": "Успешно восстановлено", "restore.success": "Успешно восстановлено",
"save.success.title": "Успешно сохранено", "save.success.title": "Успешно сохранено",
"switch.disabled": ереключение отключено, пока ассистент генерирует", "switch.disabled": ожалуйста, дождитесь завершения текущего ответа",
"topic.added": "Новый топик добавлен", "topic.added": "Новый топик добавлен",
"upgrade.success.button": "Перезапустить", "upgrade.success.button": "Перезапустить",
"upgrade.success.content": "Пожалуйста, перезапустите приложение для завершения обновления", "upgrade.success.content": "Пожалуйста, перезапустите приложение для завершения обновления",
"upgrade.success.title": "Обновление успешно", "upgrade.success.title": "Обновление успешно",
"regenerate.confirm": "Перегенерация заменит текущее сообщение", "regenerate.confirm": "Перегенерация заменит текущее сообщение",
"copy.success": "Скопировано!", "copy.success": "Скопировано!",
"error.get_embedding_dimensions": "Не удалось получить размерность встраивания" "error.get_embedding_dimensions": "Не удалось получить размерность встраивания",
"group.delete.title": "Удалить группу сообщений",
"group.delete.content": "Удаление группы сообщений удалит пользовательский вопрос и все ответы помощника"
}, },
"minapp": { "minapp": {
"title": "Встроенные приложения" "title": "Встроенные приложения",
"sidebar.add.title": "Добавить в боковую панель",
"sidebar.remove.title": "Удалить из боковой панели"
}, },
"ollama": { "ollama": {
"keep_alive_time.description": "Время в минутах, в течение которого модель остается активной, по умолчанию 5 минут.", "keep_alive_time.description": "Время в минутах, в течение которого модель остается активной, по умолчанию 5 минут.",
@@ -278,7 +302,9 @@
"regenerate.confirm": "Это заменит ваши существующие сгенерированные изображения. Хотите продолжить?", "regenerate.confirm": "Это заменит ваши существующие сгенерированные изображения. Хотите продолжить?",
"seed": "Ключ генерации", "seed": "Ключ генерации",
"seed_tip": "Одинаковый ключ генерации и промпт могут производить похожие изображения", "seed_tip": "Одинаковый ключ генерации и промпт могут производить похожие изображения",
"title": "Изображения" "title": "Изображения",
"prompt_enhancement": "Улучшение промпта",
"prompt_enhancement_tip": "При включении переписывает промпт в более детальную, модель-ориентированную версию"
}, },
"provider": { "provider": {
"aihubmix": "AiHubMix", "aihubmix": "AiHubMix",
@@ -310,7 +336,8 @@
"together": "Together", "together": "Together",
"yi": "Yi", "yi": "Yi",
"zhinao": "360AI", "zhinao": "360AI",
"zhipu": "ZHIPU AI" "zhipu": "ZHIPU AI",
"qwenlm": "QwenLM"
}, },
"settings": { "settings": {
"about": "О программе и обратная связь", "about": "О программе и обратная связь",
@@ -357,11 +384,23 @@
"webdav.password": "Пароль WebDAV", "webdav.password": "Пароль WebDAV",
"webdav.path": "Путь WebDAV", "webdav.path": "Путь WebDAV",
"webdav.path.placeholder": "/backup", "webdav.path.placeholder": "/backup",
"webdav.autoSync": "Автоматическая синхронизация", "webdav.autoSync": "Автоматическое резервное копирование",
"webdav.minutes": "минут", "webdav.minutes": "минут",
"webdav.hours": "часов",
"webdav.restore.button": "Восстановление с WebDAV", "webdav.restore.button": "Восстановление с WebDAV",
"webdav.title": "WebDAV", "webdav.title": "WebDAV",
"webdav.user": "Пользователь WebDAV" "webdav.user": "Пользователь WebDAV",
"webdav.syncStatus": "Статус резервного копирования",
"webdav.autoSync.off": "Выключено",
"webdav.noSync": "Ожидание следующего резервного копирования",
"webdav.syncError": "Ошибка резервного копирования",
"webdav.lastSync": "Последняя синхронизация"
},
"quickAssistant": {
"title": "Быстрый помощник",
"click_tray_to_show": "Нажмите на иконку трея для запуска",
"enable_quick_assistant": "Включить быстрый помощник",
"use_shortcut_to_show": "Нажмите на иконку трея или используйте горячие клавиши для запуска"
}, },
"display.title": "Настройки отображения", "display.title": "Настройки отображения",
"font_size.title": "Размер шрифта сообщений", "font_size.title": "Размер шрифта сообщений",
@@ -377,9 +416,20 @@
"general.user_name.placeholder": "Введите ваше имя", "general.user_name.placeholder": "Введите ваше имя",
"general.view_webdav_settings": "Просмотр настроек WebDAV", "general.view_webdav_settings": "Просмотр настроек WebDAV",
"general.display.title": "Настройки отображения", "general.display.title": "Настройки отображения",
"display.sidebar.translate.icon": "Показывать иконку перевода",
"display.sidebar.painting.icon": "Показывать иконку рисования",
"display.sidebar.minapp.icon": "Показывать иконку мини-приложения", "display.sidebar.minapp.icon": "Показывать иконку мини-приложения",
"display.sidebar.knowledge.icon": "Показывать иконку знаний",
"display.sidebar.files.icon": "Показывать иконку файлов", "display.sidebar.files.icon": "Показывать иконку файлов",
"display.sidebar.title": "Настройки боковой панели", "display.sidebar.title": "Настройки боковой панели",
"display.sidebar.visible": "Показывать иконки",
"display.sidebar.disabled": "Скрыть иконки",
"display.sidebar.chat.hiddenMessage": "Помощник является базовой функцией и не поддерживает скрытие",
"display.sidebar.empty": "Перетащите скрываемую функцию с левой стороны сюда",
"display.minApp.title": "Настройки отображения мини программы",
"display.minApp.visible": "Отображаемый апплет",
"display.minApp.disabled": "скрытый апплет",
"display.minApp.empty": "Перетащите апплет, который хотите скрыть, слева сюда",
"display.topic.title": "Настройки топиков", "display.topic.title": "Настройки топиков",
"display.custom.css": "Пользовательский CSS", "display.custom.css": "Пользовательский CSS",
"display.custom.css.placeholder": "/* Здесь введите пользовательский CSS */", "display.custom.css.placeholder": "/* Здесь введите пользовательский CSS */",
@@ -417,6 +467,7 @@
"models.translate_model_prompt_title": "Модель перевода", "models.translate_model_prompt_title": "Модель перевода",
"models.topic_naming_model_setting_title": "Настройки модели именования топика", "models.topic_naming_model_setting_title": "Настройки модели именования топика",
"models.enable_topic_naming": "Автоматическое переименование топика", "models.enable_topic_naming": "Автоматическое переименование топика",
"models.topic_naming_prompt": "Подсказка для именования топика",
"provider": { "provider": {
"add.name": "Имя провайдера", "add.name": "Имя провайдера",
"add.name.placeholder": "Пример: OpenAI", "add.name.placeholder": "Пример: OpenAI",
@@ -483,7 +534,11 @@
"clear_shortcut": "Очистить сочетание клавиш", "clear_shortcut": "Очистить сочетание клавиш",
"toggle_show_assistants": "Переключить отображение ассистентов", "toggle_show_assistants": "Переключить отображение ассистентов",
"toggle_show_topics": "Переключить отображение топиков", "toggle_show_topics": "Переключить отображение топиков",
"copy_last_message": "Копировать последнее сообщение" "copy_last_message": "Копировать последнее сообщение",
"search_message": "Поиск сообщения",
"mini_window": "Быстрый помощник",
"clear_topic": "Очистить все сообщения",
"toggle_new_context": "Очистить контекст"
}, },
"theme.auto": "Автоматически", "theme.auto": "Автоматически",
"theme.dark": "Темная", "theme.dark": "Темная",
@@ -516,7 +571,8 @@
}, },
"tray": { "tray": {
"quit": "Выйти", "quit": "Выйти",
"show_window": "Показать окно" "show_window": "Показать окно",
"show_mini_window": "Быстрый помощник"
}, },
"words": { "words": {
"knowledgeGraph": "Граф знаний", "knowledgeGraph": "Граф знаний",
@@ -524,7 +580,7 @@
"show_window": "Показать окно", "show_window": "Показать окно",
"quit": "Выйти" "quit": "Выйти"
}, },
"knowledge_base": { "knowledge": {
"title": "База знаний", "title": "База знаний",
"search": "Поиск в базе знаний", "search": "Поиск в базе знаний",
"empty": "База знаний не найдена", "empty": "База знаний не найдена",
@@ -566,7 +622,11 @@
"directory_placeholder": "Введите путь к директории", "directory_placeholder": "Введите путь к директории",
"model_info": "Модель информации", "model_info": "Модель информации",
"not_support": "База знаний базы данных движок обновлен, база знаний больше не поддерживается, пожалуйста, создайте новую базу знаний", "not_support": "База знаний базы данных движок обновлен, база знаний больше не поддерживается, пожалуйста, создайте новую базу знаний",
"source": "Источник" "no_provider": "База знаний модель поставщика не настроена, база знаний больше не поддерживается, пожалуйста, создайте новую базу знаний",
"source": "Источник",
"chunk_size": "Размер фрагмента",
"chunk_overlap": "Перекрытие фрагмента",
"not_set": "Не установлено"
}, },
"models": { "models": {
"pinned": "Закреплено", "pinned": "Закреплено",
@@ -585,7 +645,44 @@
"embedding": "Встраиваемые модели", "embedding": "Встраиваемые модели",
"embedding_model": "Встраиваемые модели", "embedding_model": "Встраиваемые модели",
"embedding_model_tooltip": "Добавьте в настройки->модель сервиса->управление", "embedding_model_tooltip": "Добавьте в настройки->модель сервиса->управление",
"dimensions": "{{dimensions}} мер" "dimensions": "{{dimensions}} мер",
"custom_parameters": "Пользовательские параметры",
"add_parameter": "Добавить параметр",
"parameter_name": "Имя параметра",
"parameter_type": {
"string": "Текст",
"number": "Число",
"boolean": "Логическое",
"json": "JSON"
}
},
"prompts": {
"title": "Вы - эксперт в общении, который суммирует разговоры пользователя в 10-символьном заголовке, совпадающем с языком пользователя, без использования знаков препинания и других специальных символов",
"explanation": "Объясните мне этот концепт",
"summarize": "Суммируйте этот текст"
},
"miniwindow": {
"feature": {
"chat": "Ответить на этот вопрос",
"translate": "Текст перевод",
"summary": "Содержание",
"explanation": "Объяснение"
},
"clipboard": {
"empty": "Буфер обмена пуст"
},
"input": {
"placeholder": {
"title": "Что вы хотите сделать с этим текстом?",
"empty": "Задайте вопрос {{model}}..."
}
},
"footer": {
"esc": "Нажмите ESC {{action}}",
"esc_close": "закрытия окна",
"esc_back": "возвращения",
"copy_last_message": "Нажмите C для копирования"
}
} }
} }
} }
+114 -17
View File
@@ -64,14 +64,14 @@
"default.description": "你好,我是默认助手。你可以立刻开始跟我聊天。", "default.description": "你好,我是默认助手。你可以立刻开始跟我聊天。",
"default.name": "⭐️ 默认助手", "default.name": "⭐️ 默认助手",
"default.topic.name": "默认话题", "default.topic.name": "默认话题",
"input.clear": "清空消息", "input.clear": "清空消息 {{Command}}",
"input.clear.content": "确定要清除当前会话所有消息吗?", "input.clear.content": "确定要清除当前会话所有消息吗?",
"input.clear.title": "清空消息", "input.clear.title": "清空消息",
"input.collapse": "收起", "input.collapse": "收起",
"input.context_count.tip": "上下文数", "input.context_count.tip": "上下文数",
"input.estimated_tokens.tip": "预估 token 数", "input.estimated_tokens.tip": "预估 token 数",
"input.expand": "展开", "input.expand": "展开",
"input.new.context": "清除上下文", "input.new.context": "清除上下文 {{Command}}",
"input.new_topic": "新话题 {{Command}}", "input.new_topic": "新话题 {{Command}}",
"input.pause": "暂停", "input.pause": "暂停",
"input.placeholder": "在这里输入消息...", "input.placeholder": "在这里输入消息...",
@@ -82,10 +82,11 @@
"input.upload": "上传图片或文档", "input.upload": "上传图片或文档",
"input.web_search": "开启网络搜索", "input.web_search": "开启网络搜索",
"input.knowledge_base": "知识库", "input.knowledge_base": "知识库",
"message.new.branch": "分支", "message.new.branch": "分支",
"message.new.branch.created": "新分支已创建", "message.new.branch.created": "新分支已创建",
"message.regenerate.model": "切换模型", "message.regenerate.model": "切换模型",
"message.new.context": "清除上下文", "message.new.context": "清除上下文",
"message.useful": "有用",
"save": "保存", "save": "保存",
"settings.code_collapsible": "代码块可折叠", "settings.code_collapsible": "代码块可折叠",
"settings.context_count": "上下文数", "settings.context_count": "上下文数",
@@ -112,7 +113,10 @@
"topics.list": "话题列表", "topics.list": "话题列表",
"topics.move_to": "移动到", "topics.move_to": "移动到",
"topics.title": "话题", "topics.title": "话题",
"translate": "翻译" "translate": "翻译",
"resend": "重新发送",
"thinking": "思考中",
"deeply_thought": "已深度思考(用时 {{secounds}} 秒)"
}, },
"common": { "common": {
"and": "和", "and": "和",
@@ -183,8 +187,14 @@
"name": "文件名", "name": "文件名",
"open": "打开", "open": "打开",
"size": "大小", "size": "大小",
"type": "类型",
"text": "文本", "text": "文本",
"title": "文件" "title": "文件",
"edit": "编辑",
"delete": "删除",
"delete.title": "删除文件",
"delete.content": "删除文件会删除文件在所有消息中的引用,确定要删除此文件吗?",
"delete.paintings.warning": "绘图中包含该图片,暂时无法删除"
}, },
"history": { "history": {
"continue_chat": "继续聊天", "continue_chat": "继续聊天",
@@ -212,6 +222,10 @@
"png": "下载 PNG", "png": "下载 PNG",
"svg": "下载 SVG" "svg": "下载 SVG"
}, },
"resize": {
"zoom-in": "放大",
"zoom-out": "缩小"
},
"tabs": { "tabs": {
"preview": "预览", "preview": "预览",
"source": "源码" "source": "源码"
@@ -221,6 +235,7 @@
"message": { "message": {
"api.connection.failed": "连接失败", "api.connection.failed": "连接失败",
"api.connection.success": "连接成功", "api.connection.success": "连接成功",
"api.check.model.title": "请选择要检测的模型",
"assistant.added.content": "智能体添加成功", "assistant.added.content": "智能体添加成功",
"backup.failed": "备份失败", "backup.failed": "备份失败",
"backup.success": "备份成功", "backup.success": "备份成功",
@@ -231,6 +246,7 @@
"error.enter.api.key": "请输入您的 API 密钥", "error.enter.api.key": "请输入您的 API 密钥",
"error.enter.model": "请选择一个模型", "error.enter.model": "请选择一个模型",
"error.enter.name": "请输入知识库名称", "error.enter.name": "请输入知识库名称",
"error.chunk_overlap_too_large": "分段重叠不能大于分段大小",
"error.invalid.proxy.url": "无效的代理地址", "error.invalid.proxy.url": "无效的代理地址",
"error.invalid.webdav": "无效的 WebDAV 设置", "error.invalid.webdav": "无效的 WebDAV 设置",
"message.code_style": "代码风格", "message.code_style": "代码风格",
@@ -239,22 +255,30 @@
"message.style": "消息样式", "message.style": "消息样式",
"message.style.bubble": "气泡", "message.style.bubble": "气泡",
"message.style.plain": "简洁", "message.style.plain": "简洁",
"message.multi_model_style": "多模型回答样式",
"message.multi_model_style.horizontal": "水平",
"message.multi_model_style.vertical": "垂直",
"message.multi_model_style.fold": "折叠",
"reset.confirm.content": "确定要重置所有数据吗?", "reset.confirm.content": "确定要重置所有数据吗?",
"reset.double.confirm.content": "你的全部数据都会丢失,如果没有备份数据,将无法恢复,确定要继续吗?", "reset.double.confirm.content": "你的全部数据都会丢失,如果没有备份数据,将无法恢复,确定要继续吗?",
"reset.double.confirm.title": "数据丢失!!!", "reset.double.confirm.title": "数据丢失!!!",
"restore.success": "恢复成功", "restore.success": "恢复成功",
"save.success.title": "保存成功", "save.success.title": "保存成功",
"switch.disabled": "模型回复完成后才能切换", "switch.disabled": "请等待当前回复完成后操作",
"topic.added": "话题添加成功", "topic.added": "话题添加成功",
"upgrade.success.button": "重启", "upgrade.success.button": "重启",
"upgrade.success.content": "重启用以完成升级", "upgrade.success.content": "重启用以完成升级",
"upgrade.success.title": "升级成功", "upgrade.success.title": "升级成功",
"regenerate.confirm": "重新生成会覆盖当前消息", "regenerate.confirm": "重新生成会覆盖当前消息",
"copy.success": "复制成功", "copy.success": "复制成功",
"error.get_embedding_dimensions": "获取嵌入维度失败" "error.get_embedding_dimensions": "获取嵌入维度失败",
"group.delete.title": "删除分组消息",
"group.delete.content": "删除分组消息会删除用户提问和所有助手的回答"
}, },
"minapp": { "minapp": {
"title": "小程序" "title": "小程序",
"sidebar.add.title": "添加到侧边栏",
"sidebar.remove.title": "从侧边栏移除"
}, },
"ollama": { "ollama": {
"keep_alive_time.description": "对话后模型在内存中保持的时间(默认:5分钟)", "keep_alive_time.description": "对话后模型在内存中保持的时间(默认:5分钟)",
@@ -279,7 +303,9 @@
"regenerate.confirm": "这将覆盖已生成的图片,是否继续?", "regenerate.confirm": "这将覆盖已生成的图片,是否继续?",
"seed": "随机种子", "seed": "随机种子",
"seed_tip": "相同的种子和提示词可以生成相似的图片", "seed_tip": "相同的种子和提示词可以生成相似的图片",
"title": "图片" "title": "图片",
"prompt_enhancement": "提示词增强",
"prompt_enhancement_tip": "开启后将提示重写为详细的、适合模型的版本"
}, },
"provider": { "provider": {
"aihubmix": "AiHubMix", "aihubmix": "AiHubMix",
@@ -311,7 +337,8 @@
"together": "Together", "together": "Together",
"yi": "零一万物", "yi": "零一万物",
"zhinao": "360智脑", "zhinao": "360智脑",
"zhipu": "智谱AI" "zhipu": "智谱AI",
"qwenlm": "QwenLM"
}, },
"settings": { "settings": {
"about": "关于我们", "about": "关于我们",
@@ -358,11 +385,23 @@
"webdav.password": "WebDAV 密码", "webdav.password": "WebDAV 密码",
"webdav.path": "WebDAV 路径", "webdav.path": "WebDAV 路径",
"webdav.path.placeholder": "/backup", "webdav.path.placeholder": "/backup",
"webdav.autoSync": "自动同步", "webdav.autoSync": "自动备份",
"webdav.minutes": "分钟", "webdav.minutes": "分钟",
"webdav.hours": "小时",
"webdav.restore.button": "从 WebDAV 恢复", "webdav.restore.button": "从 WebDAV 恢复",
"webdav.title": "WebDAV", "webdav.title": "WebDAV",
"webdav.user": "WebDAV 用户名" "webdav.user": "WebDAV 用户名",
"webdav.syncStatus": "备份状态",
"webdav.autoSync.off": "关闭",
"webdav.noSync": "等待下次备份",
"webdav.syncError": "备份错误",
"webdav.lastSync": "上次备份时间"
},
"quickAssistant": {
"title": "快捷助手",
"click_tray_to_show": "点击托盘图标启动",
"enable_quick_assistant": "启用快捷助手",
"use_shortcut_to_show": "右键点击托盘图标或使用快捷键启动"
}, },
"display.title": "显示设置", "display.title": "显示设置",
"font_size.title": "消息字体大小", "font_size.title": "消息字体大小",
@@ -378,9 +417,20 @@
"general.user_name.placeholder": "请输入用户名", "general.user_name.placeholder": "请输入用户名",
"general.view_webdav_settings": "查看 WebDAV 设置", "general.view_webdav_settings": "查看 WebDAV 设置",
"general.display.title": "显示设置", "general.display.title": "显示设置",
"display.sidebar.translate.icon": "显示翻译图标",
"display.sidebar.painting.icon": "显示绘画图标",
"display.sidebar.minapp.icon": "显示小程序图标", "display.sidebar.minapp.icon": "显示小程序图标",
"display.sidebar.knowledge.icon": "显示知识图标",
"display.sidebar.files.icon": "显示文件图标", "display.sidebar.files.icon": "显示文件图标",
"display.sidebar.title": "侧边栏设置", "display.sidebar.title": "侧边栏设置",
"display.sidebar.visible": "显示的图标",
"display.sidebar.disabled": "隐藏的图标",
"display.sidebar.chat.hiddenMessage": "助手是基础功能,不支持隐藏",
"display.sidebar.empty": "把要隐藏的功能从左侧拖拽到这里",
"display.minApp.title": "小程序显示设置",
"display.minApp.visible": "显示的小程序",
"display.minApp.disabled": "隐藏的小程序",
"display.minApp.empty": "把要隐藏的小程序从左侧拖拽到这里",
"display.topic.title": "话题设置", "display.topic.title": "话题设置",
"display.custom.css": "自定义 CSS", "display.custom.css": "自定义 CSS",
"display.custom.css.placeholder": "/* 这里写自定义CSS */", "display.custom.css.placeholder": "/* 这里写自定义CSS */",
@@ -418,6 +468,7 @@
"models.translate_model_prompt_title": "翻译模型提示词", "models.translate_model_prompt_title": "翻译模型提示词",
"models.topic_naming_model_setting_title": "话题命名模型设置", "models.topic_naming_model_setting_title": "话题命名模型设置",
"models.enable_topic_naming": "话题自动重命名", "models.enable_topic_naming": "话题自动重命名",
"models.topic_naming_prompt": "话题命名提示词",
"provider": { "provider": {
"add.name": "提供商名称", "add.name": "提供商名称",
"add.name.placeholder": "例如 OpenAI", "add.name.placeholder": "例如 OpenAI",
@@ -472,7 +523,11 @@
"clear_shortcut": "清除快捷键", "clear_shortcut": "清除快捷键",
"toggle_show_assistants": "切换助手显示", "toggle_show_assistants": "切换助手显示",
"toggle_show_topics": "切换话题显示", "toggle_show_topics": "切换话题显示",
"copy_last_message": "复制上一条消息" "copy_last_message": "复制上一条消息",
"search_message": "搜索消息",
"mini_window": "快捷助手",
"clear_topic": "清空消息",
"toggle_new_context": "清除上下文"
}, },
"theme.auto": "跟随系统", "theme.auto": "跟随系统",
"theme.dark": "深色主题", "theme.dark": "深色主题",
@@ -505,7 +560,8 @@
}, },
"tray": { "tray": {
"quit": "退出", "quit": "退出",
"show_window": "显示窗口" "show_window": "显示窗口",
"show_mini_window": "快捷助手"
}, },
"words": { "words": {
"knowledgeGraph": "知识图谱", "knowledgeGraph": "知识图谱",
@@ -513,7 +569,7 @@
"show_window": "显示窗口", "show_window": "显示窗口",
"quit": "退出" "quit": "退出"
}, },
"knowledge_base": { "knowledge": {
"title": "知识库", "title": "知识库",
"search": "搜索知识库", "search": "搜索知识库",
"empty": "暂无知识库", "empty": "暂无知识库",
@@ -555,7 +611,11 @@
"directory_placeholder": "请输入目录路径", "directory_placeholder": "请输入目录路径",
"model_info": "模型信息", "model_info": "模型信息",
"not_support": "知识库数据库引擎已更新,该知识库将不再支持,请重新创建知识库", "not_support": "知识库数据库引擎已更新,该知识库将不再支持,请重新创建知识库",
"source": "来源" "no_provider": "知识库模型服务商丢失,该知识库将不再支持,请重新创建知识库",
"source": "来源",
"chunk_size": "分段大小",
"chunk_overlap": "重叠大小",
"not_set": "未设置"
}, },
"models": { "models": {
"pinned": "已固定", "pinned": "已固定",
@@ -574,7 +634,44 @@
"embedding": "嵌入模型", "embedding": "嵌入模型",
"embedding_model": "嵌入模型", "embedding_model": "嵌入模型",
"embedding_model_tooltip": "在设置->模型服务中点击管理按钮添加", "embedding_model_tooltip": "在设置->模型服务中点击管理按钮添加",
"dimensions": "{{dimensions}} 维" "dimensions": "{{dimensions}} 维",
"custom_parameters": "自定义参数",
"add_parameter": "添加参数",
"parameter_name": "参数名称",
"parameter_type": {
"string": "文本",
"number": "数字",
"boolean": "布尔值",
"json": "JSON"
}
},
"prompts": {
"title": "你是一名擅长会话的助理,你需要将用户的会话总结为 10 个字以内的标题,标题语言与用户的首要语言一致,不要使用标点符号和其他特殊符号",
"explanation": "帮我解释一下这个概念",
"summarize": "帮我总结一下这段话"
},
"miniwindow": {
"feature": {
"chat": "回答此问题",
"translate": "文本翻译",
"summary": "内容总结",
"explanation": "解释说明"
},
"clipboard": {
"empty": "剪贴板为空"
},
"input": {
"placeholder": {
"title": "你想对下方文字做什么",
"empty": "询问 {{model}} 获取帮助..."
}
},
"footer": {
"esc": "按 ESC {{action}}",
"esc_close": "关闭窗口",
"esc_back": "返回",
"copy_last_message": "按 C 键复制"
}
} }
} }
} }
+115 -18
View File
@@ -64,14 +64,14 @@
"default.description": "你好,我是預設助手。你可以立即開始與我聊天。", "default.description": "你好,我是預設助手。你可以立即開始與我聊天。",
"default.name": "⭐️ 預設助手", "default.name": "⭐️ 預設助手",
"default.topic.name": "預設話題", "default.topic.name": "預設話題",
"input.clear": "清除", "input.clear": "清除 {{Command}}",
"input.clear.content": "您想要清除當前話題的所有訊息嗎?", "input.clear.content": "您想要清除當前話題的所有訊息嗎?",
"input.clear.title": "清除所有訊息?", "input.clear.title": "清除所有訊息?",
"input.collapse": "收起", "input.collapse": "收起",
"input.context_count.tip": "上下文數量", "input.context_count.tip": "上下文數量",
"input.estimated_tokens.tip": "預估 Token 數", "input.estimated_tokens.tip": "預估 Token 數",
"input.expand": "展開", "input.expand": "展開",
"input.new.context": "清除上下文", "input.new.context": "清除上下文 {{Command}}",
"input.new_topic": "新話題 {{Command}}", "input.new_topic": "新話題 {{Command}}",
"input.pause": "暫停", "input.pause": "暫停",
"input.placeholder": "在此輸入您的訊息...", "input.placeholder": "在此輸入您的訊息...",
@@ -82,10 +82,11 @@
"input.upload": "上傳圖片或文檔", "input.upload": "上傳圖片或文檔",
"input.web_search": "開啟網路搜索", "input.web_search": "開啟網路搜索",
"input.knowledge_base": "知識庫", "input.knowledge_base": "知識庫",
"message.new.branch": "分支", "message.new.branch": "分支",
"message.new.branch.created": "新分支已建立", "message.new.branch.created": "新分支已建立",
"message.regenerate.model": "切換模型", "message.regenerate.model": "切換模型",
"message.new.context": "新上下文", "message.new.context": "新上下文",
"message.useful": "有用",
"save": "保存", "save": "保存",
"settings.code_collapsible": "代码块可折叠", "settings.code_collapsible": "代码块可折叠",
"settings.context_count": "上下文", "settings.context_count": "上下文",
@@ -112,7 +113,10 @@
"topics.list": "話題列表", "topics.list": "話題列表",
"topics.move_to": "移動到", "topics.move_to": "移動到",
"topics.title": "話題", "topics.title": "話題",
"translate": "翻譯" "translate": "翻譯",
"resend": "重新發送",
"thinking": "思考中",
"deeply_thought": "已深度思考(用時 {{secounds}} 秒)"
}, },
"common": { "common": {
"and": "與", "and": "與",
@@ -167,7 +171,7 @@
"conversation_details": "會話詳情", "conversation_details": "會話詳情",
"conversation_history": "會話歷史", "conversation_history": "會話歷史",
"created": "創建時間", "created": "創建時間",
"last_updated": "最後新", "last_updated": "最後新",
"messages": "訊息數", "messages": "訊息數",
"user": "用戶" "user": "用戶"
}, },
@@ -182,8 +186,14 @@
"name": "名稱", "name": "名稱",
"open": "打開", "open": "打開",
"size": "大小", "size": "大小",
"type": "類型",
"text": "文本", "text": "文本",
"title": "檔案" "title": "檔案",
"edit": "編輯",
"delete": "刪除",
"delete.title": "刪除檔案",
"delete.content": "刪除檔案會刪除檔案在所有消息中的引用,確定要刪除此檔案嗎?",
"delete.paintings.warning": "繪圖中包含該圖片,暫時無法刪除"
}, },
"history": { "history": {
"continue_chat": "繼續聊天", "continue_chat": "繼續聊天",
@@ -211,6 +221,10 @@
"png": "下載 PNG", "png": "下載 PNG",
"svg": "下載 SVG" "svg": "下載 SVG"
}, },
"resize": {
"zoom-in": "放大",
"zoom-out": "縮小"
},
"tabs": { "tabs": {
"preview": "預覽", "preview": "預覽",
"source": "原始碼" "source": "原始碼"
@@ -220,6 +234,7 @@
"message": { "message": {
"api.connection.failed": "連接失敗", "api.connection.failed": "連接失敗",
"api.connection.success": "連接成功", "api.connection.success": "連接成功",
"api.check.model.title": "請選擇要檢測的模型",
"assistant.added.content": "智能體添加成功", "assistant.added.content": "智能體添加成功",
"backup.failed": "備份失敗", "backup.failed": "備份失敗",
"backup.success": "備份成功", "backup.success": "備份成功",
@@ -230,6 +245,7 @@
"error.enter.api.key": "請先輸入您的 API 密鑰", "error.enter.api.key": "請先輸入您的 API 密鑰",
"error.enter.model": "請先選擇一個模型", "error.enter.model": "請先選擇一個模型",
"error.enter.name": "請先輸入知識庫名稱", "error.enter.name": "請先輸入知識庫名稱",
"error.chunk_overlap_too_large": "分段重疊不能大於分段大小",
"error.invalid.proxy.url": "無效的代理 URL", "error.invalid.proxy.url": "無效的代理 URL",
"error.invalid.webdav": "無效的 WebDAV 設定", "error.invalid.webdav": "無效的 WebDAV 設定",
"message.code_style": "程式碼風格", "message.code_style": "程式碼風格",
@@ -238,22 +254,30 @@
"message.style": "消息樣式", "message.style": "消息樣式",
"message.style.bubble": "氣泡", "message.style.bubble": "氣泡",
"message.style.plain": "簡潔", "message.style.plain": "簡潔",
"message.multi_model_style": "多模型回答樣式",
"message.multi_model_style.horizontal": "水平",
"message.multi_model_style.vertical": "垂直",
"message.multi_model_style.fold": "折疊",
"reset.confirm.content": "確定要清除所有資料嗎?", "reset.confirm.content": "確定要清除所有資料嗎?",
"reset.double.confirm.content": "所有資料將會被清除,您確定要繼續嗎?", "reset.double.confirm.content": "所有資料將會被清除,您確定要繼續嗎?",
"reset.double.confirm.title": "資料將會丟失!!!", "reset.double.confirm.title": "資料將會丟失!!!",
"restore.success": "恢復成功", "restore.success": "恢復成功",
"save.success.title": "保存成功", "save.success.title": "保存成功",
"switch.disabled": "助手生成回覆時無法切換", "switch.disabled": "請等待當前回覆完成",
"topic.added": "新話題已添加", "topic.added": "新話題已添加",
"upgrade.success.button": "重新啟動", "upgrade.success.button": "重新啟動",
"upgrade.success.content": "請重新啟動應用以完成升級", "upgrade.success.content": "請重新啟動應用以完成升級",
"upgrade.success.title": "升級成功", "upgrade.success.title": "升級成功",
"regenerate.confirm": "重新生成會覆蓋當前訊息", "regenerate.confirm": "重新生成會覆蓋當前訊息",
"copy.success": "複製成功", "copy.success": "複製成功",
"error.get_embedding_dimensions": "獲取嵌入維度失敗" "error.get_embedding_dimensions": "獲取嵌入維度失敗",
"group.delete.title": "刪除分組消息",
"group.delete.content": "刪除分組消息會刪除用戶提問和所有助手的回答"
}, },
"minapp": { "minapp": {
"title": "小程序" "title": "小程序",
"sidebar.add.title": "添加到側邊欄",
"sidebar.remove.title": "從側邊欄移除"
}, },
"ollama": { "ollama": {
"keep_alive_time.description": "對話後模型在記憶體中保持的時間(預設為 5 分鐘)。", "keep_alive_time.description": "對話後模型在記憶體中保持的時間(預設為 5 分鐘)。",
@@ -278,7 +302,9 @@
"regenerate.confirm": "這將覆蓋已生成的圖片,是否繼續?", "regenerate.confirm": "這將覆蓋已生成的圖片,是否繼續?",
"seed": "隨機種子", "seed": "隨機種子",
"seed_tip": "相同的種子和提示詞可以生成相似的圖片", "seed_tip": "相同的種子和提示詞可以生成相似的圖片",
"title": "繪圖" "title": "繪圖",
"prompt_enhancement": "提示詞增強",
"prompt_enhancement_tip": "開啟後將提示重寫為詳細的、適合模型的版本"
}, },
"provider": { "provider": {
"aihubmix": "AiHubMix", "aihubmix": "AiHubMix",
@@ -310,7 +336,8 @@
"together": "Together", "together": "Together",
"yi": "零一萬物", "yi": "零一萬物",
"zhinao": "360智腦", "zhinao": "360智腦",
"zhipu": "智譜AI" "zhipu": "智譜AI",
"qwenlm": "QwenLM"
}, },
"settings": { "settings": {
"about": "關於與回饋", "about": "關於與回饋",
@@ -357,11 +384,23 @@
"webdav.password": "WebDAV 密碼", "webdav.password": "WebDAV 密碼",
"webdav.path": "WebDAV Path", "webdav.path": "WebDAV Path",
"webdav.path.placeholder": "/backup", "webdav.path.placeholder": "/backup",
"webdav.autoSync": "自動同步", "webdav.autoSync": "自動備份",
"webdav.minutes": "分鐘", "webdav.minutes": "分鐘",
"webdav.hours": "小時",
"webdav.restore.button": "從 WebDAV 恢復", "webdav.restore.button": "從 WebDAV 恢復",
"webdav.title": "WebDAV", "webdav.title": "WebDAV",
"webdav.user": "WebDAV 使用者名稱" "webdav.user": "WebDAV 使用者名稱",
"webdav.syncStatus": "備份狀態",
"webdav.autoSync.off": "關閉",
"webdav.noSync": "等待下次備份",
"webdav.syncError": "備份錯誤",
"webdav.lastSync": "上次同步時間"
},
"quickAssistant": {
"title": "快捷助手",
"click_tray_to_show": "點擊托盤圖標啟動",
"enable_quick_assistant": "啟用快捷助手",
"use_shortcut_to_show": "右鍵點擊托盤圖標或使用快捷鍵啟動"
}, },
"display.title": "顯示設定", "display.title": "顯示設定",
"font_size.title": "訊息字體大小", "font_size.title": "訊息字體大小",
@@ -377,10 +416,21 @@
"general.user_name.placeholder": "輸入您的名稱", "general.user_name.placeholder": "輸入您的名稱",
"general.view_webdav_settings": "查看 WebDAV 設定", "general.view_webdav_settings": "查看 WebDAV 設定",
"general.display.title": "顯示設定", "general.display.title": "顯示設定",
"display.sidebar.translate.icon": "顯示翻譯圖示",
"display.sidebar.painting.icon": "顯示繪圖圖示",
"display.sidebar.minapp.icon": "顯示小程序圖示", "display.sidebar.minapp.icon": "顯示小程序圖示",
"display.sidebar.knowledge.icon": "顯示知識圖示",
"display.sidebar.files.icon": "顯示文件圖示", "display.sidebar.files.icon": "顯示文件圖示",
"display.sidebar.title": "側邊欄設定", "display.sidebar.title": "側邊欄設定",
"display.topic.title": "話題設定", "display.topic.title": "話題設定",
"display.sidebar.chat.hiddenMessage": "助手是基礎功能,不支援隱藏",
"display.sidebar.empty": "把要隱藏的功能從左側拖拽到這裡",
"display.sidebar.visible": "顯示的圖標",
"display.sidebar.disabled": "隱藏的圖標",
"display.minApp.title": "小程序顯示設定",
"display.minApp.visible": "顯示的小程序",
"display.minApp.disabled": "隱藏的小程序",
"display.minApp.empty": "把要隱藏的小程序從左側拖拽到這裡",
"display.custom.css": "自定義 CSS", "display.custom.css": "自定義 CSS",
"display.custom.css.placeholder": "/* 這裡寫自定義 CSS */", "display.custom.css.placeholder": "/* 這裡寫自定義 CSS */",
"input.auto_translate_with_space": "快速敲擊3次空格翻譯", "input.auto_translate_with_space": "快速敲擊3次空格翻譯",
@@ -417,6 +467,7 @@
"models.translate_model_prompt_title": "翻譯模型提示詞", "models.translate_model_prompt_title": "翻譯模型提示詞",
"models.topic_naming_model_setting_title": "話題命名模型設定", "models.topic_naming_model_setting_title": "話題命名模型設定",
"models.enable_topic_naming": "話題自動重命名", "models.enable_topic_naming": "話題自動重命名",
"models.topic_naming_prompt": "話題命名提示詞",
"provider": { "provider": {
"add.name": "提供者名稱", "add.name": "提供者名稱",
"add.name.placeholder": "例如:OpenAI", "add.name.placeholder": "例如:OpenAI",
@@ -471,7 +522,11 @@
"clear_shortcut": "清除快捷鍵", "clear_shortcut": "清除快捷鍵",
"toggle_show_assistants": "切換助手顯示", "toggle_show_assistants": "切換助手顯示",
"toggle_show_topics": "切換話題顯示", "toggle_show_topics": "切換話題顯示",
"copy_last_message": "複製上一条消息" "copy_last_message": "複製上一条消息",
"search_message": "搜索消息",
"mini_window": "快捷助手",
"clear_topic": "清除所有訊息",
"toggle_new_context": "清除上下文"
}, },
"theme.auto": "自動", "theme.auto": "自動",
"theme.dark": "深色主題", "theme.dark": "深色主題",
@@ -504,7 +559,8 @@
}, },
"tray": { "tray": {
"quit": "退出", "quit": "退出",
"show_window": "顯示視窗" "show_window": "顯示視窗",
"show_mini_window": "快捷助手"
}, },
"words": { "words": {
"knowledgeGraph": "知識圖譜", "knowledgeGraph": "知識圖譜",
@@ -512,7 +568,7 @@
"show_window": "顯示視窗", "show_window": "顯示視窗",
"quit": "退出" "quit": "退出"
}, },
"knowledge_base": { "knowledge": {
"title": "知識庫", "title": "知識庫",
"search": "搜尋知識庫", "search": "搜尋知識庫",
"empty": "暫無知識庫", "empty": "暫無知識庫",
@@ -554,7 +610,11 @@
"directory_placeholder": "請輸入目錄路徑", "directory_placeholder": "請輸入目錄路徑",
"model_info": "模型信息", "model_info": "模型信息",
"not_support": "知識庫數據庫引擎已更新,該知識庫將不再支持,請重新創建知識庫", "not_support": "知識庫數據庫引擎已更新,該知識庫將不再支持,請重新創建知識庫",
"source": "來源" "no_provider": "知識庫模型提供商遺失,該知識庫將不再支持,請重新創建知識庫",
"source": "來源",
"chunk_size": "分段大小",
"chunk_overlap": "重疊大小",
"not_set": "未設置"
}, },
"models": { "models": {
"pinned": "已固定", "pinned": "已固定",
@@ -573,7 +633,44 @@
"embedding": "嵌入模型", "embedding": "嵌入模型",
"embedding_model": "嵌入模型", "embedding_model": "嵌入模型",
"embedding_model_tooltip": "在设置->模型服务中点击管理按钮添加", "embedding_model_tooltip": "在设置->模型服务中点击管理按钮添加",
"dimensions": "{{dimensions}} 維" "dimensions": "{{dimensions}} 維",
"custom_parameters": "自定義參數",
"add_parameter": "添加參數",
"parameter_name": "參數名稱",
"parameter_type": {
"string": "文字",
"number": "數字",
"boolean": "布林值",
"json": "JSON"
}
},
"prompts": {
"title": "你是一名擅長會話的助理,你需要將用戶的會話總結為 10 個字以內的標題,標題語言與用戶的首要語言一致,不要使用標點符號和其他特殊符號",
"explanation": "幫我解釋一下這個概念",
"summarize": "幫我總結一下這段話"
},
"miniwindow": {
"feature": {
"chat": "回答此問題",
"translate": "文本翻譯",
"summary": "內容總結",
"explanation": "解釋說明"
},
"clipboard": {
"empty": "剪貼板為空"
},
"input": {
"placeholder": {
"title": "你想對下方文字做什麼",
"empty": "詢問 {{model}} 獲取幫助..."
}
},
"footer": {
"esc": "按 ESC {{action}}",
"esc_close": "關閉窗口",
"esc_back": "返回",
"copy_last_message": "按 C 鍵複製"
}
} }
} }
} }
+23 -2
View File
@@ -1,8 +1,29 @@
import KeyvStorage from '@kangfenmao/keyv-storage' import KeyvStorage from '@kangfenmao/keyv-storage'
function init() { import { startAutoSync } from './services/BackupService'
import store from './store'
function initSpinner() {
const spinner = document.getElementById('spinner')
if (spinner && window.location.hash !== '#/mini') {
spinner.style.display = 'flex'
}
}
function initKeyv() {
window.keyv = new KeyvStorage() window.keyv = new KeyvStorage()
window.keyv.init() window.keyv.init()
} }
init() function initAutoSync() {
setTimeout(() => {
const { webdavAutoSync } = store.getState().settings
if (webdavAutoSync) {
startAutoSync()
}
}, 2000)
}
initSpinner()
initKeyv()
initAutoSync()
+7 -2
View File
@@ -1,8 +1,13 @@
import './assets/styles/index.scss' import './assets/styles/index.scss'
import './init'
import ReactDOM from 'react-dom/client' import ReactDOM from 'react-dom/client'
import App from './App' import App from './App'
import MiniApp from './windows/mini/App'
ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render(<App />) if (location.hash === '#/mini') {
document.getElementById('spinner')?.remove()
ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render(<MiniApp />)
} else {
ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render(<App />)
}
+47 -50
View File
@@ -1,7 +1,6 @@
import { SearchOutlined } from '@ant-design/icons' import { SearchOutlined } from '@ant-design/icons'
import { Navbar, NavbarCenter } from '@renderer/components/app/Navbar' import { Navbar, NavbarCenter } from '@renderer/components/app/Navbar'
import Scrollbar from '@renderer/components/Scrollbar' import Scrollbar from '@renderer/components/Scrollbar'
import SystemAgents from '@renderer/config/agents.json'
import { createAssistantFromAgent } from '@renderer/services/AssistantService' import { createAssistantFromAgent } from '@renderer/services/AssistantService'
import { Agent } from '@renderer/types' import { Agent } from '@renderer/types'
import { uuid } from '@renderer/utils' import { uuid } from '@renderer/utils'
@@ -12,35 +11,26 @@ import { useTranslation } from 'react-i18next'
import ReactMarkdown from 'react-markdown' import ReactMarkdown from 'react-markdown'
import styled from 'styled-components' import styled from 'styled-components'
import { getAgentsFromSystemAgents, useSystemAgents } from '.'
import { groupTranslations } from './agentGroupTranslations' import { groupTranslations } from './agentGroupTranslations'
import AgentCard from './components/AgentCard' import AgentCard from './components/AgentCard'
import MyAgents from './components/MyAgents' import MyAgents from './components/MyAgents'
const { Title } = Typography const { Title } = Typography
const getAgentsFromSystemAgents = () => {
const agents: Agent[] = []
for (let i = 0; i < SystemAgents.length; i++) {
for (let j = 0; j < SystemAgents[i].group.length; j++) {
const agent = { ...SystemAgents[i], group: SystemAgents[i].group[j], topics: [], type: 'agent' } as Agent
agents.push(agent)
}
}
return agents
}
let _agentGroups: Record<string, Agent[]> = {} let _agentGroups: Record<string, Agent[]> = {}
const AgentsPage: FC = () => { const AgentsPage: FC = () => {
const [search, setSearch] = useState('') const [search, setSearch] = useState('')
const [searchInput, setSearchInput] = useState('') const [searchInput, setSearchInput] = useState('')
const systemAgents = useSystemAgents()
const agentGroups = useMemo(() => { const agentGroups = useMemo(() => {
if (Object.keys(_agentGroups).length === 0) { if (Object.keys(_agentGroups).length === 0) {
_agentGroups = groupBy(getAgentsFromSystemAgents(), 'group') _agentGroups = groupBy(getAgentsFromSystemAgents(systemAgents), 'group')
} }
return _agentGroups return _agentGroups
}, []) }, [systemAgents])
const { t, i18n } = useTranslation() const { t, i18n } = useTranslation()
@@ -102,7 +92,7 @@ const AgentsPage: FC = () => {
[t] [t]
) )
const getAgentFromSystemAgent = (agent: (typeof SystemAgents)[number]) => { const getAgentFromSystemAgent = (agent: (typeof systemAgents)[number]) => {
return { return {
...omit(agent, 'group'), ...omit(agent, 'group'),
name: agent.name, name: agent.name,
@@ -120,40 +110,46 @@ const AgentsPage: FC = () => {
[i18n.language] [i18n.language]
) )
const renderAgentList = useCallback(
(agents: Agent[]) => {
return (
<Row gutter={[20, 20]}>
{agents.map((agent, index) => (
<Col span={6} key={agent.id || index}>
<AgentCard
onClick={() => onAddAgentConfirm(getAgentFromSystemAgent(agent as any))}
agent={agent as any}
/>
</Col>
))}
</Row>
)
},
[onAddAgentConfirm]
)
const tabItems = useMemo(() => { const tabItems = useMemo(() => {
const groups = Object.keys(filteredAgentGroups) const groups = Object.keys(filteredAgentGroups)
return groups.map((group, i) => { return groups.map((group, i) => {
const id = String(i + 1) const id = String(i + 1)
const localizedGroupName = getLocalizedGroupName(group) const localizedGroupName = getLocalizedGroupName(group)
const agents = filteredAgentGroups[group] || []
return { return {
label: localizedGroupName, label: localizedGroupName,
key: id, key: id,
children: ( children: (
<TabContent key={group}> <TabContent key={group}>
<Title level={5} key={group} style={{ marginBottom: 16 }}> <Title level={5} key={group} style={{ marginBottom: 10 }}>
{localizedGroupName} {localizedGroupName}
</Title> </Title>
<Row gutter={[20, 20]}> {group === '我的' ? <MyAgents onClick={onAddAgentConfirm} search={search} /> : renderAgentList(agents)}
{group === '我的' ? (
<MyAgents onClick={onAddAgentConfirm} search={search} />
) : (
filteredAgentGroups[group]?.map((agent, index) => (
<Col span={6} key={group + index}>
<AgentCard
onClick={() => onAddAgentConfirm(getAgentFromSystemAgent(agent as any))}
agent={agent as any}
/>
</Col>
))
)}
</Row>
</TabContent> </TabContent>
) )
} }
}) })
}, [filteredAgentGroups, getLocalizedGroupName, onAddAgentConfirm, search]) }, [filteredAgentGroups, getLocalizedGroupName, onAddAgentConfirm, search, renderAgentList])
const handleSearch = () => { const handleSearch = () => {
if (searchInput.trim() === '') { if (searchInput.trim() === '') {
@@ -189,22 +185,9 @@ const AgentsPage: FC = () => {
<AssistantsContainer> <AssistantsContainer>
{Object.values(filteredAgentGroups).flat().length > 0 ? ( {Object.values(filteredAgentGroups).flat().length > 0 ? (
search.trim() ? ( search.trim() ? (
<TabContent> <TabContent>{renderAgentList(Object.values(filteredAgentGroups).flat())}</TabContent>
<Row gutter={[20, 20]}>
{Object.values(filteredAgentGroups)
.flat()
.map((agent, index, array) => (
<Col span={array.length === 1 ? 12 : 6} key={index}>
<AgentCard
onClick={() => onAddAgentConfirm(getAgentFromSystemAgent(agent as any))}
agent={agent as any}
/>
</Col>
))}
</Row>
</TabContent>
) : ( ) : (
<Tabs tabPosition="right" animated items={tabItems} $language={i18n.language} /> <Tabs tabPosition="right" animated={false} items={tabItems} $language={i18n.language} />
) )
) : ( ) : (
<EmptyView> <EmptyView>
@@ -232,6 +215,7 @@ const ContentContainer = styled.div`
height: 100%; height: 100%;
padding: 0 10px; padding: 0 10px;
padding-left: 0; padding-left: 0;
border-top: 0.5px solid var(--color-border);
` `
const AssistantsContainer = styled.div` const AssistantsContainer = styled.div`
@@ -247,6 +231,9 @@ const TabContent = styled(Scrollbar)`
margin-right: -4px; margin-right: -4px;
padding-bottom: 20px !important; padding-bottom: 20px !important;
overflow-x: hidden; overflow-x: hidden;
transform: translateZ(0);
will-change: transform;
-webkit-font-smoothing: antialiased;
` `
const AgentPrompt = styled.div` const AgentPrompt = styled.div`
@@ -268,12 +255,15 @@ const Tabs = styled(TabsAntd)<{ $language: string }>`
display: flex; display: flex;
flex: 1; flex: 1;
flex-direction: row-reverse; flex-direction: row-reverse;
.ant-tabs-tabpane { .ant-tabs-tabpane {
padding-right: 0 !important; padding-right: 0 !important;
} }
.ant-tabs-nav { .ant-tabs-nav {
min-width: ${({ $language }) => ($language.startsWith('zh') ? '110px' : '140px')}; min-width: ${({ $language }) => ($language.startsWith('zh') ? '120px' : '140px')};
max-width: ${({ $language }) => ($language.startsWith('zh') ? '110px' : '140px')}; max-width: ${({ $language }) => ($language.startsWith('zh') ? '120px' : '140px')};
position: relative;
overflow: hidden;
} }
.ant-tabs-nav-list { .ant-tabs-nav-list {
padding: 10px 8px; padding: 10px 8px;
@@ -283,7 +273,7 @@ const Tabs = styled(TabsAntd)<{ $language: string }>`
} }
.ant-tabs-tab { .ant-tabs-tab {
margin: 0 !important; margin: 0 !important;
border-radius: 16px; border-radius: var(--list-item-border-radius);
margin-bottom: 5px !important; margin-bottom: 5px !important;
font-size: 13px; font-size: 13px;
justify-content: left; justify-content: left;
@@ -291,11 +281,15 @@ const Tabs = styled(TabsAntd)<{ $language: string }>`
border: 0.5px solid transparent; border: 0.5px solid transparent;
justify-content: ${({ $language }) => ($language.startsWith('zh') ? 'center' : 'flex-start')}; justify-content: ${({ $language }) => ($language.startsWith('zh') ? 'center' : 'flex-start')};
user-select: none; user-select: none;
transition: all 0.3s cubic-bezier(0.645, 0.045, 0.355, 1);
outline: none !important;
.ant-tabs-tab-btn { .ant-tabs-tab-btn {
white-space: nowrap; white-space: nowrap;
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
max-width: 100px; max-width: 100px;
transition: all 0.3s cubic-bezier(0.645, 0.045, 0.355, 1);
outline: none !important;
} }
&:hover { &:hover {
color: var(--color-text) !important; color: var(--color-text) !important;
@@ -304,8 +298,8 @@ const Tabs = styled(TabsAntd)<{ $language: string }>`
} }
.ant-tabs-tab-active { .ant-tabs-tab-active {
background-color: var(--color-background-soft); background-color: var(--color-background-soft);
border-right: none;
border: 0.5px solid var(--color-border); border: 0.5px solid var(--color-border);
transform: scale(1.02);
} }
.ant-tabs-content-holder { .ant-tabs-content-holder {
border-left: 0.5px solid var(--color-border); border-left: 0.5px solid var(--color-border);
@@ -322,6 +316,9 @@ const Tabs = styled(TabsAntd)<{ $language: string }>`
color: var(--color-text) !important; color: var(--color-text) !important;
} }
} }
.ant-tabs-content {
transition: all 0.3s cubic-bezier(0.645, 0.045, 0.355, 1);
}
` `
export default AgentsPage export default AgentsPage
@@ -2,11 +2,12 @@ import { EllipsisOutlined } from '@ant-design/icons'
import { Agent } from '@renderer/types' import { Agent } from '@renderer/types'
import { getLeadingEmoji } from '@renderer/utils' import { getLeadingEmoji } from '@renderer/utils'
import { Dropdown } from 'antd' import { Dropdown } from 'antd'
import { FC, memo } from 'react'
import styled from 'styled-components' import styled from 'styled-components'
interface Props { interface Props {
agent: Agent agent: Agent
onClick?: () => void onClick: () => void
contextMenu?: { label: string; onClick: () => void }[] contextMenu?: { label: string; onClick: () => void }[]
menuItems?: { menuItems?: {
key: string key: string
@@ -17,7 +18,7 @@ interface Props {
}[] }[]
} }
const AgentCard: React.FC<Props> = ({ agent, onClick, contextMenu, menuItems }) => { const AgentCard: FC<Props> = ({ agent, onClick, contextMenu, menuItems }) => {
const emoji = agent.emoji || getLeadingEmoji(agent.name) const emoji = agent.emoji || getLeadingEmoji(agent.name)
const prompt = (agent.description || agent.prompt).substring(0, 100).replace(/\\n/g, '') const prompt = (agent.description || agent.prompt).substring(0, 100).replace(/\\n/g, '')
const content = ( const content = (
@@ -81,7 +82,7 @@ const Container = styled.div`
text-align: center; text-align: center;
gap: 10px; gap: 10px;
background-color: var(--color-background); background-color: var(--color-background);
border-radius: 16px; border-radius: 10px;
position: relative; position: relative;
overflow: hidden; overflow: hidden;
cursor: pointer; cursor: pointer;
@@ -205,4 +206,4 @@ const MenuContainer = styled.div`
} }
` `
export default AgentCard export default memo(AgentCard)
@@ -3,7 +3,7 @@ import { useAgents } from '@renderer/hooks/useAgents'
import AssistantSettingsPopup from '@renderer/pages/settings/AssistantSettings' import AssistantSettingsPopup from '@renderer/pages/settings/AssistantSettings'
import { createAssistantFromAgent } from '@renderer/services/AssistantService' import { createAssistantFromAgent } from '@renderer/services/AssistantService'
import { Agent } from '@renderer/types' import { Agent } from '@renderer/types'
import { Col } from 'antd' import { Col, Row } from 'antd'
import { useCallback, useMemo } from 'react' import { useCallback, useMemo } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
@@ -43,7 +43,7 @@ const MyAgents: React.FC<Props> = ({ onClick, search }) => {
) )
return ( return (
<> <Row gutter={[20, 20]}>
{filteredAgents.map((agent) => { {filteredAgents.map((agent) => {
const dropdownMenuItems = [ const dropdownMenuItems = [
{ {
@@ -102,7 +102,7 @@ const MyAgents: React.FC<Props> = ({ onClick, search }) => {
<Col span={6}> <Col span={6}>
<AddAgentCard onClick={() => AddAgentPopup.show()} /> <AddAgentCard onClick={() => AddAgentPopup.show()} />
</Col> </Col>
</> </Row>
) )
} }
+33
View File
@@ -0,0 +1,33 @@
import { useRuntime } from '@renderer/hooks/useRuntime'
import { Agent } from '@renderer/types'
import { runAsyncFunction } from '@renderer/utils'
import { useEffect, useState } from 'react'
let _agents: Agent[] = []
export const getAgentsFromSystemAgents = (systemAgents: any) => {
const agents: Agent[] = []
for (let i = 0; i < systemAgents.length; i++) {
for (let j = 0; j < systemAgents[i].group.length; j++) {
const agent = { ...systemAgents[i], group: systemAgents[i].group[j], topics: [], type: 'agent' } as Agent
agents.push(agent)
}
}
return agents
}
export function useSystemAgents() {
const [agents, setAgents] = useState<Agent[]>(_agents)
const { resourcesPath } = useRuntime()
useEffect(() => {
runAsyncFunction(async () => {
if (_agents.length > 0) return
const agents = await window.api.fs.read(resourcesPath + '/data/agents.json')
_agents = JSON.parse(agents) as Agent[]
setAgents(_agents)
})
}, [resourcesPath])
return agents
}
+30 -17
View File
@@ -1,6 +1,11 @@
import MinAppIcon from '@renderer/components/Icons/MinAppIcon'
import MinApp from '@renderer/components/MinApp' import MinApp from '@renderer/components/MinApp'
import { useMinapps } from '@renderer/hooks/useMinapps'
import { MinAppType } from '@renderer/types' import { MinAppType } from '@renderer/types'
import type { MenuProps } from 'antd'
import { Dropdown } from 'antd'
import { FC } from 'react' import { FC } from 'react'
import { useTranslation } from 'react-i18next'
import styled from 'styled-components' import styled from 'styled-components'
interface Props { interface Props {
@@ -10,23 +15,37 @@ interface Props {
} }
const App: FC<Props> = ({ app, onClick, size = 60 }) => { const App: FC<Props> = ({ app, onClick, size = 60 }) => {
const { t } = useTranslation()
const { minapps, pinned, updatePinnedMinapps } = useMinapps()
const isPinned = pinned.some((p) => p.id === app.id)
const isVisible = minapps.some((m) => m.id === app.id)
const handleClick = () => { const handleClick = () => {
MinApp.start(app) MinApp.start(app)
onClick?.() onClick?.()
} }
const menuItems: MenuProps['items'] = [
{
key: 'togglePin',
label: isPinned ? t('minapp.sidebar.remove.title') : t('minapp.sidebar.add.title'),
onClick: () => {
console.debug('togglePin', app)
const newPinned = isPinned ? pinned.filter((item) => item.id !== app.id) : [...(pinned || []), app]
updatePinnedMinapps(newPinned)
}
}
]
if (!isVisible) return null
return ( return (
<Container onClick={handleClick}> <Dropdown menu={{ items: menuItems }} trigger={['contextMenu']}>
<AppIcon <Container onClick={handleClick}>
src={app.logo} <MinAppIcon size={size} app={app} />
style={{ <AppTitle>{app.name}</AppTitle>
border: app.bodered ? '0.5px solid var(--color-border)' : 'none', </Container>
width: `${size}px`, </Dropdown>
height: `${size}px`
}}
/>
<AppTitle>{app.name}</AppTitle>
</Container>
) )
} }
@@ -39,12 +58,6 @@ const Container = styled.div`
overflow: hidden; overflow: hidden;
` `
const AppIcon = styled.img`
border-radius: 16px;
user-select: none;
-webkit-user-drag: none;
`
const AppTitle = styled.div` const AppTitle = styled.div`
font-size: 12px; font-size: 12px;
margin-top: 5px; margin-top: 5px;
+25 -14
View File
@@ -1,10 +1,10 @@
import { SearchOutlined } from '@ant-design/icons' import { SearchOutlined } from '@ant-design/icons'
import { Navbar, NavbarCenter } from '@renderer/components/app/Navbar' import { Navbar, NavbarCenter } from '@renderer/components/app/Navbar'
import { Center } from '@renderer/components/Layout' import { Center } from '@renderer/components/Layout'
import { getAllMinApps } from '@renderer/config/minapps' import { useMinapps } from '@renderer/hooks/useMinapps'
import { Empty, Input } from 'antd' import { Empty, Input } from 'antd'
import { isEmpty } from 'lodash' import { isEmpty } from 'lodash'
import { FC, useMemo, useState } from 'react' import React, { FC, useState } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import styled from 'styled-components' import styled from 'styled-components'
@@ -13,16 +13,27 @@ import App from './App'
const AppsPage: FC = () => { const AppsPage: FC = () => {
const { t } = useTranslation() const { t } = useTranslation()
const [search, setSearch] = useState('') const [search, setSearch] = useState('')
const apps = useMemo(() => getAllMinApps(), []) const { minapps } = useMinapps()
const filteredApps = search const filteredApps = search
? apps.filter( ? minapps.filter(
(app) => app.name.toLowerCase().includes(search.toLowerCase()) || app.url.includes(search.toLowerCase()) (app) => app.name.toLowerCase().includes(search.toLowerCase()) || app.url.includes(search.toLowerCase())
) )
: apps : minapps
// Calculate the required number of lines
const itemsPerRow = Math.floor(930 / 115) // Maximum width divided by the width of each item (including spacing)
const rowCount = Math.ceil(filteredApps.length / itemsPerRow)
// Each line height is 85px (60px icon + 5px margin + 12px text + spacing)
const containerHeight = rowCount * 85 + (rowCount - 1) * 25 // 25px is the line spacing.
// Disable right-click menu in blank area
const handleContextMenu = (e: React.MouseEvent) => {
e.preventDefault()
}
return ( return (
<Container> <Container onContextMenu={handleContextMenu}>
<Navbar> <Navbar>
<NavbarCenter style={{ borderRight: 'none', justifyContent: 'space-between' }}> <NavbarCenter style={{ borderRight: 'none', justifyContent: 'space-between' }}>
{t('minapp.title')} {t('minapp.title')}
@@ -40,7 +51,7 @@ const AppsPage: FC = () => {
</NavbarCenter> </NavbarCenter>
</Navbar> </Navbar>
<ContentContainer id="content-container"> <ContentContainer id="content-container">
<AppsContainer> <AppsContainer style={{ height: containerHeight }}>
{filteredApps.map((app) => ( {filteredApps.map((app) => (
<App key={app.id} app={app} /> <App key={app.id} app={app} />
))} ))}
@@ -68,18 +79,18 @@ const ContentContainer = styled.div`
flex-direction: row; flex-direction: row;
justify-content: center; justify-content: center;
height: 100%; height: 100%;
overflow-y: scroll; overflow-y: auto;
padding: 50px; padding: 50px;
` `
const AppsContainer = styled.div` const AppsContainer = styled.div`
display: flex;
min-width: 930px;
max-width: 930px;
max-height: 500px;
display: grid; display: grid;
grid-template-columns: repeat(8, minmax(90px, 1fr)); min-width: 0;
gap: 25px 25px; max-width: 930px;
width: 100%;
grid-template-columns: repeat(auto-fill, 90px);
gap: 25px;
justify-content: center;
` `
export default AppsPage export default AppsPage
@@ -0,0 +1,129 @@
import FileManager from '@renderer/services/FileManager'
import { FileType, FileTypes } from '@renderer/types'
import { formatFileSize } from '@renderer/utils'
import { Col, Image, Row, Spin, Table } from 'antd'
import React, { memo } from 'react'
import styled from 'styled-components'
import GeminiFiles from './GeminiFiles'
interface ContentViewProps {
id: FileTypes | 'all' | string
files?: FileType[]
dataSource?: any[]
columns: any[]
}
const ContentView: React.FC<ContentViewProps> = ({ id, files, dataSource, columns }) => {
if (id === FileTypes.IMAGE && files?.length && files?.length > 0) {
return (
<Image.PreviewGroup>
<Row gutter={[16, 16]}>
{files?.map((file) => (
<Col key={file.id} xs={24} sm={12} md={8} lg={4} xl={3}>
<ImageWrapper>
<LoadingWrapper>
<Spin />
</LoadingWrapper>
<Image
src={FileManager.getFileUrl(file)}
style={{ height: '100%', objectFit: 'cover', cursor: 'pointer' }}
preview={{ mask: false }}
onLoad={(e) => {
const img = e.target as HTMLImageElement
img.parentElement?.classList.add('loaded')
}}
/>
<ImageInfo>
<div>{formatFileSize(file)}</div>
</ImageInfo>
</ImageWrapper>
</Col>
))}
</Row>
</Image.PreviewGroup>
)
}
if (id.startsWith('gemini_')) {
return <GeminiFiles id={id.replace('gemini_', '') as string} />
}
return (
<Table
dataSource={dataSource}
columns={columns}
style={{ width: '100%' }}
size="small"
pagination={{ pageSize: 100 }}
/>
)
}
const ImageWrapper = styled.div`
position: relative;
aspect-ratio: 1;
overflow: hidden;
border-radius: 8px;
background-color: var(--color-background-soft);
display: flex;
align-items: center;
justify-content: center;
border: 0.5px solid var(--color-border);
.ant-image {
height: 100%;
width: 100%;
opacity: 0;
transition:
opacity 0.3s ease,
transform 0.3s ease;
&.loaded {
opacity: 1;
}
}
&:hover {
.ant-image.loaded {
transform: scale(1.05);
}
div:last-child {
opacity: 1;
}
}
`
const LoadingWrapper = styled.div`
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
display: flex;
align-items: center;
justify-content: center;
background-color: var(--color-background-soft);
`
const ImageInfo = styled.div`
position: absolute;
bottom: 0;
left: 0;
right: 0;
background: rgba(0, 0, 0, 0.6);
color: white;
padding: 5px 8px;
opacity: 0;
transition: opacity 0.3s ease;
font-size: 12px;
> div:first-child {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
`
export default memo(ContentView)
+149 -142
View File
@@ -1,20 +1,36 @@
import { FileImageOutlined, FilePdfOutlined, FileTextOutlined } from '@ant-design/icons' import {
DeleteOutlined,
EditOutlined,
EllipsisOutlined,
FileImageOutlined,
FilePdfOutlined,
FileTextOutlined
} from '@ant-design/icons'
import { Navbar, NavbarCenter } from '@renderer/components/app/Navbar' import { Navbar, NavbarCenter } from '@renderer/components/app/Navbar'
import TextEditPopup from '@renderer/components/Popups/TextEditPopup'
import Scrollbar from '@renderer/components/Scrollbar' import Scrollbar from '@renderer/components/Scrollbar'
import db from '@renderer/databases' import db from '@renderer/databases'
import { useProviders } from '@renderer/hooks/useProvider'
import FileManager from '@renderer/services/FileManager' import FileManager from '@renderer/services/FileManager'
import store from '@renderer/store'
import { FileType, FileTypes } from '@renderer/types' import { FileType, FileTypes } from '@renderer/types'
import { formatFileSize } from '@renderer/utils' import { formatFileSize } from '@renderer/utils'
import { Col, Image, Menu, Row, Spin, Table } from 'antd' import type { MenuProps } from 'antd'
import { Button, Dropdown, Menu } from 'antd'
import dayjs from 'dayjs' import dayjs from 'dayjs'
import { useLiveQuery } from 'dexie-react-hooks' import { useLiveQuery } from 'dexie-react-hooks'
import { FC, useState } from 'react' import { FC, useMemo, useState } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import styled from 'styled-components' import styled from 'styled-components'
import ContentView from './ContentView'
const FilesPage: FC = () => { const FilesPage: FC = () => {
const { t } = useTranslation() const { t } = useTranslation()
const [fileType, setFileType] = useState<FileTypes | 'all'>('all') const [fileType, setFileType] = useState<FileTypes | 'all' | 'gemini'>('all')
const { providers } = useProviders()
const geminiProviders = providers.filter((provider) => provider.type === 'gemini')
const files = useLiveQuery<FileType[]>(() => { const files = useLiveQuery<FileType[]>(() => {
if (fileType === 'all') { if (fileType === 'all') {
@@ -23,56 +39,146 @@ const FilesPage: FC = () => {
return db.files.where('type').equals(fileType).sortBy('count') return db.files.where('type').equals(fileType).sortBy('count')
}, [fileType]) }, [fileType])
const handleDelete = async (fileId: string) => {
const file = await FileManager.getFile(fileId)
const paintings = await store.getState().paintings.paintings
const paintingsFiles = paintings.flatMap((p) => p.files)
if (paintingsFiles.some((p) => p.id === fileId)) {
window.modal.warning({ content: t('files.delete.paintings.warning'), centered: true })
return
}
if (file) {
await FileManager.deleteFile(fileId, true)
}
const topics = await db.topics
.filter((topic) => topic.messages.some((message) => message.files?.some((f) => f.id === fileId)))
.toArray()
if (topics.length > 0) {
for (const topic of topics) {
const updatedMessages = topic.messages.map((message) => ({
...message,
files: message.files?.filter((f) => f.id !== fileId)
}))
await db.topics.update(topic.id, { messages: updatedMessages })
}
}
}
const handleRename = async (fileId: string) => {
const file = await FileManager.getFile(fileId)
if (file) {
const newName = await TextEditPopup.show({ text: file.origin_name })
if (newName) {
FileManager.updateFile({ ...file, origin_name: newName })
}
}
}
const getActionMenu = (fileId: string): MenuProps['items'] => [
{
key: 'rename',
icon: <EditOutlined />,
label: t('files.edit'),
onClick: () => handleRename(fileId)
},
{
key: 'delete',
icon: <DeleteOutlined />,
label: t('files.delete'),
danger: true,
onClick: () => {
window.modal.confirm({
title: t('files.delete.title'),
content: t('files.delete.content'),
centered: true,
okButtonProps: { danger: true },
onOk: () => handleDelete(fileId)
})
}
}
]
const dataSource = files?.map((file) => { const dataSource = files?.map((file) => {
return { return {
key: file.id, key: file.id,
file: <FileNameText className="text-nowrap">{file.origin_name}</FileNameText>, file: (
<FileNameText className="text-nowrap" onClick={() => window.api.file.openPath(file.path)}>
{file.origin_name}
</FileNameText>
),
size: formatFileSize(file), size: formatFileSize(file),
size_bytes: file.size,
count: file.count, count: file.count,
created_at: dayjs(file.created_at).format('MM-DD HH:mm'), created_at: dayjs(file.created_at).format('MM-DD HH:mm'),
actions: <a href={'file://' + FileManager.getSafePath(file)}>{t('files.open')}</a> created_at_unix: dayjs(file.created_at).unix(),
actions: (
<Dropdown menu={{ items: getActionMenu(file.id) }} trigger={['click']} placement="bottom" arrow>
<Button type="text" size="small" icon={<EllipsisOutlined />} />
</Dropdown>
)
} }
}) })
const columns = [ const columns = useMemo(
{ () => [
title: t('files.name'), {
dataIndex: 'file', title: t('files.name'),
key: 'file', dataIndex: 'file',
width: '300px' key: 'file',
}, width: '300px'
{ },
title: t('files.size'), {
dataIndex: 'size', title: t('files.size'),
key: 'size', dataIndex: 'size',
width: '80px' key: 'size',
}, width: '80px',
{ sorter: (a: { size_bytes: number }, b: { size_bytes: number }) => b.size_bytes - a.size_bytes,
title: t('files.count'), align: 'center'
dataIndex: 'count', },
key: 'count', {
width: '60px' title: t('files.count'),
}, dataIndex: 'count',
{ key: 'count',
title: t('files.created_at'), width: '60px',
dataIndex: 'created_at', sorter: (a: { count: number }, b: { count: number }) => b.count - a.count,
key: 'created_at', align: 'center'
width: '120px' },
}, {
{ title: t('files.created_at'),
title: t('files.actions'), dataIndex: 'created_at',
dataIndex: 'actions', key: 'created_at',
key: 'actions', width: '120px',
width: '50px' align: 'center',
} sorter: (a: { created_at_unix: number }, b: { created_at_unix: number }) =>
] b.created_at_unix - a.created_at_unix
},
{
title: t('files.actions'),
dataIndex: 'actions',
key: 'actions',
width: '80px',
align: 'center'
}
],
[t]
)
const menuItems = [ const menuItems = [
{ key: 'all', label: t('files.all'), icon: <FileTextOutlined /> }, { key: 'all', label: t('files.all'), icon: <FileTextOutlined /> },
{ key: FileTypes.IMAGE, label: t('files.image'), icon: <FileImageOutlined /> }, { key: FileTypes.IMAGE, label: t('files.image'), icon: <FileImageOutlined /> },
{ key: FileTypes.TEXT, label: t('files.text'), icon: <FileTextOutlined /> }, { key: FileTypes.TEXT, label: t('files.text'), icon: <FileTextOutlined /> },
{ key: FileTypes.DOCUMENT, label: t('files.document'), icon: <FilePdfOutlined /> } { key: FileTypes.DOCUMENT, label: t('files.document'), icon: <FilePdfOutlined /> },
] ...geminiProviders.map((provider) => ({
key: 'gemini_' + provider.id,
label: provider.name,
icon: <FilePdfOutlined />
}))
].filter(Boolean) as MenuProps['items']
return ( return (
<Container> <Container>
@@ -84,41 +190,7 @@ const FilesPage: FC = () => {
<Menu selectedKeys={[fileType]} items={menuItems} onSelect={({ key }) => setFileType(key as FileTypes)} /> <Menu selectedKeys={[fileType]} items={menuItems} onSelect={({ key }) => setFileType(key as FileTypes)} />
</SideNav> </SideNav>
<TableContainer right> <TableContainer right>
{fileType === FileTypes.IMAGE && files?.length && files?.length > 0 ? ( <ContentView id={fileType} files={files} dataSource={dataSource} columns={columns} />
<Image.PreviewGroup>
<Row gutter={[16, 16]}>
{files?.map((file) => (
<Col key={file.id} xs={24} sm={12} md={8} lg={4} xl={3}>
<ImageWrapper>
<LoadingWrapper>
<Spin />
</LoadingWrapper>
<Image
src={FileManager.getFileUrl(file)}
style={{ height: '100%', objectFit: 'cover', cursor: 'pointer' }}
preview={{ mask: false }}
onLoad={(e) => {
const img = e.target as HTMLImageElement
img.parentElement?.classList.add('loaded')
}}
/>
<ImageInfo>
<div>{formatFileSize(file)}</div>
</ImageInfo>
</ImageWrapper>
</Col>
))}
</Row>
</Image.PreviewGroup>
) : (
<Table
dataSource={dataSource}
columns={columns}
style={{ width: '100%' }}
size="small"
pagination={{ pageSize: 100 }}
/>
)}
</TableContainer> </TableContainer>
</ContentContainer> </ContentContainer>
</Container> </Container>
@@ -149,72 +221,7 @@ const TableContainer = styled(Scrollbar)`
const FileNameText = styled.div` const FileNameText = styled.div`
font-size: 14px; font-size: 14px;
color: var(--color-text); color: var(--color-text);
` cursor: pointer;
const ImageWrapper = styled.div`
position: relative;
aspect-ratio: 1;
overflow: hidden;
border-radius: 8px;
background-color: var(--color-background-soft);
display: flex;
align-items: center;
justify-content: center;
border: 0.5px solid var(--color-border);
.ant-image {
height: 100%;
width: 100%;
opacity: 0;
transition:
opacity 0.3s ease,
transform 0.3s ease;
&.loaded {
opacity: 1;
}
}
&:hover {
.ant-image.loaded {
transform: scale(1.05);
}
div:last-child {
opacity: 1;
}
}
`
const LoadingWrapper = styled.div`
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
display: flex;
align-items: center;
justify-content: center;
background-color: var(--color-background-soft);
`
const ImageInfo = styled.div`
position: absolute;
bottom: 0;
left: 0;
right: 0;
background: rgba(0, 0, 0, 0.6);
color: white;
padding: 5px 8px;
opacity: 0;
transition: opacity 0.3s ease;
font-size: 12px;
> div:first-child {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
` `
const SideNav = styled.div` const SideNav = styled.div`
@@ -233,7 +240,7 @@ const SideNav = styled.div`
line-height: 36px; line-height: 36px;
margin: 4px 0; margin: 4px 0;
width: 100%; width: 100%;
border-radius: 16px; border-radius: var(--list-item-border-radius);
border: 0.5px solid transparent; border: 0.5px solid transparent;
&:hover { &:hover {
@@ -0,0 +1,98 @@
import { DeleteOutlined } from '@ant-design/icons'
import type { FileMetadataResponse } from '@google/generative-ai/server'
import { useProvider } from '@renderer/hooks/useProvider'
import { runAsyncFunction } from '@renderer/utils'
import { Table } from 'antd'
import type { ColumnsType } from 'antd/es/table'
import { FC, useCallback, useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next'
import styled from 'styled-components'
interface GeminiFilesProps {
id: string
}
const GeminiFiles: FC<GeminiFilesProps> = ({ id }) => {
const { provider } = useProvider(id)
const [files, setFiles] = useState<FileMetadataResponse[]>([])
const { t } = useTranslation()
const [loading, setLoading] = useState(false)
const fetchFiles = useCallback(async () => {
const { files } = await window.api.gemini.listFiles(provider.apiKey)
files && setFiles(files.filter((file) => file.state === 'ACTIVE'))
}, [provider])
const columns: ColumnsType<FileMetadataResponse> = [
{
title: t('files.name'),
dataIndex: 'displayName',
key: 'displayName'
},
{
title: t('files.type'),
dataIndex: 'mimeType',
key: 'mimeType'
},
{
title: t('files.size'),
dataIndex: 'sizeBytes',
key: 'sizeBytes',
render: (size: string) => `${(parseInt(size) / 1024 / 1024).toFixed(2)} MB`
},
{
title: t('files.created_at'),
dataIndex: 'createTime',
key: 'createTime',
render: (time: string) => new Date(time).toLocaleString()
},
{
title: t('files.actions'),
dataIndex: 'actions',
key: 'actions',
align: 'center',
render: (_, record) => {
return (
<DeleteOutlined
style={{ cursor: 'pointer', color: 'var(--color-error)' }}
onClick={() => {
setFiles(files.filter((file) => file.name !== record.name))
window.api.gemini.deleteFile(provider.apiKey, record.name).catch((error) => {
console.error('Failed to delete file:', error)
setFiles((prev) => [...prev, record])
})
}}
/>
)
}
}
]
useEffect(() => {
runAsyncFunction(async () => {
try {
setLoading(true)
await fetchFiles()
setLoading(false)
} catch (error: any) {
console.error('Failed to fetch files:', error)
window.message.error(error.message)
setLoading(false)
}
})
}, [fetchFiles])
useEffect(() => {
setFiles([])
}, [id])
return (
<Container>
<Table columns={columns} dataSource={files} rowKey="name" loading={loading} />
</Container>
)
}
const Container = styled.div``
export default GeminiFiles
+11 -2
View File
@@ -1,8 +1,8 @@
import { ArrowLeftOutlined, EnterOutlined, SearchOutlined } from '@ant-design/icons' import { ArrowLeftOutlined, EnterOutlined, SearchOutlined } from '@ant-design/icons'
import { Message, Topic } from '@renderer/types' import { Message, Topic } from '@renderer/types'
import { Input } from 'antd' import { Input, InputRef } from 'antd'
import { last } from 'lodash' import { last } from 'lodash'
import { FC, useState } from 'react' import { FC, useEffect, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import styled from 'styled-components' import styled from 'styled-components'
@@ -25,6 +25,7 @@ const TopicsPage: FC = () => {
const [stack, setStack] = useState<Route[]>(_stack) const [stack, setStack] = useState<Route[]>(_stack)
const [topic, setTopic] = useState<Topic | undefined>(_topic) const [topic, setTopic] = useState<Topic | undefined>(_topic)
const [message, setMessage] = useState<Message | undefined>(_message) const [message, setMessage] = useState<Message | undefined>(_message)
const inputRef = useRef<InputRef>(null)
_search = search _search = search
_stack = stack _stack = stack
@@ -58,6 +59,12 @@ const TopicsPage: FC = () => {
const isShow = (route: Route) => (last(stack) === route ? 'flex' : 'none') const isShow = (route: Route) => (last(stack) === route ? 'flex' : 'none')
useEffect(() => {
if (inputRef.current) {
inputRef.current.focus()
}
}, [])
return ( return (
<Container> <Container>
<Header> <Header>
@@ -72,7 +79,9 @@ const TopicsPage: FC = () => {
placeholder={t('history.search.placeholder')} placeholder={t('history.search.placeholder')}
type="search" type="search"
value={search} value={search}
autoFocus
allowClear allowClear
ref={inputRef}
onChange={(e) => setSearch(e.target.value.trimStart())} onChange={(e) => setSearch(e.target.value.trimStart())}
suffix={search.length >= 2 ? <EnterOutlined /> : <SearchOutlined />} suffix={search.length >= 2 ? <EnterOutlined /> : <SearchOutlined />}
onPressEnter={onSearch} onPressEnter={onSearch}
+12 -3
View File
@@ -1,5 +1,5 @@
import { useAssistants } from '@renderer/hooks/useAssistant' import { useAssistants } from '@renderer/hooks/useAssistant'
import { useShowAssistants } from '@renderer/hooks/useStore' import { useSettings } from '@renderer/hooks/useSettings'
import { useActiveTopic } from '@renderer/hooks/useTopic' import { useActiveTopic } from '@renderer/hooks/useTopic'
import NavigationService from '@renderer/services/NavigationService' import NavigationService from '@renderer/services/NavigationService'
import { Assistant } from '@renderer/types' import { Assistant } from '@renderer/types'
@@ -22,7 +22,7 @@ const HomePage: FC = () => {
const [activeAssistant, setActiveAssistant] = useState(state?.assistant || _activeAssistant || assistants[0]) const [activeAssistant, setActiveAssistant] = useState(state?.assistant || _activeAssistant || assistants[0])
const { activeTopic, setActiveTopic } = useActiveTopic(activeAssistant, state?.topic) const { activeTopic, setActiveTopic } = useActiveTopic(activeAssistant, state?.topic)
const { showAssistants } = useShowAssistants() const { showAssistants, showTopics, topicPosition } = useSettings()
_activeAssistant = activeAssistant _activeAssistant = activeAssistant
@@ -35,8 +35,17 @@ const HomePage: FC = () => {
state?.topic && setActiveTopic(state?.topic) state?.topic && setActiveTopic(state?.topic)
}, [state]) }, [state])
useEffect(() => {
const canMinimize = topicPosition == 'left' ? !showAssistants : !showAssistants && !showTopics
window.api.window.setMinimumSize(canMinimize ? 520 : 1080, 600)
return () => {
window.api.window.resetMinimumSize()
}
}, [showAssistants, showTopics, topicPosition])
return ( return (
<Container> <Container id="home-page">
<Navbar activeAssistant={activeAssistant} activeTopic={activeTopic} setActiveTopic={setActiveTopic} /> <Navbar activeAssistant={activeAssistant} activeTopic={activeTopic} setActiveTopic={setActiveTopic} />
<ContentContainer id="content-container"> <ContentContainer id="content-container">
{showAssistants && ( {showAssistants && (
@@ -16,9 +16,9 @@ const AttachmentPreview: FC<Props> = ({ files, setFiles }) => {
} }
return ( return (
<Container> <ContentContainer>
<Upload <Upload
listType="picture-card" listType={files.length > 20 ? 'text' : 'picture-card'}
fileList={files.map((file) => ({ fileList={files.map((file) => ({
uid: file.id, uid: file.id,
url: 'file://' + FileManager.getSafePath(file), url: 'file://' + FileManager.getSafePath(file),
@@ -27,17 +27,15 @@ const AttachmentPreview: FC<Props> = ({ files, setFiles }) => {
}))} }))}
onRemove={(item) => setFiles(files.filter((file) => item.uid !== file.id))} onRemove={(item) => setFiles(files.filter((file) => item.uid !== file.id))}
/> />
</Container> </ContentContainer>
) )
} }
const Container = styled.div` const ContentContainer = styled.div`
display: flex; max-height: 40vh;
flex-direction: row; overflow-y: auto;
gap: 10px; width: 100%;
padding: 10px 20px; padding: 10px 15px 0;
background: var(--color-background);
border-top: 1px solid var(--color-border-mute);
` `
export default AttachmentPreview export default AttachmentPreview
+185 -133
View File
@@ -13,7 +13,7 @@ import TranslateButton from '@renderer/components/TranslateButton'
import { isVisionModel, isWebSearchModel } from '@renderer/config/models' import { isVisionModel, isWebSearchModel } from '@renderer/config/models'
import db from '@renderer/databases' import db from '@renderer/databases'
import { useAssistant } from '@renderer/hooks/useAssistant' import { useAssistant } from '@renderer/hooks/useAssistant'
import { useRuntime } from '@renderer/hooks/useRuntime' import { modelGenerating, useRuntime } from '@renderer/hooks/useRuntime'
import { useMessageStyle, useSettings } from '@renderer/hooks/useSettings' import { useMessageStyle, useSettings } from '@renderer/hooks/useSettings'
import { useShortcut, useShortcutDisplay } from '@renderer/hooks/useShortcuts' import { useShortcut, useShortcutDisplay } from '@renderer/hooks/useShortcuts'
import { useShowTopics } from '@renderer/hooks/useStore' import { useShowTopics } from '@renderer/hooks/useStore'
@@ -24,8 +24,8 @@ import { estimateTextTokens as estimateTxtTokens } from '@renderer/services/Toke
import { translateText } from '@renderer/services/TranslateService' import { translateText } from '@renderer/services/TranslateService'
import store, { useAppDispatch, useAppSelector } from '@renderer/store' import store, { useAppDispatch, useAppSelector } from '@renderer/store'
import { setGenerating, setSearching } from '@renderer/store/runtime' import { setGenerating, setSearching } from '@renderer/store/runtime'
import { Assistant, FileType, KnowledgeBase, Message, Topic } from '@renderer/types' import { Assistant, FileType, KnowledgeBase, Message, Model, Topic } from '@renderer/types'
import { delay, getFileExtension, uuid } from '@renderer/utils' import { classNames, delay, getFileExtension, uuid } from '@renderer/utils'
import { documentExts, imageExts, textExts } from '@shared/config/constant' import { documentExts, imageExts, textExts } from '@shared/config/constant'
import { Button, Popconfirm, Tooltip } from 'antd' import { Button, Popconfirm, Tooltip } from 'antd'
import TextArea, { TextAreaRef } from 'antd/es/input/TextArea' import TextArea, { TextAreaRef } from 'antd/es/input/TextArea'
@@ -35,9 +35,12 @@ import { CSSProperties, FC, useCallback, useEffect, useMemo, useRef, useState }
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import styled from 'styled-components' import styled from 'styled-components'
import NarrowLayout from '../Messages/NarrowLayout'
import AttachmentButton from './AttachmentButton' import AttachmentButton from './AttachmentButton'
import AttachmentPreview from './AttachmentPreview' import AttachmentPreview from './AttachmentPreview'
import KnowledgeBaseButton from './KnowledgeBaseButton' import KnowledgeBaseButton from './KnowledgeBaseButton'
import MentionModelsButton from './MentionModelsButton'
import MentionModelsInput from './MentionModelsInput'
import SendMessageButton from './SendMessageButton' import SendMessageButton from './SendMessageButton'
import TokenCount from './TokenCount' import TokenCount from './TokenCount'
@@ -62,7 +65,8 @@ const Inputbar: FC<Props> = ({ assistant: _assistant, setActiveTopic }) => {
showInputEstimatedTokens, showInputEstimatedTokens,
clickAssistantToShowTopic, clickAssistantToShowTopic,
language, language,
autoTranslateWithSpace autoTranslateWithSpace,
sidebarIcons
} = useSettings() } = useSettings()
const [expended, setExpend] = useState(false) const [expended, setExpend] = useState(false)
const [estimateTokenCount, setEstimateTokenCount] = useState(0) const [estimateTokenCount, setEstimateTokenCount] = useState(0)
@@ -80,16 +84,21 @@ const Inputbar: FC<Props> = ({ assistant: _assistant, setActiveTopic }) => {
const spaceClickTimer = useRef<NodeJS.Timeout>() const spaceClickTimer = useRef<NodeJS.Timeout>()
const [isTranslating, setIsTranslating] = useState(false) const [isTranslating, setIsTranslating] = useState(false)
const [selectedKnowledgeBase, setSelectedKnowledgeBase] = useState<KnowledgeBase | undefined>(_base) const [selectedKnowledgeBase, setSelectedKnowledgeBase] = useState<KnowledgeBase | undefined>(_base)
const [mentionModels, setMentionModels] = useState<Model[]>([])
const isVision = useMemo(() => isVisionModel(model), [model]) const isVision = useMemo(() => isVisionModel(model), [model])
const supportExts = useMemo(() => [...textExts, ...documentExts, ...(isVision ? imageExts : [])], [isVision]) const supportExts = useMemo(() => [...textExts, ...documentExts, ...(isVision ? imageExts : [])], [isVision])
const showKnowledgeIcon = sidebarIcons.visible.includes('knowledge')
const estimateTextTokens = useCallback(debounce(estimateTxtTokens, 1000), []) const estimateTextTokens = useCallback(debounce(estimateTxtTokens, 1000), [])
const inputTokenCount = useMemo( const inputTokenCount = useMemo(
() => (showInputEstimatedTokens ? estimateTextTokens(text) || 0 : 0), () => (showInputEstimatedTokens ? estimateTextTokens(text) || 0 : 0),
[estimateTextTokens, showInputEstimatedTokens, text] [estimateTextTokens, showInputEstimatedTokens, text]
) )
const newTopicShortcut = useShortcutDisplay('new_topic') const newTopicShortcut = useShortcutDisplay('new_topic')
const newContextShortcut = useShortcutDisplay('toggle_new_context')
const cleanTopicShortcut = useShortcutDisplay('clear_topic')
const inputEmpty = isEmpty(text.trim()) && files.length === 0 const inputEmpty = isEmpty(text.trim()) && files.length === 0
_text = text _text = text
@@ -97,9 +106,7 @@ const Inputbar: FC<Props> = ({ assistant: _assistant, setActiveTopic }) => {
_base = selectedKnowledgeBase _base = selectedKnowledgeBase
const sendMessage = useCallback(async () => { const sendMessage = useCallback(async () => {
if (generating) { await modelGenerating()
return
}
if (inputEmpty) { if (inputEmpty) {
return return
@@ -124,6 +131,10 @@ const Inputbar: FC<Props> = ({ assistant: _assistant, setActiveTopic }) => {
message.files = await FileManager.uploadFiles(files) message.files = await FileManager.uploadFiles(files)
} }
if (mentionModels.length > 0) {
message.mentions = mentionModels
}
EventEmitter.emit(EVENT_NAMES.SEND_MESSAGE, message) EventEmitter.emit(EVENT_NAMES.SEND_MESSAGE, message)
setText('') setText('')
@@ -132,7 +143,7 @@ const Inputbar: FC<Props> = ({ assistant: _assistant, setActiveTopic }) => {
setTimeout(() => resizeTextArea(), 0) setTimeout(() => resizeTextArea(), 0)
setExpend(false) setExpend(false)
}, [generating, inputEmpty, text, assistant.id, assistant.topics, selectedKnowledgeBase, files]) }, [inputEmpty, text, assistant.id, assistant.topics, selectedKnowledgeBase, files, mentionModels])
const translate = async () => { const translate = async () => {
if (isTranslating) { if (isTranslating) {
@@ -178,7 +189,7 @@ const Inputbar: FC<Props> = ({ assistant: _assistant, setActiveTopic }) => {
if (expended) { if (expended) {
if (event.key === 'Escape') { if (event.key === 'Escape') {
return setExpend(false) return onToggleExpended()
} }
} }
@@ -207,10 +218,7 @@ const Inputbar: FC<Props> = ({ assistant: _assistant, setActiveTopic }) => {
} }
const addNewTopic = useCallback(async () => { const addNewTopic = useCallback(async () => {
if (generating) { await modelGenerating()
window.message.warning({ content: t('message.switch.disabled'), key: 'generating' })
return
}
const topic = getDefaultTopic(assistant.id) const topic = getDefaultTopic(assistant.id)
@@ -226,7 +234,7 @@ const Inputbar: FC<Props> = ({ assistant: _assistant, setActiveTopic }) => {
setActiveTopic(topic) setActiveTopic(topic)
clickAssistantToShowTopic && setTimeout(() => EventEmitter.emit(EVENT_NAMES.SHOW_TOPIC_SIDEBAR), 0) clickAssistantToShowTopic && setTimeout(() => EventEmitter.emit(EVENT_NAMES.SHOW_TOPIC_SIDEBAR), 0)
}, [addTopic, assistant, clickAssistantToShowTopic, generating, setActiveTopic, setModel, t]) }, [addTopic, assistant, clickAssistantToShowTopic, setActiveTopic, setModel])
const clearTopic = async () => { const clearTopic = async () => {
if (generating) { if (generating) {
@@ -274,25 +282,31 @@ const Inputbar: FC<Props> = ({ assistant: _assistant, setActiveTopic }) => {
const onPaste = useCallback( const onPaste = useCallback(
async (event: ClipboardEvent) => { async (event: ClipboardEvent) => {
for (const file of event.clipboardData?.files || []) { const clipboardText = event.clipboardData?.getData('text')
event.preventDefault() if (clipboardText) {
// Prioritize the text when pasting.
// handled by the default event
} else {
for (const file of event.clipboardData?.files || []) {
event.preventDefault()
if (file.path === '') { if (file.path === '') {
if (file.type.startsWith('image/')) { if (file.type.startsWith('image/')) {
const tempFilePath = await window.api.file.create(file.name) const tempFilePath = await window.api.file.create(file.name)
const arrayBuffer = await file.arrayBuffer() const arrayBuffer = await file.arrayBuffer()
const uint8Array = new Uint8Array(arrayBuffer) const uint8Array = new Uint8Array(arrayBuffer)
await window.api.file.write(tempFilePath, uint8Array) await window.api.file.write(tempFilePath, uint8Array)
const selectedFile = await window.api.file.get(tempFilePath) const selectedFile = await window.api.file.get(tempFilePath)
selectedFile && setFiles((prevFiles) => [...prevFiles, selectedFile]) selectedFile && setFiles((prevFiles) => [...prevFiles, selectedFile])
break break
}
} }
}
if (file.path) { if (file.path) {
if (supportExts.includes(getFileExtension(file.path))) { if (supportExts.includes(getFileExtension(file.path))) {
const selectedFile = await window.api.file.get(file.path) const selectedFile = await window.api.file.get(file.path)
selectedFile && setFiles((prevFiles) => [...prevFiles, selectedFile]) selectedFile && setFiles((prevFiles) => [...prevFiles, selectedFile])
}
} }
} }
} }
@@ -348,6 +362,14 @@ const Inputbar: FC<Props> = ({ assistant: _assistant, setActiveTopic }) => {
} }
}) })
useShortcut('clear_topic', () => {
clearTopic()
})
useShortcut('toggle_new_context', () => {
onNewContext()
})
useEffect(() => { useEffect(() => {
const _setEstimateTokenCount = debounce(setEstimateTokenCount, 100, { leading: false, trailing: true }) const _setEstimateTokenCount = debounce(setEstimateTokenCount, 100, { leading: false, trailing: true })
const unsubscribes = [ const unsubscribes = [
@@ -381,122 +403,152 @@ const Inputbar: FC<Props> = ({ assistant: _assistant, setActiveTopic }) => {
} }
}, []) }, [])
useEffect(() => {
window.addEventListener('focus', () => {
textareaRef.current?.focus()
})
}, [])
const textareaRows = window.innerHeight >= 1000 || isBubbleStyle ? 2 : 1 const textareaRows = window.innerHeight >= 1000 || isBubbleStyle ? 2 : 1
const handleKnowledgeBaseSelect = (base?: KnowledgeBase) => { const handleKnowledgeBaseSelect = (base?: KnowledgeBase) => {
setSelectedKnowledgeBase(base) setSelectedKnowledgeBase(base)
} }
const onMentionModel = useCallback(
(model: Model) => {
const isSelected = mentionModels.some((m) => m.id === model.id)
if (isSelected) {
setMentionModels(mentionModels.filter((m) => m.id !== model.id))
} else {
setMentionModels([...mentionModels, model])
}
},
[mentionModels]
)
const handleRemoveModel = (model: Model) => {
setMentionModels(mentionModels.filter((m) => m.id !== model.id))
}
return ( return (
<Container onDragOver={handleDragOver} onDrop={handleDrop}> <Container onDragOver={handleDragOver} onDrop={handleDrop} className="inputbar">
<AttachmentPreview files={files} setFiles={setFiles} /> <NarrowLayout style={{ width: '100%' }}>
<InputBarContainer id="inputbar" className={inputFocus ? 'focus' : ''} ref={containerRef}> <InputBarContainer
<Textarea id="inputbar"
value={text} className={classNames('inputbar-container', inputFocus && 'focus')}
onChange={(e) => setText(e.target.value)} ref={containerRef}>
onKeyDown={handleKeyDown} <AttachmentPreview files={files} setFiles={setFiles} />
placeholder={isTranslating ? t('chat.input.translating') : t('chat.input.placeholder')} <MentionModelsInput selectedModels={mentionModels} onRemoveModel={handleRemoveModel} />
autoFocus <Textarea
contextMenu="true" value={text}
variant="borderless" onChange={(e) => setText(e.target.value)}
rows={textareaRows} onKeyDown={handleKeyDown}
ref={textareaRef} placeholder={isTranslating ? t('chat.input.translating') : t('chat.input.placeholder')}
style={{ fontSize }} autoFocus
styles={{ textarea: TextareaStyle }} contextMenu="true"
onFocus={() => setInputFocus(true)} variant="borderless"
onBlur={() => setInputFocus(false)} spellCheck={false}
onInput={onInput} rows={textareaRows}
disabled={searching} ref={textareaRef}
onPaste={(e) => onPaste(e.nativeEvent)} style={{ fontSize }}
onClick={() => searching && dispatch(setSearching(false))} styles={{ textarea: TextareaStyle }}
/> onFocus={() => setInputFocus(true)}
<Toolbar> onBlur={() => setInputFocus(false)}
<ToolbarMenu> onInput={onInput}
<Tooltip placement="top" title={t('chat.input.new_topic', { Command: newTopicShortcut })} arrow> disabled={searching}
<ToolbarButton type="text" onClick={addNewTopic}> onPaste={(e) => onPaste(e.nativeEvent)}
<FormOutlined /> onClick={() => searching && dispatch(setSearching(false))}
</ToolbarButton> />
</Tooltip> <Toolbar>
{isWebSearchModel(model) && ( <ToolbarMenu>
<Tooltip placement="top" title={t('chat.input.web_search')} arrow> <Tooltip placement="top" title={t('chat.input.new_topic', { Command: newTopicShortcut })} arrow>
<ToolbarButton type="text" onClick={addNewTopic}>
<FormOutlined />
</ToolbarButton>
</Tooltip>
<MentionModelsButton
mentionModels={mentionModels}
onMentionModel={onMentionModel}
ToolbarButton={ToolbarButton}
/>
{isWebSearchModel(model) && (
<Tooltip placement="top" title={t('chat.input.web_search')} arrow>
<ToolbarButton
type="text"
onClick={() => updateAssistant({ ...assistant, enableWebSearch: !assistant.enableWebSearch })}>
<GlobalOutlined
style={{ color: assistant.enableWebSearch ? 'var(--color-link)' : 'var(--color-icon)' }}
/>
</ToolbarButton>
</Tooltip>
)}
<Tooltip placement="top" title={t('chat.input.clear', { Command: cleanTopicShortcut })} arrow>
<Popconfirm
title={t('chat.input.clear.content')}
placement="top"
onConfirm={clearTopic}
okButtonProps={{ danger: true }}
icon={<QuestionCircleOutlined style={{ color: 'red' }} />}
okText={t('chat.input.clear.title')}>
<ToolbarButton type="text">
<ClearOutlined />
</ToolbarButton>
</Popconfirm>
</Tooltip>
<Tooltip placement="top" title={t('chat.input.settings')} arrow>
<ToolbarButton <ToolbarButton
type="text" type="text"
onClick={() => updateAssistant({ ...assistant, enableWebSearch: !assistant.enableWebSearch })}> onClick={() => {
<GlobalOutlined !showTopics && toggleShowTopics()
style={{ color: assistant.enableWebSearch ? 'var(--color-link)' : 'var(--color-icon)' }} setTimeout(() => EventEmitter.emit(EVENT_NAMES.SHOW_CHAT_SETTINGS), 0)
/> }}>
<ControlOutlined />
</ToolbarButton> </ToolbarButton>
</Tooltip> </Tooltip>
)} {showKnowledgeIcon && (
<Tooltip placement="top" title={t('chat.input.clear')} arrow> <KnowledgeBaseButton
<Popconfirm selectedBase={selectedKnowledgeBase}
title={t('chat.input.clear.content')} onSelect={handleKnowledgeBaseSelect}
placement="top" ToolbarButton={ToolbarButton}
onConfirm={clearTopic} disabled={files.length > 0}
okButtonProps={{ danger: true }} />
icon={<QuestionCircleOutlined style={{ color: 'red' }} />} )}
okText={t('chat.input.clear')}> <AttachmentButton model={model} files={files} setFiles={setFiles} ToolbarButton={ToolbarButton} />
<ToolbarButton type="text"> <Tooltip placement="top" title={t('chat.input.new.context', { Command: newContextShortcut })} arrow>
<ClearOutlined /> <ToolbarButton type="text" onClick={onNewContext}>
</ToolbarButton> <PicCenterOutlined />
</Popconfirm>
</Tooltip>
<Tooltip placement="top" title={t('chat.input.settings')} arrow>
<ToolbarButton
type="text"
onClick={() => {
!showTopics && toggleShowTopics()
setTimeout(() => EventEmitter.emit(EVENT_NAMES.SHOW_CHAT_SETTINGS), 0)
}}>
<ControlOutlined />
</ToolbarButton>
</Tooltip>
<KnowledgeBaseButton
selectedBase={selectedKnowledgeBase}
onSelect={handleKnowledgeBaseSelect}
ToolbarButton={ToolbarButton}
disabled={files.length > 0}
/>
<AttachmentButton
model={model}
files={files}
setFiles={setFiles}
ToolbarButton={ToolbarButton}
disabled={!!selectedKnowledgeBase}
/>
<ToolbarButton type="text" onClick={onNewContext}>
<Tooltip placement="top" title={t('chat.input.new.context')}>
<PicCenterOutlined />
</Tooltip>
</ToolbarButton>
<Tooltip placement="top" title={expended ? t('chat.input.collapse') : t('chat.input.expand')} arrow>
<ToolbarButton type="text" onClick={onToggleExpended}>
{expended ? <FullscreenExitOutlined /> : <FullscreenOutlined />}
</ToolbarButton>
</Tooltip>
<TokenCount
estimateTokenCount={estimateTokenCount}
inputTokenCount={inputTokenCount}
contextCount={contextCount}
ToolbarButton={ToolbarButton}
onClick={onNewContext}
/>
</ToolbarMenu>
<ToolbarMenu>
{!language.startsWith('en') && (
<TranslateButton text={text} onTranslated={onTranslated} isLoading={isTranslating} />
)}
{generating && (
<Tooltip placement="top" title={t('chat.input.pause')} arrow>
<ToolbarButton type="text" onClick={onPause} style={{ marginRight: -2, marginTop: 1 }}>
<PauseCircleOutlined style={{ color: 'var(--color-error)', fontSize: 20 }} />
</ToolbarButton> </ToolbarButton>
</Tooltip> </Tooltip>
)} <Tooltip placement="top" title={expended ? t('chat.input.collapse') : t('chat.input.expand')} arrow>
{!generating && <SendMessageButton sendMessage={sendMessage} disabled={generating || inputEmpty} />} <ToolbarButton type="text" onClick={onToggleExpended}>
</ToolbarMenu> {expended ? <FullscreenExitOutlined /> : <FullscreenOutlined />}
</Toolbar> </ToolbarButton>
</InputBarContainer> </Tooltip>
<TokenCount
estimateTokenCount={estimateTokenCount}
inputTokenCount={inputTokenCount}
contextCount={contextCount}
ToolbarButton={ToolbarButton}
onClick={onNewContext}
/>
</ToolbarMenu>
<ToolbarMenu>
{!language.startsWith('en') && (
<TranslateButton text={text} onTranslated={onTranslated} isLoading={isTranslating} />
)}
{generating && (
<Tooltip placement="top" title={t('chat.input.pause')} arrow>
<ToolbarButton type="text" onClick={onPause} style={{ marginRight: -2, marginTop: 1 }}>
<PauseCircleOutlined style={{ color: 'var(--color-error)', fontSize: 20 }} />
</ToolbarButton>
</Tooltip>
)}
{!generating && <SendMessageButton sendMessage={sendMessage} disabled={generating || inputEmpty} />}
</ToolbarMenu>
</Toolbar>
</InputBarContainer>
</NarrowLayout>
</Container> </Container>
) )
} }
@@ -62,6 +62,7 @@ const KnowledgeBaseButton: FC<Props> = ({ selectedBase, onSelect, disabled, Tool
<Popover <Popover
placement="top" placement="top"
content={<KnowledgeBaseSelector selectedBase={selectedBase} onSelect={onSelect} />} content={<KnowledgeBaseSelector selectedBase={selectedBase} onSelect={onSelect} />}
overlayStyle={{ maxWidth: 400 }}
trigger="click"> trigger="click">
<ToolbarButton type="text" onClick={() => selectedBase && onSelect(undefined)} disabled={disabled}> <ToolbarButton type="text" onClick={() => selectedBase && onSelect(undefined)} disabled={disabled}>
<FileSearchOutlined style={{ color: selectedBase ? 'var(--color-link)' : 'var(--color-icon)' }} /> <FileSearchOutlined style={{ color: selectedBase ? 'var(--color-link)' : 'var(--color-icon)' }} />
@@ -0,0 +1,155 @@
import { PushpinOutlined } from '@ant-design/icons'
import ModelTags from '@renderer/components/ModelTags'
import { getModelLogo, isEmbeddingModel } 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, Dropdown, Tooltip } from 'antd'
import { first, sortBy } from 'lodash'
import { FC, useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next'
import styled, { createGlobalStyle } from 'styled-components'
interface Props {
mentionModels: Model[]
onMentionModel: (model: Model) => void
ToolbarButton: any
}
const MentionModelsButton: FC<Props> = ({ onMentionModel: onSelect, ToolbarButton }) => {
const { providers } = useProviders()
const [pinnedModels, setPinnedModels] = useState<string[]>([])
const { t } = useTranslation()
useEffect(() => {
const loadPinnedModels = async () => {
const setting = await db.settings.get('pinned:models')
setPinnedModels(setting?.value || [])
}
loadPinnedModels()
}, [])
const togglePin = async (modelId: string) => {
const newPinnedModels = pinnedModels.includes(modelId)
? pinnedModels.filter((id) => id !== modelId)
: [...pinnedModels, modelId]
await db.settings.put({ id: 'pinned:models', value: newPinnedModels })
setPinnedModels(newPinnedModels)
}
const modelMenuItems = providers
.filter((p) => p.models && p.models.length > 0)
.map((p) => {
const filteredModels = sortBy(p.models, ['group', 'name'])
.filter((m) => !isEmbeddingModel(m))
.map((m) => ({
key: getModelUniqId(m),
label: (
<ModelItem>
<span>
{m?.name} <ModelTags model={m} />
</span>
{/* <Checkbox checked={selectedModels.some((sm) => sm.id === m.id)} /> */}
<PinIcon
onClick={(e) => {
e.stopPropagation()
togglePin(getModelUniqId(m))
}}
$isPinned={pinnedModels.includes(getModelUniqId(m))}>
<PushpinOutlined />
</PinIcon>
</ModelItem>
),
icon: (
<Avatar src={getModelLogo(m.id)} size={24}>
{first(m.name)}
</Avatar>
),
onClick: () => {
onSelect(m)
}
}))
return filteredModels.length > 0
? {
key: p.id,
label: p.isSystem ? t(`provider.${p.id}`) : p.name,
type: 'group' as const,
children: filteredModels
}
: null
})
.filter(Boolean)
if (pinnedModels.length > 0) {
const pinnedItems = modelMenuItems
.flatMap((p) => p?.children || [])
.filter((m) => pinnedModels.includes(m.key))
.map((m) => ({ ...m, key: m.key + 'pinned' }))
if (pinnedItems.length > 0) {
modelMenuItems.unshift({
key: 'pinned',
label: t('models.pinned'),
type: 'group' as const,
children: pinnedItems
})
}
}
return (
<>
<DropdownMenuStyle />
<Dropdown menu={{ items: modelMenuItems }} trigger={['click']} overlayClassName="mention-models-dropdown">
<Tooltip placement="top" title={t('agents.edit.model.select.title')} arrow>
<ToolbarButton type="text">
<i className="iconfont icon-at" style={{ fontSize: 18 }}></i>
</ToolbarButton>
</Tooltip>
</Dropdown>
</>
)
}
const DropdownMenuStyle = createGlobalStyle`
.mention-models-dropdown {
.ant-dropdown-menu {
max-height: 400px;
}
}
`
const ModelItem = styled.div`
display: flex;
align-items: center;
justify-content: space-between;
font-size: 14px;
width: 100%;
gap: 16px;
&:hover {
.pin-icon {
opacity: 0.3;
}
}
`
const PinIcon = styled.span.attrs({ className: 'pin-icon' })<{ $isPinned: boolean }>`
margin-left: auto;
padding: 0 8px;
opacity: ${(props) => (props.$isPinned ? 1 : 'inherit')};
transition: opacity 0.2s;
right: 0;
color: ${(props) => (props.$isPinned ? 'var(--color-primary)' : 'inherit')};
transform: ${(props) => (props.$isPinned ? 'rotate(-45deg)' : 'none')};
opacity: 0;
&:hover {
opacity: 1 !important;
color: ${(props) => (props.$isPinned ? 'var(--color-primary)' : 'inherit')};
}
`
export default MentionModelsButton
@@ -0,0 +1,26 @@
import { Model } from '@renderer/types'
import { Flex, Tag } from 'antd'
import { FC } from 'react'
import styled from 'styled-components'
const MentionModelsInput: FC<{
selectedModels: Model[]
onRemoveModel: (model: Model) => void
}> = ({ selectedModels, onRemoveModel }) => {
return (
<Container gap="4px 0" wrap>
{selectedModels.map((model) => (
<Tag bordered={false} color="processing" key={model.id} closable onClose={() => onRemoveModel(model)}>
@{model.name}
</Tag>
))}
</Container>
)
}
const Container = styled(Flex)`
width: 100%;
padding: 10px 15px 0;
`
export default MentionModelsInput
@@ -66,6 +66,9 @@ const Container = styled.div`
font-size: 10px; font-size: 10px;
margin-right: 3px; margin-right: 3px;
} }
@media (max-width: 600px) {
display: none;
}
` `
const Text = styled.div` const Text = styled.div`

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