Compare commits

...

336 Commits

Author SHA1 Message Date
kangfenmao
a25f4e90dd chore(version): 1.0.5 2025-03-05 15:53:08 +08:00
kangfenmao
3d701b98aa feat(knowledge-base): Enhance file processing and localization
- Update useKnowledgeFiles hook to filter only completed processing files
- Improve localization messages for knowledge base file management
- Refactor delete and remove file button translations across locales
- Update confirmation messages to clarify file deletion behavior
2025-03-05 14:59:30 +08:00
kangfenmao
dcaac54c75 feat(app-update): Refactor update handling and add manual update dialog
- Modify AppUpdater to separate update dialog logic
- Add new IPC handler for manually showing update dialog
- Update renderer hooks and store to track downloaded update state
- Switch import for UpdateInfo from electron-updater to builder-util-runtime
2025-03-05 14:33:10 +08:00
suyao
b2b89a1339 fix(settings): Improve reasoning effort segmented control with 'off' option 2025-03-05 11:18:20 +08:00
ousugo
4692f98770 feat: Enable conditional settings shortcut based on user preferences 2025-03-05 11:17:34 +08:00
SuYao
86a3a108a7 feat: support table and hyperlink when export to word (#2837)
* feat(export): Enhance markdown-to-docx export with table support

Add comprehensive table rendering capabilities to the ExportService, including:
- Support for table headers and body rows
- Configurable cell styling and alignment
- Handling of inline text formatting within table cells

* feat(export): Add hyperlink support in markdown-to-docx export

Enhance ExportService to handle hyperlinks during document export:
- Implement link detection in inline tokens
- Create ExternalHyperlink with proper styling
- Preserve link text and URL in exported document
2025-03-05 11:16:45 +08:00
kangfenmao
5ec4403bfb feat: Improve message filtering across providers
- Add new `filterUserRoleStartMessages` function in MessagesService
- Update Anthropic, Gemini, and OpenAI providers to use new message filtering
- Refactor message handling to ensure user messages start the conversation
- Remove redundant message filtering logic from individual providers
2025-03-05 11:11:02 +08:00
kangfenmao
ec0be1ff27 feat: Enhance image upload and model-specific message handling
- Add vision model check before image upload in Inputbar
- Implement flexible message start forcing for specific models
- Improve provider-level message routing logic
2025-03-04 21:02:08 +08:00
kangfenmao
516315ac45 feat(i18n): Add cherrycss.com link to custom CSS settings across locales 2025-03-04 17:48:06 +08:00
美兰十三
ff55739376 feat(显示设置): 自定义css 增加cherrycss入口方便快速获取样式 (#2807)
* feat(显示设置): 自定义css 增加cherrycss入口方便快速获取样式

这是个无用的小辣鸡功能 = =、

* feat(显示设置): 自定义css 增加cherrycss入口方便快速获取样式

这是个无用的小辣鸡功能 = =、
2025-03-04 17:31:36 +08:00
Konjac-XZ
1e24b7bc45 fix: Token counter display issues when translating assistant messages. 2025-03-04 17:11:08 +08:00
kangfenmao
dd92dca34b chore: Move html-to-image package to dev dependencies 2025-03-04 15:42:38 +08:00
SuYao
a592fdc550 refactor: Replace html2canvas with html-to-image for improved sup element (#2739)
* refactor: Replace html2canvas with html-to-image for improved screenshot capture

* refactor: Simplify scrollable div capture method

* refactor: Simplify captureScrollableDivAsBlob method

* fix: Specify PNG format in captureScrollableDivAsBlob method

* feat: Add error handling for large content dimensions in screenshot capture

* fix: Reorder error messages in en-us.json locale file
2025-03-04 15:37:29 +08:00
Asurada
846e7ca097 feat: Update model label format to include provider information (#2793)
* feat: Update model label format to include provider information

* feat: Improve AddModelPopup layout and button styling
2025-03-04 15:36:07 +08:00
SuYao
93c2a94658 feat: Add code block wrapping functionality (#2411)
Signed-off-by: suyao <sy20010504@gmail.com>
2025-03-04 12:30:22 +08:00
FischLu
309b66e4df feat: add resize handle to input textarea with drag interaction (#2174)
* feat: add resize handle to input textarea with drag interaction

* handle auto size inputbar

* optimize auto resize function and add i18n

* fix: expand button bug in inputbar and rebase to latest main

* rebase to main
2025-03-04 11:44:52 +08:00
ousugo
4d9476e99b feat: Save proxy setting 2025-03-04 11:27:15 +08:00
Wei GENG
46f796a74c feat: Add Agent German Tutor (#2748) 2025-03-04 10:38:50 +08:00
kangfenmao
00bf28b999 fix: Correct Tencent Cloud provider identifier and update UI styling
- Fixed typo in Tencent Cloud TI provider key from 'tentent-cloud-ti' to 'tencent-cloud-ti'
- Enhanced ProviderSettings search input with search icon and custom styling
- Adjusted button border radius for consistent UI design
2025-03-04 10:37:41 +08:00
fullex
640d3783a0 feat: add search function on provider list & optimize ui behaviors when dragging provider item (#2706)
* 添加“腾讯云TI”供应商及其支持的deepseek模型

* add search feature for model providers & adjust ui element behaviors when dragging

* dev merge fix

* merge fix
2025-03-04 10:27:18 +08:00
one
0e4f06e86a feat: improve SelectModelPopup (#2740)
* fix: do not match provider id for non-system models

* feat: match multiple words
2025-03-04 10:11:54 +08:00
kangfenmao
886a7ec1e9 feat: Add knowledge base file management functionality
- Implemented `useKnowledgeFiles` hook for managing knowledge base files
- Added localization support for knowledge base file management in multiple languages
- Created UI option to remove all knowledge base files in DataSettings
- Updated file size formatting utility function
- Modified ContentView and FilesPage to use file size correctly
2025-03-03 23:20:31 +08:00
kangfenmao
37cf7427f9 feat: Implement comprehensive data restore functionality with progress tracking
- Added RestorePopup component for visualizing restore process
- Enhanced BackupManager with detailed restore progress tracking
- Implemented file copy progress and stage tracking during restore
- Updated localization files with restore progress translations
- Integrated restore progress reporting to renderer process
2025-03-03 22:32:55 +08:00
kangfenmao
e69d0c89a6 feat: Add comprehensive backup progress tracking and UI
- Implemented detailed backup progress tracking in BackupManager
- Added new BackupPopup component for backup process visualization
- Enhanced backup process with file copy progress and stage tracking
- Updated localization files with backup progress translations
- Integrated backup progress reporting to renderer process
2025-03-03 22:22:29 +08:00
kangfenmao
581e2fb786 refactor: Improve reasoning effort configuration for Claude models
- Refactored reasoning effort handling for Claude models
- Added type definition for ReasoningEffort
- Simplified budget token calculation
- Improved type safety and readability of the method
2025-03-03 18:44:18 +08:00
MyPrototypeWhat
13b465fe73 fix: shadow markdown (#1871)
问题:[Bug]: 当模型回复的html代码没有正确在代码框中时,html代码内容会影响到UI界面 #1767
原因:解析html之后css会污染应用样式
解决:将markdown完全放入shadow dom中,shadow dom天然隔绝样式,即可解决

* feat: Conditionally hide thinking loader for paused messages

* feat: Implement Shadow DOM for Markdown rendering

* feat: Add StyleProvider to Shadow DOM Markdown rendering

* fix: Refactor Markdown rendering with inline ShadowDOM component

Modify ReactMarkdown component to use style component for ShadowDOM rendering instead of wrapping component, simplifying the rendering approach

---------

Co-authored-by: lizhixuan <zhixuan.li@banosuperapp.com>
2025-03-03 18:23:18 +08:00
Asurada
a12d10f4f7 feat: Added reset avatar feature and center align items in userpopup (#2658)
* feat: Add reset avatar functionality and update localization strings

* fix: Center align upload and reset avatar options in UserPopup
2025-03-03 17:58:15 +08:00
kangfenmao
e8bfb2b49b refactor: Update Tencent Cloud TI provider identifier and assets 2025-03-03 17:48:17 +08:00
fullex
ae995182b2 feat: 添加“腾讯云TI”供应商及其支持的deepseek模型 (#2685) 2025-03-03 17:44:12 +08:00
kangfenmao
59c69e065c refactor: Enhance message context menu with improved text selection and copying 2025-03-03 17:40:17 +08:00
Asurada
4ca2d61ccc feat: Add quote feature (#2657)
* feat: Add text quoting functionality to messages

* feat(i18n): add quote message to multiple language files
2025-03-03 17:25:34 +08:00
Konjac-XZ
d62ff69351 fix: Token estimate count cannot be updated on the UI when typing quickly. 2025-03-03 17:23:07 +08:00
one
012e79a7e2 fix: show correct logo for qwen-omni-turbo (#2656)
* fix: show correct logo for qwen-omni-turbo

* fix: match omni in the middle
2025-03-03 17:17:25 +08:00
kangfenmao
97dc80a07f refactor: Improve reasoning effort configuration for Anthropic models
- Refactored getReasoningEffort method to handle reasoning configuration more robustly
- Added type definitions for reasoning effort and configuration
- Simplified logic for calculating budget tokens
- Improved type safety and readability of the method
2025-03-03 17:12:50 +08:00
kangfenmao
b974f8537f chore(version): 1.0.4 2025-03-02 21:35:49 +08:00
kangfenmao
c32e17968e fix: Adjust UI styling for consistent font sizes and layout spacing 2025-03-02 21:35:30 +08:00
kangfenmao
cf09d1d44d chore(version): 1.0.3 2025-03-02 20:54:41 +08:00
MyPrototypeWhat
ad39d8774d refactor: Improve Ellipsis component and usage (#2603)
* refactor: Improve Ellipsis component and usage

- Modify Ellipsis component to use children instead of text prop
- Add support for multi-line and single-line ellipsis with styled-components
- Update KnowledgeContent to use new Ellipsis component structure
- Enhance ClickableSpan styling for better text truncation

* fix: Improve text wrapping in Ellipsis component

Add overflow-wrap: break-word to ensure long words are properly truncated in multi-line ellipsis

* refactor: Improve link and tooltip rendering in KnowledgeContent

- Wrap links with ClickableSpan for better interaction and styling
- Adjust Tooltip and Ellipsis placement for improved readability
- Remove unnecessary inline styling for links in ItemInfo

---------

Co-authored-by: lizhixuan <zhixuan.li@banosuperapp.com>
2025-03-02 20:43:56 +08:00
kangfenmao
687f140a5c docs: Add comprehensive documentation for KnowledgeService concurrent processing mechanism
- Created detailed technical documentation explaining the KnowledgeService's task queue and workload management
- Added JSDoc comment to KnowledgeService.ts to provide high-level overview
- Documented key aspects of concurrent task processing, including workload evaluation, task state management, and resource optimization strategies
2025-03-02 20:36:13 +08:00
icinggslits
1b09bb47bf feat: Improve file upload for the knowledge base (#2544)
* feat: Improve file upload for the knowledge base

* feat: Improve file upload for the knowledge base

* feat: Improve file upload for the knowledge base

---------

Co-authored-by: 亢奋猫 <kangfenmao@qq.com>
2025-03-02 20:21:23 +08:00
kangfenmao
808b457503 feat: Add Zhihu mini app to the platform
- Added Zhihu (知乎直答) mini app logo
- Updated minapps configuration to include Zhihu mini app
- Modified store migration to enable Zhihu mini app by default
2025-03-02 20:12:54 +08:00
kangfenmao
92e054569c feat: Add Cici mini app and update mini apps grid layout
- Added Cici mini app logo in WebP format
- Updated mini apps grid to display 8 columns instead of 6
- Updated store migration to include Cici in enabled mini apps
- Updated Cici app logo import path
2025-03-02 20:06:35 +08:00
Sharker
2f22e68559 chore: revert test code 2025-03-02 20:00:35 +08:00
Sharker
53a8628fab feat: added cici for mini app 2025-03-02 20:00:35 +08:00
shiquda
df04503674 feat: add you.com miniapp 2025-03-02 19:58:36 +08:00
周子健
1fe74fa753 feat: add Civil Engineer assistant (#2640) 2025-03-02 19:57:44 +08:00
PilgrimLyieu
55a9be2fa5 feat: Add KaTeX copy-tex and mhchem support 2025-03-02 19:55:11 +08:00
suyao
fdf4821d56 fix: Fix app quit exceptionally on MacOS's full screen mode add Add Escape key support for exiting fullscreen mode 2025-03-02 19:53:12 +08:00
kangfenmao
84e6caa846 refactor: Extract StyleSheetManager into a separate context component
- Move StyleSheetManager logic from App.tsx to a new dedicated context component
- Simplify App.tsx by importing the new StyleSheetManager
- Preserve existing prop validation and filtering logic
2025-03-02 19:51:20 +08:00
lizhixuan
3bc8dfdf8c feat: Enhance Styled Components configuration and prop handling
- Add Babel plugin for styled-components with performance and naming optimizations
- Integrate @emotion/is-prop-valid for improved prop filtering in StyleSheetManager
- Update Antd Input components to use variant="borderless" instead of deprecated bordered prop
- Add new dependencies for styled-components configuration
2025-03-02 19:47:11 +08:00
kangfenmao
ed96940e82 fix: Improve metrics completion tokens handling 2025-03-02 19:41:06 +08:00
ZhuangYumin
1c60375d71 fix: fix bug of metrics missing 2025-03-02 19:38:20 +08:00
hanzhao
c9699609ed fix bug: #2306 2025-03-02 19:37:33 +08:00
kangfenmao
f91caff7ec feat: Conditionally render reasoning effort control for reasoning models 2025-03-02 09:23:39 +08:00
kangfenmao
11bd55701c revert: some proxy errors in main process (#2294) 2025-03-01 23:17:44 +08:00
kangfenmao
efa9c6c546 refactor: Centralize emoji detection utility and improve avatar rendering 2025-03-01 23:16:12 +08:00
Pin Studios
92ab67eb3d feat: Add emoji avatar support with image upload and picker & support i18n language (#2473) 2025-03-01 21:33:47 +08:00
SuYao
ae11490f87 feat: Add reasoning effort control for Claude 3.7 (#2540)
* feat: Add reasoning effort control for Anthropic models with Anthropic Provider and OpenAI Provider

- Add reasoning effort settings with low/medium/high options
- Implement reasoning effort for Claude 3.7 Sonnet models
- Update localization tips for reasoning effort
- Enhance provider handling of reasoning effort parameters

* fix: Extract o1-mini and o1-preview

* fix: Add OpenAI o-series model to ReasoningModel

* fix: Improve OpenAI o-series model detection

* style: Reduce font size

* fix: Add default token handling using DEFAULT_MAX_TOKENS

* fix: Add beta parameter for Anthropic reasoning models
2025-03-01 21:22:12 +08:00
George·Dong
956c2f683d feat: add MessageGroupMenuBar Tooltip (#2600)
* feat: add MessageGroupMenuBar Tooltip

* feat: update MenuBar Tooltip i18n
2025-03-01 20:28:40 +08:00
Jorben
d01f793558 feat: Add Hunyuan TurboS model (launched on February 26)
- Add hunyuan-turbos-latest
- Add hunyuan-embedding
- Fix the issue of the embedding model displaying the internet icon (when the provider fully supports web search)
2025-03-01 20:27:41 +08:00
kangfenmao
94d9b79957 chore(version): 1.0.2 2025-03-01 12:25:33 +08:00
kangfenmao
78a7b2759e fix: Simplify model selection logic in message regeneration
Modify the model selection process to directly use the appropriate model based on the conversation context, removing redundant conditional logic
2025-03-01 11:20:58 +08:00
kangfenmao
27c0edfb79 fix: Improve ModelAvatar and AssistantItem styling for better layout and text handling
- Add min-width and min-height to ModelAvatar to ensure consistent sizing
- Separate AssistantName into a new styled component for better text overflow handling
- Refactor AssistantItem to improve text display and icon alignment
2025-03-01 10:41:24 +08:00
kangfenmao
59b1d8bcc4 style: Refine Segmented component styling with transparent background and rounded corners 2025-03-01 01:44:02 +08:00
kangfenmao
741d84b4d3 feat: Sort model groups alphabetically in provider settings
- Implement alphabetical sorting for model groups
- Use lodash's sortBy and toPairs to order model groups
- Modify rendering to use sorted model groups
2025-03-01 01:00:44 +08:00
suyao
5bacf048f2 fix: The proxy configuration is not correctly passed. 2025-03-01 00:42:56 +08:00
kangfenmao
1d4916c516 feat: Extract ModelEditContent into separate component and refine UI styling
- Create new ModelEditContent component in ProviderSettings
- Separate model editing logic from ProviderSetting
- Adjust styling for AssistantItem and TopicsTab list items
- Refine Segmented component styling with transparent background
- Improve modal layout and interaction for model type configuration
2025-03-01 00:40:55 +08:00
kangfenmao
8e1207c2a2 feat: Add assistant icon display toggle in settings
- Implement new setting to show/hide model icons in assistant list
- Add localization support for new assistant settings
- Update UI to conditionally render model avatar in AssistantItem
- Modify settings store to include showAssistantIcon state
- Enhance display settings with new toggle switch for assistant icon
2025-03-01 00:18:47 +08:00
kangfenmao
ac92f1a783 refactor: Simplify Segmented component styling
- Remove custom Segmented styling from AntdProvider
- Update HomeTabs Segmented component with refined styles
- Consolidate Segmented styling in a single location
- Improve visual consistency with rounded corners and transparent background
2025-02-28 23:25:36 +08:00
kangfenmao
28c59ea436 feat: Enhance UI styling and interaction details
- Update window vibrancy to 'sidebar' for better visual effect
- Refine input bar styling with softer background and rounded corners
- Adjust toolbar button sizes and styling
- Modify topic position selection to use Segmented component
- Tweak light theme background opacity
2025-02-28 23:25:36 +08:00
ousugo
9e808208ab fix: Normal content is misidentified as chain of thought content 2025-02-28 22:06:08 +08:00
Asurada
feefaaf3e3 feat: Added More Settings section with risk warnings in model type configuration (#2560)
* feat: add "More Settings" option in multiple languages and enhance model type selection UI

* feat: add "More Settings" option with warnings and confirmation prompts in multiple languages

* fix: improve modal close handling and reset model type visibility
2025-02-28 22:04:44 +08:00
kangfenmao
31078b8ec5 chore: Update Xirang provider configuration and localization 2025-02-28 18:20:23 +08:00
lbc123456
f3f32cc591 feat: 模型服务添加天翼云模型 (#2238)
* feat: 模型服务添加天翼云模型

* feat:add website

* fix: id is duplicate

---------

Co-authored-by: 李保成 <libaocheng@cndatacom.com>
2025-02-28 17:40:57 +08:00
kangfenmao
f489b034b5 Revert "feat: allow o1 streaming"
This reverts commit d4848faa5a.
2025-02-28 14:20:37 +08:00
ZhuangYumin
3d9d5b6263 feat: let quick assistant follow theme setting 2025-02-28 13:14:45 +08:00
Pres
89440c9c10 feat: add support for GPT 4.5 2025-02-28 13:14:05 +08:00
Pres
4c0f358323 fix:Claude model name typo 2025-02-28 11:53:30 +08:00
kangfenmao
ad01fc43e5 Revert "docs: remove Chinese issue templates for bug reports, feature requests, and questions"
This reverts commit ab5e830ed1.
2025-02-28 10:34:28 +08:00
xiaotianxt
9b17416f9c fix: remove restrictions on mac alt+x shortcuts
Restrictions are introduced in earlier beta versions of MacOS Sequoia.
However, in the public release, Apple revoke this restrictions.

Alt+[Char] shortcut has no restrictions now.
2025-02-28 10:30:47 +08:00
onevcat
cda4edfb7f Remove unused protocol register and open url code 2025-02-28 08:19:04 +08:00
eeee0717
acc803aa43 fix bug: #2474 2025-02-28 08:18:14 +08:00
TW
2ab8f325df feat: Enhance Assistant and Topic components with title attributes for better accessibility 2025-02-28 08:17:01 +08:00
onevcat
a68cbe4438 Switch to @use and module system for scss 2025-02-28 08:16:03 +08:00
onevcat
646d0e4ccb Use the Russian language label
This ensures the language label aligns with others (using its own language name instead of English).
2025-02-27 20:40:52 +08:00
Asurada
a7a82be083 feat: Add model editing functionality to provider settings (#2243) 2025-02-27 17:00:01 +08:00
CherryLover
c0117c25ac feat: 为模型选择弹窗添加键盘导航功能 (#2057)
* feat: 为模型选择弹窗添加键盘导航功能

* fix(SelectModelPopup): 改进键盘导航和项目筛选
2025-02-27 16:58:11 +08:00
SuYao
d51da99b8f fix: some proxy errors in main process (#2294)
* formatter proxy config type

* fix: some proxy errors in main process

* chore: Remove debug logging in ProxyManager
2025-02-27 16:46:05 +08:00
ZhuangYumin
d4848faa5a feat: allow o1 streaming 2025-02-27 16:42:11 +08:00
NJczh
bfeca0b383 fix:修复备份恢复完成后可能无权限清理临时目录 (#2247) 2025-02-27 16:40:47 +08:00
xiaotianxt
79c7c3dc1c feat: Improve settings navigation hotkey behavior
- Add location check to prevent navigating to settings when already on settings page
- Enable hotkey on content editable and form elements
- Use useLocation to track current route
2025-02-27 16:39:52 +08:00
SuYao
b2ebbc1e30 fix: favicon can't load error (#2426)
* feat: Implement robust favicon loading with fallback mechanisms

* refactor: Improve favicon loading state and use Promise Method

* refactor: Extract FallbackFavicon into a separate component

* feat: Add Splitbee favicon service to fallback favicon URLs
2025-02-27 16:38:14 +08:00
Cicada
50e2dd0ec0 docs: Convert Traditional Chinese to Simplified 2025-02-27 16:37:06 +08:00
yeongpin
5e753de71c feat: Add GIF support for avatar uploads without compression 2025-02-27 15:02:02 +08:00
kangfenmao
6bc6dab879 docs: Update README files to mark networking and first official version as completed 2025-02-26 19:30:30 +08:00
kangfenmao
7d794d33dd chore(version): 1.0.1 2025-02-26 17:28:08 +08:00
kangfenmao
aab318e8ca fix: Adjust file upload and navbar styling for cross-platform compatibility
- Update Dragger component to use customRequest for file uploads
- Add Windows-specific padding adjustment for navbar right section
2025-02-26 13:33:21 +08:00
kangfenmao
6554a3817b feat: Add O3 provider and refactor migration logic 2025-02-26 13:33:11 +08:00
icinggslits
7d76db40e8 feat: Add search button to the knowledge base, and adjusted the display for drag-and-drop uploads (#2365)
* feat: Add search button to the knowledge base, and adjusted the display for drag-and-drop uploads

* feat: Add search button to the knowledge base, and adjusted the display for drag-and-drop uploads
2025-02-26 12:06:34 +08:00
ZhuangYumin
186c82e355 feat: add better support for openrouter thinking format 2025-02-26 12:04:47 +08:00
首都爱护动物协会
67311f1cbe add o3 2025-02-26 12:03:08 +08:00
kangfenmao
62d969335e chore: Improve localization formatting for Notion and Yuque error messages
- Standardize spacing and capitalization in Chinese (Simplified and Traditional) locales
- Enhance readability of error and configuration messages
- Consistent formatting for API keys, database IDs, and URLs
2025-02-26 00:21:49 +08:00
kangfenmao
c6eb77ab8b feat: Enhance web search settings with blacklist and result configuration
- Update Tavily description across locales to emphasize AI agent capabilities
- Add blacklist functionality for web search results
- Introduce configurable maximum search results
- Improve localization for web search settings in multiple languages
2025-02-26 00:16:13 +08:00
kangfenmao
7e17987fa3 chore: Reorganize and clean up localization files
This commit involves minor reorganization and cleanup of translation files across multiple languages (en-us, ja-jp, ru-ru, zh-cn, zh-tw). Changes include:

- Reordering of some translation keys
- Removing duplicate entries
- Slight restructuring of nested translation objects
- Minor formatting adjustments
2025-02-26 00:02:03 +08:00
Chen Tao
4bc69b7c5e feat: add web search settings (#2314)
* fix: add time when using web search

* feat: add optional

* chore

* chore

* chore

* clean code

* feat: set search max results

* feat: add manual blacklist

* clean code

* chore

* chore

* clean
2025-02-25 23:46:51 +08:00
ousugo
e08029a6f5 feat: Improve think tag processing with more robust parsing 2025-02-25 23:46:25 +08:00
kangfenmao
93d68102d6 fix: Update favicon service for message citations 2025-02-25 23:43:22 +08:00
kangfenmao
f448d8a8db fix: assistant and agent emoji 2025-02-25 21:00:23 +08:00
kangfenmao
a047048f69 refactor: Extract file drop handling logic to separate utility function 2025-02-25 19:53:04 +08:00
icinggslits
aec14567ee feat: Support more file drag and drop scenarios (#2278)
* feat: Support more file drag and drop scenarios

* feat: Support more file drag and drop scenarios
2025-02-25 19:46:32 +08:00
kangfenmao
bad89e3d28 chore: Remove social media links from About settings 2025-02-25 19:40:52 +08:00
kangfenmao
408f2b16ad fix: Standardize Notion connection error messages across locales 2025-02-25 19:34:15 +08:00
kangfenmao
d6b87ece23 fix: 修复深色模式下气泡样式的用户名样式错误问题
This reverts commit 69513cc76e.
2025-02-25 19:31:19 +08:00
icinggslits
91104e288c fix: Export image in dark mode (#2332)
Co-authored-by: 亢奋猫 <kangfenmao@qq.com>
2025-02-25 19:20:11 +08:00
eeee0717
aeeded2aa1 fix bug: 深色模式对话导出渲染不正确 2025-02-25 19:12:44 +08:00
ousugo
b10198de1f feat: Enhance reference prompt with language matching instruction 2025-02-25 19:09:56 +08:00
Yuankui Li
0789ccedbb feat: 知识库支持更多txt based格式 2025-02-25 19:07:06 +08:00
Aglargil
0a5401174b feat: add export to Yuque 2025-02-25 19:06:37 +08:00
Godcoper
69513cc76e fix: 修复深色模式下气泡样式的用户名样式错误问题 2025-02-25 18:58:10 +08:00
suyao
5a471125db chore: Update Grok app logo and related images 2025-02-25 15:01:01 +08:00
kangfenmao
06b2ca9149 refactor: Move fullscreen notification logic to a separate hook 2025-02-25 13:32:15 +08:00
ousugo
26dd931f70 feat: Add fullscreen mode notification with localized message 2025-02-25 13:28:29 +08:00
kangfenmao
68df5cd211 chore: Remove console logs and clean up code
This commit removes unnecessary console.log statements and makes minor code cleanup in several files:
- PlantUML component
- WebSearchSettings
- WebSearchService
- Store settings

Additionally, it updates the store migration to handle LM Studio provider addition and reverts the store version
2025-02-25 13:27:31 +08:00
Chen Tao
c7071a98f0 fix: 网络搜索添加时间信息 (#2255)
* fix: add time when using web search

* feat: add optional

* chore

* chore

* chore

* clean code
2025-02-25 13:20:53 +08:00
zzsx
ff14dcc559 feat: 添加更多C++文件扩展名支持
在 `textExts` 数组中添加了以下C++文件扩展名:
- `.hpp`: C++ 头文件
- `.cc`: C++ 源文件
- `.cxx`: C++ 源文件
- `.cppm`: C++20 模块接口文件
- `.ipp`: 模板实现文件
- `.ixx`: C++20 模块实现文件
2025-02-25 13:19:32 +08:00
preszzz
bb02ca83dc feat: add new claude 3.7 model support 2025-02-25 13:17:09 +08:00
Alice39s
4d9e842381 feat: Add support for Gemini 2.0 and update Yi model configurations
- Added `gemini-2.0` to `visionAllowedModels`.
- Introduced new Gemini models in `SYSTEM_MODELS`:
  - `gemini-1.5-flash-8b`
  - `gemini-2.0-flash`
- Updated Yi model configurations:
  - Renamed `yi-lightning` to "Yi Lightning".
  - Deprecated `yi-medium`, `yi-large`, and `yi-vision` (replaced by `yi-lightning` per documentation).
  - Added `yi-vision-v2` as a new version of Yi Vision.
2025-02-25 08:29:59 +08:00
ousugo
0165bcdce3 feat: Adapt glmzeropreview thinking chain format, enhance message thought processing with flexible extractors, 2025-02-25 08:29:16 +08:00
自由的世界人
f015c78060 fix: folder spelling change (#2242) 2025-02-25 08:26:25 +08:00
Zicheng Zhang
c233ba0a1c fix: count reasoning content into token estimation (#2276) 2025-02-25 08:24:53 +08:00
eeee0717
88dd75827a fix bug: uniqueIds is null when uploaded directory 2025-02-24 21:40:17 +08:00
kangfenmao
2ab63f2e4c fix: Update Infini provider API URL to include '/maas' path 2025-02-24 18:08:22 +08:00
icinggslits
91bf356c73 feat: add shortcut to home window of mini app 2025-02-24 12:58:40 +08:00
vic123
28c0748001 fix: undesired assistant selection menu popup when no ' ' before '@' 2025-02-24 12:57:26 +08:00
SagoLu
3108a1c0b3 fix:修复在备份和恢复备份时由于无法删除临时目录导致的功能终止 (#2224)
* refactor: Remove chat settings toolbar button from input bar

* fix:修复在备份时,由于临时文件只读导致的备份失败

* fix:修复在恢复备份时,由于临时文件只读导致的备份失败

---------

Co-authored-by: kangfenmao <kangfenmao@qq.com>
2025-02-24 12:56:19 +08:00
Alice39s
6cfa7d0eb6 feat: Improve support for the Codestral and additional Mistral models
- Added new image asset `codestral.png` for Codestral model.
- Updated `embedings.ts` to include the `mistral-embed` embedding model with `max_context: 8000`.
- Updated `models.ts` to:
  - Add support for Codestral model with `codestral-latest` ID.
  - Introduce new Mistral models, including:
    - `ministral-3b-latest`
    - `ministral-8b-latest`
    - `mistral-large-latest`
    - `mistral-small-latest`
    - `mistral-embed`
  - Organize models into appropriate groups (e.g., "Mistral Chat", "Mistral Code").
- Adjusted `getModelLogo` function to include Codestral and Ministral logos.
2025-02-24 12:55:28 +08:00
kangfenmao
da6c80ebc2 refactor: Rename onResend to onResendUserMessage for clarity 2025-02-24 12:50:29 +08:00
Asurada
af1a9868db feat: Add remark function to knowledge url (#2210)
* chore: Update .gitignore to exclude .cursor/rules

* feat: Add remark function to knowledge url
2025-02-24 12:47:47 +08:00
dengshenkk
f87ba144c8 feat: 用户侧消息快捷重试功能 (#2221)
* Update MessageMenubar.tsx

用户发送消息可以重试, 方便在接口错误后进行重试

* Update MessageMenubar.tsx
2025-02-24 12:41:59 +08:00
ousugo
8d61cbcae9 feat: Improve knowledge base creation popup input focus 2025-02-24 12:40:46 +08:00
ousugo
c61dde5085 feat: Enhance knowledge search popup input focus behavior 2025-02-24 12:40:46 +08:00
sijie-chan
3015e90925 fix: 添加多个目录到知识库多个进度之间展示混淆 2025-02-24 12:40:00 +08:00
牡丹凤凰
b3629e83f2 Update README.ja.md 2025-02-24 07:23:23 +08:00
牡丹凤凰
cef9312e7e Update README.zh.md 2025-02-24 07:22:55 +08:00
牡丹凤凰
802622646a Update README.md (#2213) 2025-02-24 07:22:12 +08:00
kangfenmao
ce70c7239c chore(version): 1.0.0 2025-02-23 15:10:24 +08:00
kangfenmao
c354537f30 feat: Add Tavily dark mode logo and improve web search settings UI 2025-02-23 15:10:01 +08:00
kangfenmao
fb6b0b0c97 refactor: Rename AssistantItemComponent to AssistantItem and update imports 2025-02-23 14:30:17 +08:00
wnzzer
fc59144b1d fix:清空话题总是修复当前话题 (#2167) 2025-02-23 14:26:31 +08:00
kangfenmao
cacd0a1387 feat: Improve web search UI and localization 2025-02-23 14:22:09 +08:00
kangfenmao
af9763d142 chore: Update Tavily core package and remove js-tiktoken dependency 2025-02-23 11:01:30 +08:00
kangfenmao
b9402a8370 feat: add web search 2025-02-23 11:01:29 +08:00
kangfenmao
97a08f00a3 chore(version): 0.9.30 2025-02-23 07:02:00 +08:00
kangfenmao
4e20bd1ef8 feat: Enhance assistant emoji and popup UI interactions 2025-02-23 06:45:35 +08:00
George·Dong
5c2d936688 feat: 添加Notion导出自动分页功能 (#2098)
* fix: 长对话Notion导出失败(分页导出)

* feat: 添加Notion导出自动分页设置
2025-02-23 06:45:26 +08:00
ousugo
6b34aac263 feat: Auto-select newly added knowledge base 2025-02-23 00:17:46 +08:00
kangfenmao
f59f6ade69 chore(version): 0.9.29 2025-02-22 22:58:19 +08:00
ousugo
50478c600f fix: Regenerat messages don't use @ specified models 2025-02-22 22:35:02 +08:00
ousugo
bba5cac246 fix: Restore textarea focus after selecting mention model via mouse 2025-02-22 22:34:16 +08:00
shijian
b9b31aed52 fix: 复制聊天信息按钮显示不全 2025-02-22 22:32:30 +08:00
kangfenmao
40203fb721 fix: Remove LM Studio provider from initial state 2025-02-22 22:31:37 +08:00
kangfenmao
b83343a8b9 fix: Remove duplicate empty LM Studio providers 2025-02-22 13:35:14 +08:00
aber0724
7677850547 feat: Add Monica minapp 2025-02-22 11:27:30 +08:00
sijie-chan
d0e233f1b3 fix: 添加目录到知识库任意一个失败界面上会展示失败 2025-02-22 11:26:31 +08:00
kangfenmao
903d0043ba chore(version): 0.9.28 2025-02-21 18:25:30 +08:00
kangfenmao
811815d69d feat: update miniapp logo 2025-02-21 18:25:23 +08:00
kangfenmao
d5b9c35f0a feat: Enhance topic message clearing functionality
关于“清空话题”的Bug反馈 #2107

close #2107
2025-02-21 16:48:04 +08:00
suyao
83c8f06b81 fix: add first message handling in mini home window 2025-02-21 16:32:56 +08:00
kangfenmao
acb2ea30fb feat: add export function to message 2025-02-21 16:27:07 +08:00
kangfenmao
55317b5608 refactor: Remove chat settings toolbar button from input bar 2025-02-21 14:31:05 +08:00
kangfenmao
b42a5c5e63 chore: Upgrade Yarn and TypeScript patch versions 2025-02-21 14:18:16 +08:00
kangfenmao
cc76fe19f9 feat: Synchronize and clean up localization files
This commit involves several improvements to localization files across different languages:
- Reordered and cleaned up translation keys
- Removed redundant entries
- Ensured consistent ordering of keys
- Added missing translations for various features
- Normalized whitespace and formatting
2025-02-21 14:15:36 +08:00
落子
cf2d7ba8b4 feat: add "Copy as" options to topics right click menu (#2095)
* feat: Add copy topic as image and Markdown functionality

* add translation
2025-02-21 13:59:34 +08:00
Avan
1c163c55b8 feat: baidu ai search 2025-02-21 13:58:19 +08:00
kangfenmao
69bb661b5a feat: Add API host formatting utility function 2025-02-21 13:50:24 +08:00
lizhixuan
f062c56de4 refactor: Move abort controller to utils and update imports 2025-02-21 12:50:18 +08:00
lizhixuan
4c9bd02f8e feat: Add message completion abort functionality 2025-02-21 12:50:18 +08:00
kangfenmao
241cb0c0d8 refactor: Simplify translation history clear button configuration 2025-02-21 11:17:41 +08:00
ousugo
6a57973864 feat: Add newline tooltip for translation input 2025-02-21 11:16:02 +08:00
kangfenmao
369f629206 refactor: Simplify embedding model regex pattern 2025-02-21 09:46:26 +08:00
kangfenmao
09a8f83650 refactor: Clean up MinAppsPopover component formatting 2025-02-20 15:35:51 +08:00
kangfenmao
40912eaaf4 refactor: Simplify topic deletion interaction 2025-02-20 15:33:24 +08:00
ousugo
0d236a94ab feat: Add direct topic deletion shortcut 2025-02-20 15:19:32 +08:00
ousugo
3a936e0f26 feat: Add confirmation dialog for topic deletion 2025-02-20 15:19:32 +08:00
Yrom
81a35d129d feat: add developer tools button in MinApp for development mode 2025-02-20 15:19:10 +08:00
ousugo
e2d0c3bbce feat: Add emoji selection for assistant name 2025-02-20 15:17:04 +08:00
kangfenmao
12c9d810a2 feat: Add C and C++ file extensions to supported text files
close #2066
2025-02-20 14:45:11 +08:00
kangfenmao
b18b161094 fix: Update migration config to set default thought auto-collapse 2025-02-20 12:24:24 +08:00
ousugo
32da853f27 feat: Add thought content auto-collapse setting 2025-02-20 12:22:45 +08:00
ousugo
bf51a0b5c6 feat: Add copy functionality to message reasoning content 2025-02-20 12:22:45 +08:00
kangfenmao
ae71a7be9e refactor: Simplify translation page layout and styling 2025-02-20 12:16:38 +08:00
Chen Tao
0fb6795833 feat: add shortcuts to open settings page (#1964)
* feat: add shortcuts to open settings page
* refactor: simplify code
* fix: change to useHotKeys
2025-02-20 12:10:16 +08:00
jtsang4
dd6d228760 feat: Add backspace handling to remove last mentioned model 2025-02-20 12:08:25 +08:00
eeee0717
8c5999dc82 fix: knowledge base status problem 2025-02-20 12:07:05 +08:00
kangfenmao
31b0fbf775 fix: Improve null handling in code block and syntax highlighting 2025-02-20 12:04:29 +08:00
kangfenmao
39fe583030 refactor: Remove default grid settings from migration config 2025-02-20 11:54:21 +08:00
littel_penguin66
5c19695e21 feat: add dify miniapp (#1999)
* add Dify miniapp

* add wpslingxi,lechat,abacus miniapps

* resize pic

* change png to webp

* update 71
2025-02-20 11:52:36 +08:00
FischLu
6e4610e337 fix: fix translation 2025-02-20 11:50:49 +08:00
onlyfeng
eb2439b90c fix(KnowledgeBase): handle JSON parse failure by falling back to text file processing to avoid error display affecting normal progress indication 2025-02-20 11:44:36 +08:00
kangfenmao
a4a0980cd3 fix: Ignore hidden files when traversing directories 2025-02-20 11:43:01 +08:00
FischLu
7d99765589 fix: fix the issue where the menu scrolls back to the top after clicking an option 2025-02-19 21:40:32 +08:00
kangfenmao
5f6cf1bd66 docs: Update README files with comprehensive TODO list and minor formatting improvements 2025-02-19 20:07:10 +08:00
Teo
02930a2793 feat: 添加翻译历史功能 2025-02-19 19:38:59 +08:00
Yrom
b31b1c7908 feat: Support dark theme for PlantUML 2025-02-19 18:37:05 +08:00
Yrom
d0ee764732 feat: Add PlantUML diagram support (via PlantUML official online server) 2025-02-19 18:37:05 +08:00
ousugo
01cd10b364 refactor: Simplify model type detection with utility functions 2025-02-19 18:33:51 +08:00
ousugo
29ba156b9a feat: Add Doubao provider support for model type detection
(cherry picked from commit 008b84e128484c98eb59c75d8f44cbc320509049)
2025-02-19 18:33:51 +08:00
ousugo
e541c7b429 feat: Add model generation check before narrow mode toggle 2025-02-19 18:33:16 +08:00
ousugo
5f4142f0c4 chore: Enhance issue templates with improved guidance and visual context 2025-02-19 17:19:15 +08:00
jiangjiwei
95bbc70c93 feat: 为 Notion 导出添加可配置的页面名称 key 2025-02-19 17:18:45 +08:00
ousugo
4add56ae6a fix: Ensure model is updated when setting default model 2025-02-19 17:13:52 +08:00
Teo
16f87537a2 refactor: 翻译页UI重构 2025-02-19 15:53:52 +08:00
kangfenmao
7f05626a8f chore(version): 0.9.27 2025-02-19 10:21:12 +08:00
kangfenmao
2094e2201a feat: Add web search support for OpenRouter provider 2025-02-19 10:11:49 +08:00
icinggslits
e0fcdf43c5 feature: Adaptive height of textarea on translation page 2025-02-19 09:57:26 +08:00
kangfenmao
affc866c17 feat: Add default API host for DMX provider in migration 2025-02-19 09:45:41 +08:00
kangfenmao
799267049f fix: Safely update topic with existing topic data 2025-02-19 09:35:10 +08:00
kangfenmao
cb8d47a17b fix: Improve knowledge base processing and deletion handling 2025-02-19 09:28:36 +08:00
kangfenmao
c494288f7b refactor: Simplify DragableList styling and remove unnecessary margins 2025-02-19 09:21:10 +08:00
suyao
2c3f89dbde fix: update model identification with provider-specific uniqueness 2025-02-19 09:14:45 +08:00
ousugo
4721a660fa feat: Add German language support to translation options 2025-02-19 08:21:00 +08:00
George·Dong
6aaa3def0d feat: 添加Notion文档按钮Tooltip 2025-02-19 08:20:21 +08:00
George·Dong
045708d9b3 fix: 修改导出到Notion的相关提示 2025-02-19 08:20:21 +08:00
icinggslits
9ffe92d378 fix: Update language options promptly 2025-02-19 08:18:41 +08:00
首都爱护动物协会
7159481217 Updated provider information 2025-02-19 07:10:45 +08:00
kangfenmao
d07e136037 fix: Add top margin to 'Add Assistant' button in AssistantsTab 2025-02-18 21:17:13 +08:00
Yrom
b38a9c954a feat: Enable search capability for Qwen commercial version model 2025-02-18 21:14:43 +08:00
kangfenmao
7139d5093a chore(version): 0.9.26 2025-02-18 20:55:04 +08:00
kangfenmao
9e283d6930 fix: Update agent knowledge base field name and handling 2025-02-18 20:55:04 +08:00
Chen Tao
c9a4e12765 feat: artifacts add open external (#1812)
* feat: artifacts add open external

* fix: remove modal
2025-02-18 19:56:39 +08:00
Teo
7bd644451b fix: 解决生成过程中出现错误内容被清空覆盖问题 2025-02-18 19:46:50 +08:00
美兰十三
5a00bdcbc6 fix: 修复mac下快捷键注册control被替换成command的问题 2025-02-18 19:46:28 +08:00
eeee0717
3c958c3d11 feat: 目录进度可视化 2025-02-18 19:45:47 +08:00
kangfenmao
1d5ace0fb2 feat: Add 'off' option for reasoning effort in assistant settings 2025-02-18 18:16:14 +08:00
ousugo
f8fce871da fix: Recalculate token consumption after modifying the message, resolve #1829 2025-02-18 16:34:52 +08:00
ousugo
de76d3fedc fix: Improve DragableList component styling and placeholder handling 2025-02-18 16:26:34 +08:00
lucifer9
b2c6662192 adjust Notion database ID input width in DataSettings 2025-02-18 16:22:53 +08:00
lucifer9
bf8a7c01b0 Refactor WebDAV i18n and UI for improved flexibility and localization
- i18n Updates:
   - Refactored WebDAV-related translations into nested JSON structures for better organization.
   - Added support for pluralization in time intervals (minutes and hours) across all locales (en-us, ja-jp, ru-ru, zh-cn, zh-tw).

 - UI Enhancements:
   - Updated `DataSettings` and `WebDavSettings` components to use the new i18n keys for time intervals.
   - Improved the `Select` dropdown for sync intervals with dynamic pluralization based on locale.
   - Adjusted input field widths for better alignment and consistency.

 - Code Cleanup:
   - Removed redundant comments and unused code in `WebDavSettings.tsx`.
   - Simplified button and input styling for a cleaner layout.
2025-02-18 16:22:53 +08:00
ousugo
fb8ed35b59 fix: Clicking the taskbar icon while enable the Quick Assistant can't open the main window 2025-02-18 16:01:10 +08:00
ousugo
7c4d81c108 feat: Add kimi-latest model support in vision and model logos 2025-02-18 15:50:01 +08:00
kangfenmao
7199f73e06 style: Adjust horizontal message layout display property 2025-02-18 15:48:13 +08:00
Teo
869e56b53c style: 优化聊天窗口UI (#1881) 2025-02-18 11:43:42 +08:00
MyPrototypeWhat
f99851fb6b feat: Conditionally hide thinking loader for paused messages (#1875)
Co-authored-by: lizhixuan <zhixuan.li@banosuperapp.com>
2025-02-18 11:10:48 +08:00
kangfenmao
c94450db44 chore(version): 0.9.25 2025-02-18 10:41:44 +08:00
kangfenmao
195ef92acc feat: Add size prop to MessageThought Collapse component 2025-02-18 10:12:30 +08:00
kangfenmao
a67370426b fix: Handle undefined provider in model name generation 2025-02-18 09:56:26 +08:00
kangfenmao
9d35205681 feat: Improve system prompt styling with theme-aware background 2025-02-18 09:42:52 +08:00
icinggslits
98087e50db feat: Backspace deletes clipboard text in MiniApp 2025-02-18 08:13:56 +08:00
icinggslits
bedac4f59d fix: init zoom 2025-02-18 08:11:38 +08:00
ousugo
aba3874797 refactor: Improve PromptPopup text area focus and cursor placement 2025-02-18 08:10:58 +08:00
ousugo
3383280726 feat: Improve text edit popup focus and cursor placement 2025-02-18 08:10:58 +08:00
kangfenmao
0c13e708b9 refactor: Extract message group menu bar into a separate component 2025-02-17 23:21:24 +08:00
kangfenmao
bc77c423b3 fix: Adjust paragraph margin when followed by list 2025-02-17 23:08:17 +08:00
kangfenmao
4821756301 fix: Conditionally render message group border based on popover state 2025-02-17 23:02:49 +08:00
Chen Tao
78290ca70e feat: add knowledge base filter (#1822)
* feat: add search filter

* chore
2025-02-17 22:18:10 +08:00
kangfenmao
7feeb07624 refactor: Extract message group settings into a separate component 2025-02-17 22:14:47 +08:00
luwux
93e28ed916 improvement(shortcut): Supports Option + Space on Mac
Supports the Option (⌥) + Space shortcut, as it's the default shortcut for ChatGPT Desktop app to show popup.
2025-02-17 19:04:08 +08:00
ousugo
b4aaf052fe feat: Add page title for Cherry Studio, resolve #1222 2025-02-17 19:02:56 +08:00
rebecca554owen
b37e0389fc fix 2025-02-17 18:38:22 +08:00
kangfenmao
e1ebe069a5 feat: Add grid mode settings for message display 2025-02-17 18:35:36 +08:00
kangfenmao
d73912ee3b feat: Enhance Notion settings with placeholders and help icon 2025-02-17 17:19:24 +08:00
kangfenmao
f81c7c7a6c feat: update knowledge base file upload hint text 2025-02-17 16:50:34 +08:00
FischLu
5a7bcd5997 feat: improve model mention autocomplete behavior under IME 2025-02-17 16:38:44 +08:00
duanyongcheng
09a347cae4 feat: show provider in mesage 2025-02-17 16:38:00 +08:00
Chen Tao
266f909045 feat: allow knowledge base multiple search #1346 (#1773)
* feat: agent can select multiple knowledge bases

* feat: basic search multiple knowledge base

* fix bug: knowledge base is delete, assistants and agents sync delete

* fix bug: assistant and knowledge base button sync

* feat: allow to search multiple knowledge base

* chore: finish rebase to upstream/main
2025-02-17 16:36:25 +08:00
cl1107
bad2f15c1f feat: Add a new grid mode for message display. (#1626)
* chore(version): 0.9.23

* feat(renderer): 新增网格模式的消息展示方式

* feat(message): 新增消息网格展示相关设置

* 根据 gridPopoverTrigger 属性动态设置消息分组的样式

* 在 MessageMenubar 组件中,各个按钮 click 事件阻止事件冒泡,避免打开 popover

* 多模型回答样式添加网格模式并优化消息样式

---------

Co-authored-by: kangfenmao <kangfenmao@qq.com>
2025-02-17 16:36:01 +08:00
shniubobo
e3115d00bf fix: Rendering error with MathJax for Chinese text 2025-02-17 16:24:38 +08:00
首都爱护动物协会
0c0ccf3d11 update provider info 2025-02-17 16:21:09 +08:00
ousugo
2076e6f998 fix: open current webview URL when launching external link 2025-02-17 16:20:12 +08:00
ousugo
b49d80b78d fix: Clicking the help button always opens a new webview
(cherry picked from commit 4939afafabcbfb294f00d21053939cad8238731e)
2025-02-17 16:19:31 +08:00
kangfenmao
ab5e830ed1 docs: remove Chinese issue templates for bug reports, feature requests, and questions 2025-02-17 12:02:07 +08:00
ousugo
e0eca97053 fix: update Baidu API key URL in provider configuration, resolve #1794 2025-02-17 11:55:53 +08:00
George·Dong
d175212d9a fix: 修复切换助手时无法正确切换到助手默认模型的问题 (#1776) 2025-02-17 11:28:17 +08:00
Shelly
642ce160a1 fix: 修复同名模型选择问题 (#1772)
1. 同名模型显示的供应商名称问题
2. 同名模型不同供应商不能被同时选择

Co-authored-by: duanyongcheng <duanyongcheng77@gmail.com>
2025-02-17 09:47:01 +08:00
Wenwei Lin
574d02a8c9 feat: support json and draftsExport file in knowledge base (#1717) 2025-02-17 08:25:07 +08:00
ousugo
7764507d74 feat: Expand reasoning model regex to include 'thinking' keyword 2025-02-17 08:16:47 +08:00
ousugo
fa8bf61532 fix: sidebar navigation and active state handling
- 当固定在侧边栏的小程序被打开时,对应图标显示为被选中
- 修复点击两次主题切换按钮会导致当前 Webview 被错误关闭的问题
- 修复当 Webview 处于打开状态,点击侧边栏按钮无法立即跳转到对应界面的问题
- 修复打开帮助文档,其按钮没有显示为被选中的问题
- 修复在设置界面时打开帮助文档,设置按钮继续显示为被选中的问题
2025-02-17 08:15:03 +08:00
ousugo
30e8cef9cc fix: correction of the capitalization of Perplexity names 2025-02-17 08:13:56 +08:00
ousugo
1a2861e81a fix: Fix the miniapp sorting problem, resolve #1725
- 修复小程序拖动排序不生效的问题
- 修复小程序拖动排序时列表滚动排序不生效的问题
2025-02-17 08:13:56 +08:00
Neal_Tan
653e5d82ed docs: optimize issues (#1790) 2025-02-17 08:12:40 +08:00
kangfenmao
5be0e0ae72 style: Enhance scrollbar appearance in mention models dropdown 2025-02-16 13:56:10 +08:00
FischLu
b92b46f2b0 refine code 2025-02-16 13:54:32 +08:00
FischLu
23686d4926 feat: implement select mode menu autoscroll for long mode lists 2025-02-16 13:54:32 +08:00
kangfenmao
b340b40bcf Revert "fix: Improve the @ model list experience"
This reverts commit c53d63f7af.
2025-02-16 13:54:09 +08:00
bfdyanshe
253fc6f4e1 fix: Separate EPUB files into dedicated book file extension category 2025-02-16 13:46:52 +08:00
bC2y5tal
99aa0d3255 feat: Add EPUB file support to document loader 2025-02-16 13:46:52 +08:00
icinggslits
23a2a6b57c improvement(shortcut): Support more keyboard shortcuts 2025-02-16 13:45:03 +08:00
icinggslits
a869857fc1 add usableEndKeys 2025-02-16 13:45:03 +08:00
kangfenmao
4ecedcb267 feat: Enhance topic handling and message prompt generation 2025-02-16 13:41:31 +08:00
kangfenmao
cbd6a30e14 feat: Improve knowledge base threshold tooltip and input 2025-02-16 12:20:08 +08:00
kangfenmao
5f2cddee09 chore: Update store migration for Coze minapp 2025-02-16 12:14:20 +08:00
Chen Tao
c0e0e924f7 feat: 添加知识库匹配度阈值 (#1634)
* feat: 添加知识库匹配度阈值

* fix: 增加问答时知识库阈值

* feat: 当知识库未检索到数据时使用通用对话逻辑

* fix: add toast
2025-02-16 11:38:00 +08:00
Avan
b6ad7eeb9a style: add bot.n.cn logo 2025-02-16 11:36:58 +08:00
Avan
9cf74317a6 feat: add bot.n.cn 2025-02-16 11:36:58 +08:00
George·Dong
82fcc2292e feat: Add Coze minapp 2025-02-16 10:38:28 +08:00
yangtb2024
4eb0c25682 fix: 窗口较小时,工具显示适配问题 2025-02-16 10:35:45 +08:00
kassadin
9e128d2524 fix: unregister global shortcuts 2025-02-16 10:34:24 +08:00
jyeric
1473cb3123 Fix: Font size and Latex problem, resolve CherryHQ#1034 CherryHQ#1596 (#1723) 2025-02-15 22:55:43 +08:00
Wenwei Lin
2c5fe01fbf fix: add ellipsis in knowledge base item (#1718) 2025-02-15 22:51:07 +08:00
Wenwei Lin
d574a09529 fix: support html file in knowledge base (#1703) 2025-02-15 22:50:05 +08:00
美兰十三
f20bccfd7d feature: add topic prompt (#1696)
* feat: 新增话题补充提示词

* feat: 新增话题补充提示词

* feat: 新增话题补充提示词

* feat: 新增话题补充提示词

* feat: 新增话题补充提示词
2025-02-15 08:21:59 +08:00
icinggslits
5dcc892f31 调整show_app快捷键功能的交互逻辑 2025-02-15 08:17:18 +08:00
kangfenmao
26e3871688 Revert "fix: 网页链接附带中文标点解析错误"
This reverts commit 16feb49e9e.
2025-02-15 01:30:13 +08:00
kangfenmao
9a6aad35b0 fix: Improve handling of 'undefined' values in JSON parsing 2025-02-15 01:25:59 +08:00
eeee0717
16feb49e9e fix: 网页链接附带中文标点解析错误 2025-02-15 01:06:32 +08:00
kangfenmao
30959e2380 feat: Add LM Studio and ModelScope as system LLM providers
- Update llm.ts to include LM Studio and ModelScope in initial system providers
- Modify migrate.ts to add migration logic for adding these new providers
- Ensure providers are added only if they don't already exist in the configuration
2025-02-15 01:03:09 +08:00
kangfenmao
2c17f75f4f fix: Correct migration version configuration 2025-02-15 00:55:07 +08:00
Yihong Wang
2d1a930bfe feat: Add NotebookLM to MinApps solve #1679 2025-02-15 00:52:47 +08:00
eeee0717
320d27059f fix: 分组和非分组逻辑修改 2025-02-15 00:33:39 +08:00
eeee0717
31014aa8a6 fix: Switching model does not work 2025-02-15 00:33:39 +08:00
ousugo
b468ecfce7 feat: Improve textarea cursor positioning on focus 2025-02-15 00:31:36 +08:00
ousugo
c53d63f7af fix: Improve the @ model list experience
- 修复使用方向键上下移动时,列表不随之滚动的问题
- 添加滚动条
2025-02-15 00:29:32 +08:00
ousugo
dabff0a847 feat: Add platform and version fields to all issue templates 2025-02-15 00:26:31 +08:00
Konjac-XZ
26a5ae0086 fix: Translation error when passing empty user messages to certain models.(Refined) 2025-02-15 00:24:17 +08:00
kangfenmao
88e0d293a2 chore(version): 0.9.24 2025-02-14 15:04:59 +08:00
kangfenmao
0c97b52c53 refactor: Improve provider removal logic in LLM store 2025-02-14 14:49:34 +08:00
ousugo
2449a22c69 perf: Add new Infini AI models to system models list 2025-02-14 14:37:57 +08:00
ousugo
028f9d88d9 feat: Add reasoning model filter in EditModelsPopup 2025-02-14 14:30:48 +08:00
kangfenmao
a07c6cdffb refactor: Improve provider settings and menu handling 2025-02-14 13:35:58 +08:00
kangfenmao
5a647b0d61 style: Adjust group menu bar styling 2025-02-14 13:18:16 +08:00
kangfenmao
007e6419ba feat: Add ModelScope provider to LLM providers list 2025-02-14 13:13:32 +08:00
Col0ring
caa473639c feat: add modelscope provider (#1563)
Co-authored-by: 亢奋猫 <kangfenmao@qq.com>
2025-02-14 13:12:46 +08:00
kangfenmao
b6825a6ea2 feat(notion): Add divider to Notion settings page 2025-02-14 13:08:47 +08:00
Trey Dong
710180997f feat(notion): 添加 Notion连接检查功能 (#1620)
- 在 Notion 配置页面添加"检查"按钮
- 实现 Notion 连接检查逻辑
- 添加相关国际化文本
2025-02-14 10:52:16 +08:00
hehua2008
fd4334f331 feat: Add LM Studio support (#1572)
Co-authored-by: hehua2008 <hegan2010@gmail.com>
Co-authored-by: 亢奋猫 <kangfenmao@qq.com>
2025-02-14 10:49:57 +08:00
FischLu
80dedc149a feat: Implement circular selection in model selector 2025-02-14 10:40:03 +08:00
duanyongcheng77
8eacaa281a chore: add ignore for .cursoerules 2025-02-14 10:38:51 +08:00
duanyongcheng77
6e75140939 chore: 🤖 add aider gitignore 2025-02-14 10:38:51 +08:00
Asurada
5a3a97135f feat: Add XiaoYi miniapp, resolve #1591 (#1595)
Co-authored-by: 亢奋猫 <kangfenmao@qq.com>
2025-02-14 10:37:42 +08:00
ousugo
44d42d64ef fix: Solve the problem that eslint always reports line break errors on Windows 2025-02-14 10:36:07 +08:00
ousugo
fad3f67678 feat: Improve model search by adding name-based filtering, resolve #1520
搜索模型时,同时搜索模型的名字和 ID
2025-02-14 10:32:34 +08:00
kangfenmao
65b30b3b0d chore: Update Vite config and remove deprecated migration code
- Exclude additional chunk in Electron Vite configuration
- Remove outdated migration logic for providers and MinApps
2025-02-14 10:31:24 +08:00
首都爱护动物协会
0278228a84 add providers
新增服务商:
1.无问芯穹
2Perplexity
3.DMXAPI

补充部分embedding模型信息
2025-02-14 10:28:52 +08:00
shniubobo
bb0cb1cecc fix: Regression on reasoning time
PR #1253 fixed reasoning time calculation for APIs that return reasoning
content in `delta.content`, but introduced a regression for those
returning it in `delta.reasoning_content`. This commit fixes the
regression.

Fixes #1593
2025-02-14 10:26:54 +08:00
shniubobo
f5cd6ecb50 fix: Remove trailing newline in codeblocks 2025-02-14 10:10:30 +08:00
Xin Rui
76c0ad9985 fix: translation error when passing empty user messages to certain models.. (#1612) 2025-02-14 10:09:47 +08:00
223 changed files with 11911 additions and 3133 deletions

View File

@@ -16,6 +16,7 @@ module.exports = {
'react/prop-types': 'off', 'react/prop-types': 'off',
'simple-import-sort/imports': 'error', 'simple-import-sort/imports': 'error',
'simple-import-sort/exports': 'error', 'simple-import-sort/exports': 'error',
'react/no-is-mounted': 'off' 'react/no-is-mounted': 'off',
'prettier/prettier': ['error', { endOfLine: 'auto' }]
} }
} }

View File

@@ -1,4 +1,4 @@
name: 🐛 错误报告 name: 🐛 错误报告 (中文)
description: 创建一个报告以帮助我们改进 description: 创建一个报告以帮助我们改进
title: '[错误]: ' title: '[错误]: '
labels: ['bug'] labels: ['bug']
@@ -7,17 +7,20 @@ body:
attributes: attributes:
value: | value: |
感谢您花时间填写此错误报告! 感谢您花时间填写此错误报告!
在提交此问题之前,请确保您已经了解了[常见问题](https://docs.cherry-ai.com/question-contact/questions)和[知识科普](https://docs.cherry-ai.com/question-contact/knowledge)
- type: checkboxes - type: checkboxes
id: checklist id: checklist
attributes: attributes:
label: Issue 检查清单 label: 提交前检查
description: | description: |
在提交 Issue 前请确保您已经完成了以下所有步骤 在提交 Issue 前请确保您已经完成了以下所有步骤
options: options:
- label: 已经查看了置顶 Issue 并搜索了现有的 Issue但没有找到类似的问题 - label: 理解 Issue 是用于反馈和解决问题的,而非吐槽评论区,将尽可能提供更多信息帮助问题解决
required: true required: true
- label: 正确填写了 Issue 标题。 - label: 我已经查看了置顶 Issue 并搜索了现有的 [开放Issue](https://github.com/CherryHQ/cherry-studio/issues)和[已关闭Issue](https://github.com/CherryHQ/cherry-studio/issues?q=is%3Aissue%20state%3Aclosed%20),没有找到类似的问题。
required: true
- label: 我填写了简短且清晰明确的标题,以便开发者在翻阅 Issue 列表时能快速确定大致问题。而不是“一个建议”、“卡住了”等。
required: true required: true
- type: dropdown - type: dropdown
@@ -45,7 +48,7 @@ body:
id: description id: description
attributes: attributes:
label: 错误描述 label: 错误描述
description: 清晰简洁地描述错误是什么 description: 描述问题时请尽可能详细
placeholder: 告诉我们发生了什么... placeholder: 告诉我们发生了什么...
validations: validations:
required: true required: true
@@ -54,7 +57,7 @@ body:
id: reproduction id: reproduction
attributes: attributes:
label: 重现步骤 label: 重现步骤
description: 重现行为的步骤 description: 提供详细的重现步骤,以便于我们可以准确地重现问题
placeholder: | placeholder: |
1. 转到 '...' 1. 转到 '...'
2. 点击 '....' 2. 点击 '....'
@@ -82,4 +85,4 @@ body:
id: additional id: additional
attributes: attributes:
label: 附加信息 label: 附加信息
description: 在此添加有关问题的任何其他上下文 description: 任何能让我们对你所遇到的问题有更多了解的东西

View File

@@ -1,4 +1,4 @@
name: 💡 功能建议 name: 💡 功能建议 (中文)
description: 为项目提出新的想法 description: 为项目提出新的想法
title: '[功能]: ' title: '[功能]: '
labels: ['enhancement'] labels: ['enhancement']
@@ -7,23 +7,49 @@ body:
attributes: attributes:
value: | value: |
感谢您花时间提出新的功能建议! 感谢您花时间提出新的功能建议!
在提交此问题之前,请确保您已经了解了[项目规划](https://docs.cherry-ai.com/cherrystudio/planning)和[功能介绍](https://docs.cherry-ai.com/cherrystudio/preview)
- type: checkboxes - type: checkboxes
id: checklist id: checklist
attributes: attributes:
label: Issue 检查清单 label: 提交前检查
description: | description: |
在提交 Issue 前请确保您已经完成了以下所有步骤 在提交 Issue 前请确保您已经完成了以下所有步骤
options: options:
- label: 已经查看了置顶 Issue 并搜索了现有的 Issue但没有找到类似的问题 - label: 理解 Issue 是用于反馈和解决问题的,而非吐槽评论区,将尽可能提供更多信息帮助问题解决
required: true required: true
- label: 正确填写了 Issue 标题 - label: 我已经查看了置顶 Issue 并搜索了现有的 [开放Issue](https://github.com/CherryHQ/cherry-studio/issues)和[已关闭Issue](https://github.com/CherryHQ/cherry-studio/issues?q=is%3Aissue%20state%3Aclosed%20),没有找到类似的建议
required: true
- label: 我填写了简短且清晰明确的标题,以便开发者在翻阅 Issue 列表时能快速确定大致问题。而不是“一个建议”、“卡住了”等。
required: true
- label: 最新的 Cherry Studio 版本没有实现我所提出的功能。
required: true
- type: dropdown
id: platform
attributes:
label: 平台
description: 您正在使用哪个平台?
options:
- Windows
- macOS
- Linux
validations:
required: true
- type: input
id: version
attributes:
label: 版本
description: 您正在运行的 Cherry Studio 版本是什么?
placeholder: 例如 v1.0.0
validations:
required: true required: true
- type: textarea - type: textarea
id: problem id: problem
attributes: attributes:
label: 您的功能建议是否与某个问题相关? label: 您的功能建议是否与某个问题/issue相关?
description: 请简明扼要地描述您遇到的问题 description: 请简明扼要地描述您遇到的问题
placeholder: 我总是感到沮丧,因为... placeholder: 我总是感到沮丧,因为...
validations: validations:

View File

@@ -1,6 +1,6 @@
name: 提问 name: 讨论 & 提问 (中文)
description: 提出一个问题或寻求帮助 description: 寻求帮助、讨论问题、提出疑问等...
title: '[问题]: ' title: '[讨论]: '
labels: ['question'] labels: ['question']
body: body:
- type: markdown - type: markdown
@@ -15,9 +15,30 @@ body:
description: | description: |
在提交 Issue 前请确保您已经完成了以下所有步骤 在提交 Issue 前请确保您已经完成了以下所有步骤
options: options:
- label: 已经查看了置顶 Issue 并搜索了现有的 Issue但没有找到类似的问题 - label: 理解 Issue 是用于反馈和解决问题的,而非吐槽评论区,将尽可能提供更多信息帮助问题解决
required: true required: true
- label: 正确填写了 Issue 标题 - label: 我确认自己需要的是提出问题并且讨论问题,而不是 Bug 反馈或需求建议
required: true
- type: dropdown
id: platform
attributes:
label: 平台
description: 您正在使用哪个平台?
options:
- Windows
- macOS
- Linux
validations:
required: true
- type: input
id: version
attributes:
label: 版本
description: 您正在运行的 Cherry Studio 版本是什么?
placeholder: 例如 v1.0.0
validations:
required: true required: true
- type: textarea - type: textarea

View File

@@ -1,4 +1,4 @@
name: 🐛 Bug Report name: 🐛 Bug Report (English)
description: Create a report to help us improve description: Create a report to help us improve
title: '[Bug]: ' title: '[Bug]: '
labels: ['bug'] labels: ['bug']
@@ -6,7 +6,8 @@ body:
- type: markdown - type: markdown
attributes: attributes:
value: | value: |
Thanks for taking the time to fill out this bug report! Thank you for taking the time to fill out this bug report!
Before submitting this issue, please make sure that you have understood the [FAQ](https://docs.cherry-ai.com/question-contact/questions) and [Knowledge Science](https://docs.cherry-ai.com/question-contact/knowledge)
- type: checkboxes - type: checkboxes
id: checklist id: checklist
@@ -15,9 +16,11 @@ body:
description: | description: |
Before submitting an issue, please make sure you have completed the following steps Before submitting an issue, please make sure you have completed the following steps
options: options:
- label: I have viewed the pinned issues and searched existing issues but couldn't find anything similar. - label: I understand that issues are for feedback and problem solving, not for complaining in the comment section, and will provide as much information as possible to help solve the problem.
required: true required: true
- label: I have filled out the issue title correctly. - label: I've looked at pinned issues and searched for existing [Open Issues](https://github.com/CherryHQ/cherry-studio/issues), [Closed Issues](https://github.com/CherryHQ/cherry-studio/issues?q=is%3Aissue%20state%3Aclosed), and [Discussions](https://github.com/CherryHQ/cherry-studio/discussions), no similar issue or discussion was found.
required: true
- label: I've filled in short, clear headings so that developers can quickly identify a rough idea of what to expect when flipping through the list of issues. And not "a suggestion", "stuck", etc.
required: true required: true
- type: dropdown - type: dropdown
@@ -45,8 +48,8 @@ body:
id: description id: description
attributes: attributes:
label: Bug Description label: Bug Description
description: A clear and concise description of what the bug is description: Please be as detailed as possible when describing the problem. Please provide screenshots or screen recordings whenever possible to help us better understand the issue.
placeholder: Tell us what happened... placeholder: Tell us what happened... (Remember to attach screenshots/recordings if applicable)
validations: validations:
required: true required: true
@@ -54,12 +57,14 @@ body:
id: reproduction id: reproduction
attributes: attributes:
label: Steps To Reproduce label: Steps To Reproduce
description: Steps to reproduce the behavior description: Provide detailed steps to reproduce the issue so that our developers can reproduce the issue accurately. Please include screenshots or screen recordings for each step when possible.
placeholder: | placeholder: |
1. Go to '...' 1. Go to '...'
2. Click on '....' 2. Click on '....'
3. Scroll down to '....' 3. Scroll down to '....'
4. See error 4. See error
Remember to attach screenshots/recordings for each step when possible!
validations: validations:
required: true required: true
@@ -82,4 +87,4 @@ body:
id: additional id: additional
attributes: attributes:
label: Additional Context label: Additional Context
description: Add any other context about the problem here description: Anything that gives us a better understanding of the problem you're experiencing

View File

@@ -1,4 +1,4 @@
name: 💡 Feature Request name: 💡 Feature Request (English)
description: Suggest an idea for this project description: Suggest an idea for this project
title: '[Feature]: ' title: '[Feature]: '
labels: ['enhancement'] labels: ['enhancement']
@@ -6,7 +6,8 @@ body:
- type: markdown - type: markdown
attributes: attributes:
value: | value: |
Thanks for taking the time to suggest a new feature! Thank you for taking the time to submit a feature request!
Before submitting this issue, please make sure you have reviewed the [Project Roadmap](https://docs.cherry-ai.com/cherrystudio/planning) and the [Feature Overview](https://docs.cherry-ai.com/cherrystudio/preview).
- type: checkboxes - type: checkboxes
id: checklist id: checklist
@@ -15,36 +16,61 @@ body:
description: | description: |
Before submitting an issue, please make sure you have completed the following steps Before submitting an issue, please make sure you have completed the following steps
options: options:
- label: I have viewed the pinned issues and searched existing issues but couldn't find anything similar. - label: I understand that issues are for reporting problems and requesting features, not for off-topic comments, and I will provide as much detail as possible to help resolve the issue.
required: true required: true
- label: I have filled out the issue title correctly. - label: I have checked the pinned issues and searched through the existing [open issues](https://github.com/CherryHQ/cherry-studio/issues), [closed issues](https://github.com/CherryHQ/cherry-studio/issues?q=is%3Aissue%20state%3Aclosed), and [discussions](https://github.com/CherryHQ/cherry-studio/discussions) and did not find a similar suggestion.
required: true
- label: I have provided a short and descriptive title so that developers can quickly understand the issue when browsing the issue list, rather than vague titles like "A suggestion" or "Stuck."
required: true
- label: The latest version of Cherry Studio does not include the feature I am suggesting.
required: true
- type: dropdown
id: platform
attributes:
label: Platform
description: What platform are you using?
options:
- Windows
- macOS
- Linux
validations:
required: true
- type: input
id: version
attributes:
label: Version
description: What version of Cherry Studio are you running?
placeholder: e.g. v1.0.0
validations:
required: true required: true
- type: textarea - type: textarea
id: problem id: problem
attributes: attributes:
label: Is your feature request related to a problem? label: Is your feature request related to an existing issue?
description: A clear and concise description of what the problem is description: Please briefly describe the problem you are experiencing. If possible, include screenshots or recordings to help illustrate the current situation or pain points.
placeholder: I'm always frustrated when... placeholder: I often feel frustrated because... (Remember to attach screenshots/recordings if applicable)
validations: validations:
required: true required: true
- type: textarea - type: textarea
id: solution id: solution
attributes: attributes:
label: Describe the solution you'd like label: Desired Solution
description: A clear and concise description of what you want to happen description: Please briefly describe what you would like to happen. You can include mockups, screenshots, or screen recordings to better illustrate your proposed solution.
validations: validations:
required: true required: true
- type: textarea - type: textarea
id: alternatives id: alternatives
attributes: attributes:
label: Describe alternatives you've considered label: Alternative Solutions
description: A clear and concise description of any alternative solutions or features you've considered description: Please briefly describe any alternative solutions or features you have considered. Feel free to include screenshots or mockups of alternative approaches.
- type: textarea - type: textarea
id: additional id: additional
attributes: attributes:
label: Additional Context label: Additional Information
description: Add any other context or screenshots about the feature request here description: Add any other context, screenshots, mockups or recordings that can help us better understand your feature request.

View File

@@ -1,12 +1,12 @@
name: ❓ Question name: Discussion & Questions
description: Ask a question or seek help description: Seeking help, discussing issues, asking questions, etc...
title: '[Question]: ' title: '[Discussion]: '
labels: ['question'] labels: ['question']
body: body:
- type: markdown - type: markdown
attributes: attributes:
value: | value: |
Thanks for asking a question! Please provide as much detail as possible so we can better assist you. Thank you for your question! Please describe your issue in as much detail as possible so that we can better assist you.
- type: checkboxes - type: checkboxes
id: checklist id: checklist
@@ -15,17 +15,40 @@ body:
description: | description: |
Before submitting an issue, please make sure you have completed the following steps Before submitting an issue, please make sure you have completed the following steps
options: options:
- label: I have viewed the pinned issues and searched existing issues but couldn't find anything similar. - label: I understand that issues are meant for feedback and problem-solving, not for venting, and I will provide as much detail as possible to help resolve the issue.
required: true required: true
- label: I have filled out the issue title correctly. - label: I have checked the pinned issues and searched through the existing [open issues](https://github.com/CherryHQ/cherry-studio/issues), [closed issues](https://github.com/CherryHQ/cherry-studio/issues?q=is%3Aissue%20state%3Aclosed), and [discussions](https://github.com/CherryHQ/cherry-studio/discussions) and did not find a similar suggestion.
required: true
- label: I confirm that I am here to ask questions and discuss issues, not to report bugs or request features.
required: true
- type: dropdown
id: platform
attributes:
label: Platform
description: What platform are you using?
options:
- Windows
- macOS
- Linux
validations:
required: true
- type: input
id: version
attributes:
label: Version
description: What version of Cherry Studio are you running?
placeholder: e.g. v1.0.0
validations:
required: true required: true
- type: textarea - type: textarea
id: question id: question
attributes: attributes:
label: Your Question label: Your Question
description: Please describe your question in detail description: Please describe your issue in detail. Include screenshots or screen recordings whenever possible to help us better understand your question.
placeholder: Please explain your question as clearly as possible... placeholder: Please explain your issue as clearly as possible...(Remember to attach screenshots/recordings if applicable)
validations: validations:
required: true required: true
@@ -33,23 +56,23 @@ body:
id: context id: context
attributes: attributes:
label: Context label: Context
description: Please provide some background information to help us better understand your question description: Please provide some background information to help us better understand your question. Screenshots or recordings of your current setup or situation can be very helpful.
placeholder: "For example: use case, solutions you've tried, etc." placeholder: "For example: use case, solutions you've tried, etc. Don't forget to include relevant screenshots/recordings!"
- type: textarea - type: textarea
id: additional id: additional
attributes: attributes:
label: Additional Information label: Additional Information
description: Any other relevant information, screenshots, or code examples description: Any other relevant information, screenshots, recordings, or code examples that can help us better assist you
render: shell render: shell
- type: dropdown - type: dropdown
id: priority id: priority
attributes: attributes:
label: Priority label: Priority
description: How urgent is this question for you? description: How urgent is this issue for you?
options: options:
- Low (Can wait) - Low (Review when available)
- Medium (Would like a response soon) - Medium (Would like a response soon)
- High (Blocking progress) - High (Blocking progress)
validations: validations:

3
.gitignore vendored
View File

@@ -44,3 +44,6 @@ stats.html
# Local # Local
local local
.aider*
.cursorrules
.cursor/rules

View File

@@ -0,0 +1,57 @@
diff --git a/dist/index.js b/dist/index.js
index 88c405a000d21b3631eaa378690907c5527b8eaf..e03e66440c7c93aee38adf57df3096c6fefcd96d 100644
--- a/dist/index.js
+++ b/dist/index.js
@@ -82,7 +82,6 @@ module.exports = __toCommonJS(index_exports);
// src/utils.ts
var import_axios = __toESM(require("axios"));
-var import_js_tiktoken = require("js-tiktoken");
var BASE_URL = "https://api.tavily.com";
var DEFAULT_MODEL_ENCODING = "gpt-3.5-turbo";
var DEFAULT_MAX_TOKENS = 4e3;
@@ -97,8 +96,7 @@ function post(endpoint, body, apiKey) {
});
}
function getTotalTokensFromString(str, encodingName = DEFAULT_MODEL_ENCODING) {
- const encoding = (0, import_js_tiktoken.encodingForModel)(encodingName);
- return encoding.encode(str).length;
+ return 0;
}
function getMaxTokensFromList(data, maxTokens = DEFAULT_MAX_TOKENS) {
var result = [];
diff --git a/dist/index.mjs b/dist/index.mjs
index 0a9ea6a0add8d709e6721e806571f373d9fe0487..b81f1ea48a2b2a30ee98d53980a1b04ea3fdc5d4 100644
--- a/dist/index.mjs
+++ b/dist/index.mjs
@@ -49,7 +49,6 @@ var __async = (__this, __arguments, generator) => {
// src/utils.ts
import axios from "axios";
-import { encodingForModel } from "js-tiktoken";
var BASE_URL = "https://api.tavily.com";
var DEFAULT_MODEL_ENCODING = "gpt-3.5-turbo";
var DEFAULT_MAX_TOKENS = 4e3;
@@ -64,8 +63,7 @@ function post(endpoint, body, apiKey) {
});
}
function getTotalTokensFromString(str, encodingName = DEFAULT_MODEL_ENCODING) {
- const encoding = encodingForModel(encodingName);
- return encoding.encode(str).length;
+ return 0;
}
function getMaxTokensFromList(data, maxTokens = DEFAULT_MAX_TOKENS) {
var result = [];
diff --git a/package.json b/package.json
index 36d4a613166a7906c1dc5377a89dc0a65f746f73..dc6e0e9363046755cad123e627cc270a2e3580d1 100644
--- a/package.json
+++ b/package.json
@@ -36,7 +36,6 @@
"typescript": "^5.6.2"
},
"dependencies": {
- "axios": "^1.7.7",
- "js-tiktoken": "^1.0.14"
+ "axios": "^1.7.7"
}
}

View File

@@ -12,7 +12,7 @@
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)[Discord](https://discord.gg/wez8HtpxqQ) | [QQ Group(1022779719)](https://qm.qq.com/q/Qtw8As0cwe) 👏 Join [Telegram Group](https://t.me/CherryStudioAI)[Discord](https://discord.gg/wez8HtpxqQ) | [QQ Group(1025067911)](https://qm.qq.com/q/RIBAO2pPKS)
❤️ Like Cherry Studio? Give it a star 🌟 or [Sponsor](docs/sponsor.md) to support the development! ❤️ Like Cherry Studio? Give it a star 🌟 or [Sponsor](docs/sponsor.md) to support the development!
@@ -30,7 +30,7 @@ Cherry Studio is a desktop client that supports for multiple LLM providers, avai
- ☁️ Major LLM Cloud Services: OpenAI, Gemini, Anthropic, and more - ☁️ Major LLM Cloud Services: OpenAI, Gemini, Anthropic, and more
- 🔗 AI Web Service Integration: Claude, Peplexity, Poe, and others - 🔗 AI Web Service Integration: Claude, Peplexity, Poe, and others
- 💻 Local Model Support with Ollama - 💻 Local Model Support with Ollama, LM Studio
2. **AI Assistants & Conversations**: 2. **AI Assistants & Conversations**:
@@ -60,6 +60,21 @@ Cherry Studio is a desktop client that supports for multiple LLM providers, avai
- 📝 Complete Markdown Rendering - 📝 Complete Markdown Rendering
- 🤲 Easy Content Sharing - 🤲 Easy Content Sharing
# 📝 TODO
- [x] Quick popup (read clipboard, quick question, explain, translate, summarize)
- [x] Comparison of multi-model answers
- [x] Support login using SSO provided by service providers
- [x] All models support networking
- [x] Launch of the first official version
- [x] Bug fixes and improvements (In progress...)
- [ ] Plugin functionality (JavaScript)
- [ ] Browser extension (highlight text to translate, summarize, add to knowledge base)
- [ ] iOS & Android client
- [ ] AI notes
- [ ] Voice input and output (AI call)
- [ ] Data backup supports custom backup content
# 🖥️ Develop # 🖥️ Develop
## IDE Setup ## IDE Setup
@@ -117,7 +132,8 @@ For more detailed guidelines, please refer to our [Contributing Guide](./CONTRIB
Thank you for your support and contributions! Thank you for your support and contributions!
## Related Projects ## Related Projects
* [one-api](https://github.com/songquanpeng/one-api):LLM API management and distribution system, supporting mainstream models like OpenAI, Azure, and Anthropic. Features unified API interface, suitable for key management and secondary distribution.
- [one-api](https://github.com/songquanpeng/one-api):LLM API management and distribution system, supporting mainstream models like OpenAI, Azure, and Anthropic. Features unified API interface, suitable for key management and secondary distribution.
# 🚀 Contributors # 🚀 Contributors

View File

@@ -13,7 +13,7 @@
Cherry Studioは、複数のLLMプロバイダーをサポートするデスクトップクライアントで、Windows、Mac、Linuxで利用可能です。 Cherry Studioは、複数のLLMプロバイダーをサポートするデスクトップクライアントで、Windows、Mac、Linuxで利用可能です。
👏 [Telegram](https://t.me/CherryStudioAI)[Discord](https://discord.gg/wez8HtpxqQ) | [QQグループ(1022779719)](https://qm.qq.com/q/Qtw8As0cwe) 👏 [Telegram](https://t.me/CherryStudioAI)[Discord](https://discord.gg/wez8HtpxqQ) | [QQグループ(1025067911)](https://qm.qq.com/q/RIBAO2pPKS)
❤️ Cherry Studioをお気に入りにしましたか小さな星をつけてください 🌟 または [スポンサー](sponsor.md) をして開発をサポートしてください!❤️ ❤️ Cherry Studioをお気に入りにしましたか小さな星をつけてください 🌟 または [スポンサー](sponsor.md) をして開発をサポートしてください!❤️
@@ -31,7 +31,7 @@ Cherry Studioは、複数のLLMプロバイダーをサポートするデスク
- ☁️ 主要な LLM クラウドサービス対応OpenAI、Gemini、Anthropic など - ☁️ 主要な LLM クラウドサービス対応OpenAI、Gemini、Anthropic など
- 🔗 AI Web サービス統合Claude、Peplexity、Poe など - 🔗 AI Web サービス統合Claude、Peplexity、Poe など
- 💻 Ollama によるローカルモデル実行対応 - 💻 Ollama、LM Studio によるローカルモデル実行対応
2. **AI アシスタントと対話** 2. **AI アシスタントと対話**
@@ -61,6 +61,21 @@ Cherry Studioは、複数のLLMプロバイダーをサポートするデスク
- 📝 完全な Markdown レンダリング - 📝 完全な Markdown レンダリング
- 🤲 簡単な共有機能 - 🤲 簡単な共有機能
# 📝 TODO
- [x] クイックポップアップ(クリップボードの読み取り、簡単な質問、説明、翻訳、要約)
- [x] 複数モデルの回答の比較
- [x] サービスプロバイダーが提供するSSOを使用したログインをサポート
- [x] すべてのモデルがネットワークをサポート
- [x] 最初の公式バージョンのリリース
- [ ] 錯誤修復と改善 (開発中...)
- [ ] プラグイン機能JavaScript
- [ ] ブラウザ拡張機能(テキストをハイライトして翻訳、要約、ナレッジベースに追加)
- [ ] iOS & Android クライアント
- [ ] AIート
- [ ] 音声入出力AIコール
- [ ] データバックアップはカスタムバックアップコンテンツをサポート
# 🖥️ 開発 # 🖥️ 開発
## IDEの設定 ## IDEの設定
@@ -118,7 +133,8 @@ Cherry Studioへの貢献を歓迎します以下の方法で貢献できま
ご支援と貢献に感謝します! ご支援と貢献に感謝します!
## 関連頁版 ## 関連頁版
* [one-api](https://github.com/songquanpeng/one-api):LLM APIの管理・配信システム。OpenAI、Azure、Anthropicなどの主要モデルに対応し、統一APIインターフェースを提供。APIキー管理と再配布に利用可能。
- [one-api](https://github.com/songquanpeng/one-api):LLM APIの管理・配信システム。OpenAI、Azure、Anthropicなどの主要モデルに対応し、統一APIインターフェースを提供。APIキー管理と再配布に利用可能。
# 🚀 コントリビューター # 🚀 コントリビューター

View File

@@ -13,7 +13,7 @@
Cherry Studio 是一款支持多个大语言模型LLM服务商的桌面客户端兼容 Windows、Mac 和 Linux 系统。 Cherry Studio 是一款支持多个大语言模型LLM服务商的桌面客户端兼容 Windows、Mac 和 Linux 系统。
👏 欢迎加入 [Telegram 群组](https://t.me/CherryStudioAI)[Discord](https://discord.gg/wez8HtpxqQ) | [QQ群(1022779719)](https://qm.qq.com/q/Qtw8As0cwe) 👏 欢迎加入 [Telegram 群组](https://t.me/CherryStudioAI)[Discord](https://discord.gg/wez8HtpxqQ) | [QQ群(1025067911)](https://qm.qq.com/q/RIBAO2pPKS)
❤️ 喜欢 Cherry Studio? 点亮小星星 🌟 或 [赞助开发者](sponsor.md)! ❤️ ❤️ 喜欢 Cherry Studio? 点亮小星星 🌟 或 [赞助开发者](sponsor.md)! ❤️
@@ -31,7 +31,7 @@ Cherry Studio 是一款支持多个大语言模型LLM服务商的桌面客
- ☁️ 支持主流 LLM 云服务OpenAI、Gemini、Anthropic、硅基流动等 - ☁️ 支持主流 LLM 云服务OpenAI、Gemini、Anthropic、硅基流动等
- 🔗 集成流行 AI Web 服务Claude、Peplexity、Poe、腾讯元宝、知乎直答等 - 🔗 集成流行 AI Web 服务Claude、Peplexity、Poe、腾讯元宝、知乎直答等
- 💻 支持 Ollama 本地模型部署 - 💻 支持 Ollama、LM Studio 本地模型部署
2. **智能助手与对话** 2. **智能助手与对话**
@@ -61,6 +61,21 @@ Cherry Studio 是一款支持多个大语言模型LLM服务商的桌面客
- 📝 完整的 Markdown 渲染 - 📝 完整的 Markdown 渲染
- 🤲 便捷的内容分享功能 - 🤲 便捷的内容分享功能
# 📝 待辦事項
- [x] 快捷弹窗 (读取剪贴板、快速提问、解释、翻译、总结)
- [x] 多模型回答对比
- [x] 支持使用服务供应商提供的 SSO 进行登入
- [x] 全部模型支持连网(开发中...
- [x] 推出第一个正式版
- [x] 错误修复和改进(开发中...
- [ ] 插件功能JavaScript
- [ ] 浏览器插件(划词翻译、总结、新增至知识库)
- [ ] iOS & Android 客户端
- [ ] AI 笔记
- [ ] 语音输入输出AI 通话)
- [ ] 数据备份支持自定义备份内容
# 🖥️ 开发 # 🖥️ 开发
## IDE 设置 ## IDE 设置
@@ -118,7 +133,8 @@ $ yarn build:linux
感谢您的支持和贡献! 感谢您的支持和贡献!
## 相关项目 ## 相关项目
* [one-api](https://github.com/songquanpeng/one-api):LLM API管理及分发系统支持OpenAI、Azure、Anthropic等主流模型统一API接口可用于密钥管理与二次分发。
- [one-api](https://github.com/songquanpeng/one-api):LLM API管理及分发系统支持OpenAI、Azure、Anthropic等主流模型统一API接口可用于密钥管理与二次分发。
# 🚀 贡献者 # 🚀 贡献者

View File

@@ -0,0 +1,174 @@
我来详细介绍 KnowledgeService 中控制并发处理的项目数量和工作负载的部分。
## 并发控制与工作负载管理机制
KnowledgeService 实现了一个精细的任务队列系统,用于控制并发处理的项目数量和工作负载。这个系统主要通过以下几个关键部分实现:
### 1. 关键变量和限制
```typescript
private workload = 0
private processingItemCount = 0
private knowledgeItemProcessingQueueMappingPromise: Map<LoaderTaskOfSet, () => void> = new Map()
private static MAXIMUM_WORKLOAD = 1024 * 1024 * 80 // 约80MB
private static MAXIMUM_PROCESSING_ITEM_COUNT = 30
```
- `workload`: 跟踪当前正在处理的总工作量(以字节为单位)
- `processingItemCount`: 跟踪当前正在处理的项目数量
- `MAXIMUM_WORKLOAD`: 设置最大工作负载为80MB
- `MAXIMUM_PROCESSING_ITEM_COUNT`: 设置最大并发处理项目数为30个
### 2. 工作负载评估
每个任务都有一个评估工作负载的机制,通过 `evaluateTaskWorkload` 属性来表示:
```typescript
interface EvaluateTaskWorkload {
workload: number
}
```
不同类型的任务有不同的工作负载评估方式:
- 文件任务:使用文件大小作为工作负载 `{ workload: file.size }`
- URL任务使用固定值 `{ workload: 1024 * 1024 * 2 }` (约2MB)
- 网站地图任务:使用固定值 `{ workload: 1024 * 1024 * 20 }` (约20MB)
- 笔记任务:使用文本内容的字节长度 `{ workload: contentBytes.length }`
### 3. 任务状态管理
任务通过状态枚举来跟踪其生命周期:
```typescript
enum LoaderTaskItemState {
PENDING, // 等待处理
PROCESSING, // 正在处理
DONE // 已完成
}
```
### 4. 任务队列处理核心逻辑
核心的队列处理逻辑在 `processingQueueHandle` 方法中:
```typescript
private processingQueueHandle() {
const getSubtasksUntilMaximumLoad = (): QueueTaskItem[] => {
const queueTaskList: QueueTaskItem[] = []
that: for (const [task, resolve] of this.knowledgeItemProcessingQueueMappingPromise) {
for (const item of task.loaderTasks) {
if (this.maximumLoad()) {
break that
}
const { state, task: taskPromise, evaluateTaskWorkload } = item
if (state !== LoaderTaskItemState.PENDING) {
continue
}
const { workload } = evaluateTaskWorkload
this.workload += workload
this.processingItemCount += 1
item.state = LoaderTaskItemState.PROCESSING
queueTaskList.push({
taskPromise: () =>
taskPromise().then(() => {
this.workload -= workload
this.processingItemCount -= 1
task.loaderTasks.delete(item)
if (task.loaderTasks.size === 0) {
this.knowledgeItemProcessingQueueMappingPromise.delete(task)
resolve()
}
this.processingQueueHandle()
}),
resolve: () => {},
evaluateTaskWorkload
})
}
}
return queueTaskList
}
const subTasks = getSubtasksUntilMaximumLoad()
if (subTasks.length > 0) {
const subTaskPromises = subTasks.map(({ taskPromise }) => taskPromise())
Promise.all(subTaskPromises).then(() => {
subTasks.forEach(({ resolve }) => resolve())
})
}
}
```
这个方法的工作流程是:
1. 遍历所有待处理的任务集合
2. 对于每个任务集合中的每个子任务:
- 检查是否已达到最大负载(通过 `maximumLoad()` 方法)
- 如果任务状态为 PENDING
- 增加当前工作负载和处理项目计数
- 将任务状态更新为 PROCESSING
- 将任务添加到待执行队列
3. 执行所有收集到的子任务
4. 当子任务完成时:
- 减少工作负载和处理项目计数
- 从任务集合中移除已完成的任务
- 如果任务集合为空,则解析相应的 Promise
- 递归调用 `processingQueueHandle()` 以处理更多任务
### 5. 负载检查
```typescript
private maximumLoad() {
return (
this.processingItemCount >= KnowledgeService.MAXIMUM_PROCESSING_ITEM_COUNT ||
this.workload >= KnowledgeService.MAXIMUM_WORKLOAD
)
}
```
这个方法检查当前是否已达到最大负载,通过两个条件:
- 处理项目数量达到上限30个
- 总工作负载达到上限80MB
### 6. 任务添加与执行流程
当添加新任务时,流程如下:
1. 创建任务(根据类型不同创建不同的任务)
2. 通过 `appendProcessingQueue` 将任务添加到队列
3. 调用 `processingQueueHandle` 开始处理队列中的任务
```typescript
private appendProcessingQueue(task: LoaderTask): Promise<LoaderReturn> {
return new Promise((resolve) => {
this.knowledgeItemProcessingQueueMappingPromise.set(loaderTaskIntoOfSet(task), () => {
resolve(task.loaderDoneReturn!)
})
})
}
```
## 并发控制的优势
这种并发控制机制有几个重要优势:
1. **资源使用优化**:通过限制同时处理的项目数量和总工作负载,避免系统资源过度使用
2. **自动调节**:当任务完成时,会自动从队列中获取新任务,保持资源的高效利用
3. **灵活性**:不同类型的任务有不同的工作负载评估,更准确地反映实际资源需求
4. **可靠性**通过状态管理和Promise解析机制确保任务正确完成并通知调用者
## 实际应用场景
这种并发控制在处理大量数据时特别有用,例如:
- 导入大型目录时,可能包含数百个文件
- 处理大型网站地图可能包含大量URL
- 处理多个用户同时添加知识库项目的请求
通过这种机制,系统可以平滑地处理大量请求,避免资源耗尽,同时保持良好的响应性。
总结来说KnowledgeService 实现了一个复杂而高效的任务队列系统,通过精确控制并发处理的项目数量和工作负载,确保系统在处理大量数据时保持稳定和高效。

View File

@@ -24,9 +24,9 @@ files:
- '!**/{LICENSE,LICENSE.txt,LICENSE-MIT.txt,*.LICENSE.txt,NOTICE.txt,README.md,CHANGELOG.md}' - '!**/{LICENSE,LICENSE.txt,LICENSE-MIT.txt,*.LICENSE.txt,NOTICE.txt,README.md,CHANGELOG.md}'
- '!node_modules/rollup-plugin-visualizer' - '!node_modules/rollup-plugin-visualizer'
- '!node_modules/js-tiktoken' - '!node_modules/js-tiktoken'
- '!node_modules/@tavily/core/node_modules/js-tiktoken'
- '!node_modules/pdf-parse/lib/pdf.js/{v1.9.426,v1.10.88,v2.0.550}' - '!node_modules/pdf-parse/lib/pdf.js/{v1.9.426,v1.10.88,v2.0.550}'
- '!node_modules/mammoth/{mammoth.browser.js,mammoth.browser.min.js}' - '!node_modules/mammoth/{mammoth.browser.js,mammoth.browser.min.js}'
- '!node_modules/html2canvas/dist/{html2canvas.min.js,html2canvas.esm.js}'
asarUnpack: asarUnpack:
- resources/** - resources/**
- '**/*.{node,dll,metal,exp,lib}' - '**/*.{node,dll,metal,exp,lib}'
@@ -80,11 +80,9 @@ afterPack: scripts/after-pack.js
afterSign: scripts/notarize.js afterSign: scripts/notarize.js
releaseInfo: releaseInfo:
releaseNotes: | releaseNotes: |
知识库增加更多文件类型支持 数据备份和恢复支持进度显示
使用@呼出模型选择列表 支持快捷引用模型回复内容
添加话题固定功能 输入框可以手动调整大小
增加导出话题至Notion的功能 知识库文件支持一键删除
增加 Google AI Studio 小程序 服务商列表支持查询(拖拽可排序)
增加 Gitee 服务商 增加代码块换行功能
增加 PPIO 服务商
为 OpenAI 请求添加引用来源数据显示

View File

@@ -43,7 +43,24 @@ export default defineConfig({
plugins: [externalizeDepsPlugin()] plugins: [externalizeDepsPlugin()]
}, },
renderer: { renderer: {
plugins: [react(), ...visualizerPlugin('renderer')], plugins: [
react({
babel: {
plugins: [
[
'styled-components',
{
displayName: true, // 开发环境下启用组件名称
fileName: false, // 不在类名中包含文件名
pure: true, // 优化性能
ssr: false // 不需要服务端渲染
}
]
]
}
}),
...visualizerPlugin('renderer')
],
resolve: { resolve: {
alias: { alias: {
'@renderer': resolve('src/renderer/src'), '@renderer': resolve('src/renderer/src'),
@@ -51,7 +68,7 @@ export default defineConfig({
} }
}, },
optimizeDeps: { optimizeDeps: {
exclude: ['chunk-PZ64DZKH.js', 'chunk-JMKENWIY.js'] exclude: ['chunk-PZ64DZKH.js', 'chunk-JMKENWIY.js', 'chunk-UXYB6GHG.js']
} }
} }
}) })

View File

@@ -1,6 +1,6 @@
{ {
"name": "CherryStudio", "name": "CherryStudio",
"version": "0.9.23", "version": "1.0.5",
"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",
@@ -51,6 +51,7 @@
"@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",
"@emotion/is-prop-valid": "^1.3.1",
"@google/generative-ai": "^0.21.0", "@google/generative-ai": "^0.21.0",
"@llm-tools/embedjs": "patch:@llm-tools/embedjs@npm%3A0.1.28#~/.yarn/patches/@llm-tools-embedjs-npm-0.1.28-8e4393fa2d.patch", "@llm-tools/embedjs": "patch:@llm-tools/embedjs@npm%3A0.1.28#~/.yarn/patches/@llm-tools-embedjs-npm-0.1.28-8e4393fa2d.patch",
"@llm-tools/embedjs-libsql": "^0.1.28", "@llm-tools/embedjs-libsql": "^0.1.28",
@@ -71,15 +72,15 @@
"electron-store": "^8.2.0", "electron-store": "^8.2.0",
"electron-updater": "^6.3.9", "electron-updater": "^6.3.9",
"electron-window-state": "^5.0.3", "electron-window-state": "^5.0.3",
"epub": "^1.3.0",
"fs-extra": "^11.2.0", "fs-extra": "^11.2.0",
"html2canvas": "^1.4.1",
"markdown-it": "^14.1.0", "markdown-it": "^14.1.0",
"officeparser": "^4.1.1", "officeparser": "^4.1.1",
"tokenx": "^0.4.1", "tokenx": "^0.4.1",
"webdav": "4.11.4" "webdav": "4.11.4"
}, },
"devDependencies": { "devDependencies": {
"@anthropic-ai/sdk": "^0.24.3", "@anthropic-ai/sdk": "^0.38.0",
"@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",
@@ -87,12 +88,14 @@
"@kangfenmao/keyv-storage": "^0.1.0", "@kangfenmao/keyv-storage": "^0.1.0",
"@llm-tools/embedjs-loader-image": "^0.1.28", "@llm-tools/embedjs-loader-image": "^0.1.28",
"@reduxjs/toolkit": "^2.2.5", "@reduxjs/toolkit": "^2.2.5",
"@tavily/core": "patch:@tavily/core@npm%3A0.3.1#~/.yarn/patches/@tavily-core-npm-0.3.1-fe69bf2bea.patch",
"@types/adm-zip": "^0", "@types/adm-zip": "^0",
"@types/fs-extra": "^11", "@types/fs-extra": "^11",
"@types/lodash": "^4.17.5", "@types/lodash": "^4.17.5",
"@types/markdown-it": "^14", "@types/markdown-it": "^14",
"@types/md5": "^2.3.5", "@types/md5": "^2.3.5",
"@types/node": "^18.19.9", "@types/node": "^18.19.9",
"@types/pako": "^1.0.2",
"@types/react": "^18.2.48", "@types/react": "^18.2.48",
"@types/react-dom": "^18.2.18", "@types/react-dom": "^18.2.18",
"@types/react-infinite-scroll-component": "^5.0.0", "@types/react-infinite-scroll-component": "^5.0.0",
@@ -101,6 +104,7 @@
"antd": "^5.22.5", "antd": "^5.22.5",
"applescript": "^1.0.0", "applescript": "^1.0.0",
"axios": "^1.7.3", "axios": "^1.7.3",
"babel-plugin-styled-components": "^2.1.4",
"browser-image-compression": "^2.0.2", "browser-image-compression": "^2.0.2",
"dayjs": "^1.11.11", "dayjs": "^1.11.11",
"dexie": "^4.0.8", "dexie": "^4.0.8",
@@ -118,6 +122,7 @@
"eslint-plugin-react-hooks": "^4.6.2", "eslint-plugin-react-hooks": "^4.6.2",
"eslint-plugin-simple-import-sort": "^12.1.1", "eslint-plugin-simple-import-sort": "^12.1.1",
"eslint-plugin-unused-imports": "^4.0.0", "eslint-plugin-unused-imports": "^4.0.0",
"html-to-image": "^1.11.13",
"i18next": "^23.11.5", "i18next": "^23.11.5",
"lodash": "^4.17.21", "lodash": "^4.17.21",
"mime": "^4.0.4", "mime": "^4.0.4",
@@ -136,7 +141,7 @@
"redux": "^5.0.1", "redux": "^5.0.1",
"redux-persist": "^6.0.0", "redux-persist": "^6.0.0",
"rehype-katex": "^7.0.1", "rehype-katex": "^7.0.1",
"rehype-mathjax": "^6.0.0", "rehype-mathjax": "^7.0.0",
"rehype-raw": "^7.0.0", "rehype-raw": "^7.0.0",
"remark-gfm": "^4.0.0", "remark-gfm": "^4.0.0",
"remark-math": "^6.0.0", "remark-math": "^6.0.0",
@@ -160,5 +165,5 @@
"@langchain/openai@npm:>=0.1.0 <0.4.0": "patch:@langchain/openai@npm%3A0.3.16#~/.yarn/patches/@langchain-openai-npm-0.3.16-e525b59526.patch", "@langchain/openai@npm:>=0.1.0 <0.4.0": "patch:@langchain/openai@npm%3A0.3.16#~/.yarn/patches/@langchain-openai-npm-0.3.16-e525b59526.patch",
"openai@npm:^4.77.0": "patch:openai@npm%3A4.77.3#~/.yarn/patches/openai-npm-4.77.3-59c6d42e7a.patch" "openai@npm:^4.77.0": "patch:openai@npm%3A4.77.3#~/.yarn/patches/openai-npm-4.77.3-59c6d42e7a.patch"
}, },
"packageManager": "yarn@4.5.0" "packageManager": "yarn@4.6.0"
} }

View File

@@ -2,6 +2,8 @@ export const imageExts = ['.jpg', '.jpeg', '.png', '.gif', '.bmp', '.webp']
export const videoExts = ['.mp4', '.avi', '.mov', '.wmv', '.flv', '.mkv'] export const videoExts = ['.mp4', '.avi', '.mov', '.wmv', '.flv', '.mkv']
export const audioExts = ['.mp3', '.wav', '.ogg', '.flac', '.aac'] export const audioExts = ['.mp3', '.wav', '.ogg', '.flac', '.aac']
export const documentExts = ['.pdf', '.docx', '.pptx', '.xlsx', '.odt', '.odp', '.ods'] export const documentExts = ['.pdf', '.docx', '.pptx', '.xlsx', '.odt', '.odp', '.ods']
export const thirdPartyApplicationExts = ['.draftsExport']
export const bookExts = ['.epub']
export const textExts = [ export const textExts = [
'.txt', // 普通文本文件 '.txt', // 普通文本文件
'.md', // Markdown 文件 '.md', // Markdown 文件
@@ -17,6 +19,8 @@ export const textExts = [
'.ini', // 配置文件 '.ini', // 配置文件
'.log', // 日志文件 '.log', // 日志文件
'.rtf', // 富文本格式文件 '.rtf', // 富文本格式文件
'.org', // org-mode 文件
'.wiki', // VimWiki 文件
'.tex', // LaTeX 文件 '.tex', // LaTeX 文件
'.srt', // 字幕文件 '.srt', // 字幕文件
'.xhtml', // XHTML 文件 '.xhtml', // XHTML 文件
@@ -33,6 +37,7 @@ export const textExts = [
'.bat', // Windows 批处理文件 '.bat', // Windows 批处理文件
'.sh', // Unix/Linux Shell 脚本文件 '.sh', // Unix/Linux Shell 脚本文件
'.py', // Python 脚本文件 '.py', // Python 脚本文件
'.ipynb', // Jupyter 笔记本格式
'.rb', // Ruby 脚本文件 '.rb', // Ruby 脚本文件
'.pl', // Perl 脚本文件 '.pl', // Perl 脚本文件
'.sql', // SQL 脚本文件 '.sql', // SQL 脚本文件
@@ -88,7 +93,16 @@ export const textExts = [
'.groovy', // Gradle 构建文件 '.groovy', // Gradle 构建文件
'.kts', // Kotlin Script 文件 '.kts', // Kotlin Script 文件
'.java', // Java 代码文件 '.java', // Java 代码文件
'.cs' // C# 代码文件 '.cs', // C# 代码文件
'.cpp', // C++ 代码文件
'.c', // C++ 代码文件
'.h', // C++ 头文件
'.hpp', // C++ 头文件
'.cc', // C++ 源文件
'.cxx', // C++ 源文件
'.cppm', // C++20 模块接口文件
'.ipp', // 模板实现文件
'.ixx' // C++20 模块实现文件
] ]
export const ZOOM_SHORTCUTS = [ export const ZOOM_SHORTCUTS = [

File diff suppressed because one or more lines are too long

View File

@@ -54,7 +54,7 @@ function syncRecursively(target: any, template: any): boolean {
function syncTranslations() { function syncTranslations() {
if (!fs.existsSync(baseFilePath)) { if (!fs.existsSync(baseFilePath)) {
console.error(`主模板文件 ${baseFileName} 不存在,请检查路径或文件名`) console.error(`主模板文件 ${baseFileName} 不存在,请检查路径或文件名`)
return return
} }
@@ -84,13 +84,13 @@ function syncTranslations() {
if (isUpdated) { if (isUpdated) {
try { try {
fs.writeFileSync(filePath, JSON.stringify(targetJson, null, 2), 'utf-8') fs.writeFileSync(filePath, JSON.stringify(targetJson, null, 2) + '\n', 'utf-8')
console.log(`文件 ${file} 已更新同步主模板的内容`) console.log(`文件 ${file} 已更新同步主模板的内容`)
} catch (error) { } catch (error) {
console.error(`写入 ${file} 出错:`, error) console.error(`写入 ${file} 出错:`, error)
} }
} else { } else {
console.log(`文件 ${file} 无需更新`) console.log(`文件 ${file} 无需更新`)
} }
} }
} }

View File

@@ -1,5 +1,5 @@
import { electronApp, optimizer } from '@electron-toolkit/utils' import { electronApp, optimizer } from '@electron-toolkit/utils'
import { app, BrowserWindow } from 'electron' import { app } from 'electron'
import installExtension, { REDUX_DEVTOOLS } from 'electron-devtools-installer' import installExtension, { REDUX_DEVTOOLS } from 'electron-devtools-installer'
import { registerIpc } from './ipc' import { registerIpc } from './ipc'
@@ -20,25 +20,6 @@ if (!app.requestSingleInstanceLock()) {
app.whenReady().then(async () => { app.whenReady().then(async () => {
await updateUserDataPath() await updateUserDataPath()
// Register custom protocol
if (!app.isDefaultProtocolClient('cherrystudio')) {
app.setAsDefaultProtocolClient('cherrystudio')
}
// Handle protocol open
app.on('open-url', (event, url) => {
event.preventDefault()
const parsedUrl = new URL(url)
if (parsedUrl.pathname === 'siliconflow.oauth.login') {
const code = parsedUrl.searchParams.get('code')
if (code) {
// Handle the OAuth code here
console.log('OAuth code received:', code)
// You can send this code to your renderer process via IPC if needed
}
}
})
// Set app user model id for windows // Set app user model id for windows
electronApp.setAppUserModelId(import.meta.env.VITE_MAIN_BUNDLE_ID || 'com.kangfenmao.CherryStudio') electronApp.setAppUserModelId(import.meta.env.VITE_MAIN_BUNDLE_ID || 'com.kangfenmao.CherryStudio')
@@ -46,9 +27,8 @@ if (!app.requestSingleInstanceLock()) {
new TrayService() new TrayService()
app.on('activate', function () { app.on('activate', function () {
// On macOS it's common to re-create a window in the app when the const mainWindow = windowService.getMainWindow()
// dock icon is clicked and there are no other windows open. if (!mainWindow || mainWindow.isDestroyed()) {
if (BrowserWindow.getAllWindows().length === 0) {
windowService.createMainWindow() windowService.createMainWindow()
} else { } else {
windowService.showMainWindow() windowService.showMainWindow()
@@ -68,12 +48,7 @@ if (!app.requestSingleInstanceLock()) {
// Listen for second instance // Listen for second instance
app.on('second-instance', () => { app.on('second-instance', () => {
const mainWindow = BrowserWindow.getAllWindows()[0] windowService.showMainWindow()
if (mainWindow) {
mainWindow.isMinimized() && mainWindow.restore()
mainWindow.show()
mainWindow.focus()
}
}) })
app.on('browser-window-created', (_, window) => { app.on('browser-window-created', (_, window) => {

View File

@@ -27,7 +27,7 @@ const backupManager = new BackupManager()
const exportService = new ExportService(fileManager) const exportService = new ExportService(fileManager)
export function registerIpc(mainWindow: BrowserWindow, app: Electron.App) { export function registerIpc(mainWindow: BrowserWindow, app: Electron.App) {
const { autoUpdater } = new AppUpdater(mainWindow) const appUpdater = new AppUpdater(mainWindow)
ipcMain.handle('app:info', () => ({ ipcMain.handle('app:info', () => ({
version: app.getVersion(), version: app.getVersion(),
@@ -48,6 +48,9 @@ export function registerIpc(mainWindow: BrowserWindow, app: Electron.App) {
ipcMain.handle('app:reload', () => mainWindow.reload()) ipcMain.handle('app:reload', () => mainWindow.reload())
ipcMain.handle('open:website', (_, url: string) => shell.openExternal(url)) ipcMain.handle('open:website', (_, url: string) => shell.openExternal(url))
// Update
ipcMain.handle('app:show-update-dialog', () => appUpdater.showUpdateDialog(mainWindow))
// language // language
ipcMain.handle('app:set-language', (_, language) => { ipcMain.handle('app:set-language', (_, language) => {
configManager.setLanguage(language) configManager.setLanguage(language)
@@ -99,9 +102,9 @@ export function registerIpc(mainWindow: BrowserWindow, app: Electron.App) {
// check for update // check for update
ipcMain.handle('app:check-for-update', async () => { ipcMain.handle('app:check-for-update', async () => {
const update = await autoUpdater.checkForUpdates() const update = await appUpdater.autoUpdater.checkForUpdates()
return { return {
currentVersion: autoUpdater.currentVersion, currentVersion: appUpdater.autoUpdater.currentVersion,
updateInfo: update?.updateInfo updateInfo: update?.updateInfo
} }
}) })

View File

@@ -0,0 +1,22 @@
import * as fs from 'node:fs'
import { JsonLoader } from '@llm-tools/embedjs'
/**
* Drafts 应用导出的笔记文件加载器
* 原始文件是一个 JSON 数组。每条笔记只保留 content、tags、modified_at 三个字段
*/
export class DraftsExportLoader extends JsonLoader {
constructor(filePath: string) {
const fileContent = fs.readFileSync(filePath, 'utf-8')
const rawJson = JSON.parse(fileContent) as any[]
const json = rawJson.map((item) => {
return {
content: item.content?.replace(/\n/g, '<br>'),
tags: item.tags,
modified_at: item.created_at
}
})
super({ object: json })
}
}

View File

@@ -0,0 +1,228 @@
import { RecursiveCharacterTextSplitter } from '@langchain/textsplitters'
import { BaseLoader } from '@llm-tools/embedjs-interfaces'
import { cleanString } from '@llm-tools/embedjs-utils'
import Logger from 'electron-log'
import EPub from 'epub'
import * as fs from 'fs'
/**
* epub 加载器的配置选项
*/
interface EpubLoaderOptions {
/** epub 文件路径 */
filePath: string
/** 文本分块大小 */
chunkSize: number
/** 分块重叠大小 */
chunkOverlap: number
}
/**
* epub 文件的元数据信息
*/
interface EpubMetadata {
/** 作者显示名称(例如:"Lewis Carroll" */
creator?: string
/** 作者规范化名称,用于排序和索引(例如:"Carroll, Lewis" */
creatorFileAs?: string
/** 书籍标题(例如:"Alice's Adventures in Wonderland" */
title?: string
/** 语言代码(例如:"en" 或 "zh-CN" */
language?: string
/** 主题或分类(例如:"Fantasy"、"Fiction" */
subject?: string
/** 创建日期(例如:"2024-02-14" */
date?: string
/** 书籍描述或简介 */
description?: string
}
/**
* epub 章节信息
*/
interface EpubChapter {
/** 章节 ID */
id: string
/** 章节标题 */
title?: string
/** 章节顺序 */
order?: number
}
/**
* epub 文件加载器
* 用于解析 epub 电子书文件,提取文本内容和元数据
*/
export class EpubLoader extends BaseLoader<Record<string, string | number | boolean>, Record<string, unknown>> {
protected filePath: string
protected chunkSize: number
protected chunkOverlap: number
private extractedText: string
private metadata: EpubMetadata | null
/**
* 创建 epub 加载器实例
* @param options 加载器配置选项
*/
constructor(options: EpubLoaderOptions) {
super(options.filePath, {
chunkSize: options.chunkSize,
chunkOverlap: options.chunkOverlap
})
this.filePath = options.filePath
this.chunkSize = options.chunkSize
this.chunkOverlap = options.chunkOverlap
this.extractedText = ''
this.metadata = null
}
/**
* 等待 epub 文件初始化完成
* epub 库使用事件机制,需要等待 'end' 事件触发后才能访问文件内容
* @param epub epub 实例
* @returns 元数据和章节信息
*/
private waitForEpubInit(epub: any): Promise<{ metadata: EpubMetadata; chapters: EpubChapter[] }> {
return new Promise((resolve, reject) => {
epub.on('end', () => {
// 提取元数据
const metadata: EpubMetadata = {
creator: epub.metadata.creator,
creatorFileAs: epub.metadata.creatorFileAs,
title: epub.metadata.title,
language: epub.metadata.language,
subject: epub.metadata.subject,
date: epub.metadata.date,
description: epub.metadata.description
}
// 提取章节信息
const chapters: EpubChapter[] = epub.flow.map((chapter: any, index: number) => ({
id: chapter.id,
title: chapter.title || `Chapter ${index + 1}`,
order: index + 1
}))
resolve({ metadata, chapters })
})
epub.on('error', (error: Error) => {
reject(error)
})
epub.parse()
})
}
/**
* 获取章节内容
* @param epub epub 实例
* @param chapterId 章节 ID
* @returns 章节文本内容
*/
private getChapter(epub: any, chapterId: string): Promise<string> {
return new Promise((resolve, reject) => {
epub.getChapter(chapterId, (error: Error | null, text: string) => {
if (error) {
reject(error)
} else {
resolve(text)
}
})
})
}
/**
* 从 epub 文件中提取文本内容
* 1. 检查文件是否存在
* 2. 初始化 epub 并获取元数据
* 3. 遍历所有章节并提取文本
* 4. 清理 HTML 标签
* 5. 合并所有章节文本
*/
private async extractTextFromEpub() {
try {
// 检查文件是否存在
if (!fs.existsSync(this.filePath)) {
throw new Error(`File not found: ${this.filePath}`)
}
const epub = new EPub(this.filePath)
// 等待 epub 初始化完成并获取元数据
const { metadata, chapters } = await this.waitForEpubInit(epub)
this.metadata = metadata
if (!epub.flow || epub.flow.length === 0) {
throw new Error('No content found in epub file')
}
const chapterTexts: string[] = []
// 遍历所有章节
for (const chapter of chapters) {
try {
const content = await this.getChapter(epub, chapter.id)
if (!content) {
continue
}
// 移除 HTML 标签并清理文本
const text = content
.replace(/<[^>]*>/g, ' ') // 移除所有 HTML 标签
.replace(/\s+/g, ' ') // 将多个空白字符替换为单个空格
.trim() // 移除首尾空白
if (text) {
chapterTexts.push(text)
}
} catch (error) {
Logger.error(`[EpubLoader] Error processing chapter ${chapter.id}:`, error)
}
}
// 使用双换行符连接所有章节文本
this.extractedText = chapterTexts.join('\n\n')
} catch (error) {
Logger.error('[EpubLoader] Error in extractTextFromEpub:', error)
throw error
}
}
/**
* 生成文本块
* 重写 BaseLoader 的方法,将提取的文本分割成适当大小的块
* 每个块都包含源文件和元数据信息
*/
override async *getUnfilteredChunks() {
// 如果还没有提取文本,先提取
if (!this.extractedText) {
await this.extractTextFromEpub()
}
Logger.info('[EpubLoader] 书名:', this.metadata?.title || '未知书名', ' 文本大小:', this.extractedText.length)
// 创建文本分块器
const chunker = new RecursiveCharacterTextSplitter({
chunkSize: this.chunkSize,
chunkOverlap: this.chunkOverlap
})
// 清理并分割文本
const chunks = await chunker.splitText(cleanString(this.extractedText))
// 为每个文本块添加元数据
for (const chunk of chunks) {
yield {
pageContent: chunk,
metadata: {
source: this.filePath,
title: this.metadata?.title || '',
creator: this.metadata?.creator || '',
language: this.metadata?.language || ''
}
}
}
}
}

View File

@@ -1,15 +1,18 @@
import * as fs from 'node:fs' import * as fs from 'node:fs'
import { LocalPathLoader, RAGApplication, TextLoader } from '@llm-tools/embedjs' import { JsonLoader, LocalPathLoader, RAGApplication, TextLoader } from '@llm-tools/embedjs'
import type { AddLoaderReturn } from '@llm-tools/embedjs-interfaces' import type { AddLoaderReturn } from '@llm-tools/embedjs-interfaces'
import { WebLoader } from '@llm-tools/embedjs-loader-web'
import { LoaderReturn } from '@shared/config/types' import { LoaderReturn } from '@shared/config/types'
import { FileType, KnowledgeBaseParams } from '@types' import { FileType, KnowledgeBaseParams } from '@types'
import Logger from 'electron-log' import Logger from 'electron-log'
import { DraftsExportLoader } from './draftsExportLoader'
import { EpubLoader } from './epubLoader'
import { OdLoader, OdType } from './odLoader' import { OdLoader, OdType } from './odLoader'
// embedjs内置loader类型 // embedjs内置loader类型
const commonExts = ['.pdf', '.csv', '.json', '.docx', '.pptx', '.xlsx', '.md'] const commonExts = ['.pdf', '.csv', '.docx', '.pptx', '.xlsx', '.md']
export async function addOdLoader( export async function addOdLoader(
ragApplication: RAGApplication, ragApplication: RAGApplication,
@@ -69,8 +72,77 @@ export async function addFileLoader(
} as LoaderReturn } as LoaderReturn
} }
// 文本类型 // epub 文件处理
if (file.ext === '.epub') {
const loaderReturn = await ragApplication.addLoader(
new EpubLoader({
filePath: file.path,
chunkSize: base.chunkSize ?? 1000,
chunkOverlap: base.chunkOverlap ?? 200
}) as any,
forceReload
)
return {
entriesAdded: loaderReturn.entriesAdded,
uniqueId: loaderReturn.uniqueId,
uniqueIds: [loaderReturn.uniqueId],
loaderType: loaderReturn.loaderType
} as LoaderReturn
}
// DraftsExport类型 (file.ext会自动转换成小写)
if (['.draftsexport'].includes(file.ext)) {
const loaderReturn = await ragApplication.addLoader(new DraftsExportLoader(file.path) as any, forceReload)
return {
entriesAdded: loaderReturn.entriesAdded,
uniqueId: loaderReturn.uniqueId,
uniqueIds: [loaderReturn.uniqueId],
loaderType: loaderReturn.loaderType
}
}
const fileContent = fs.readFileSync(file.path, 'utf-8') const fileContent = fs.readFileSync(file.path, 'utf-8')
// HTML类型
if (['.html', '.htm'].includes(file.ext)) {
const loaderReturn = await ragApplication.addLoader(
new WebLoader({
urlOrContent: fileContent,
chunkSize: base.chunkSize,
chunkOverlap: base.chunkOverlap
}) as any,
forceReload
)
return {
entriesAdded: loaderReturn.entriesAdded,
uniqueId: loaderReturn.uniqueId,
uniqueIds: [loaderReturn.uniqueId],
loaderType: loaderReturn.loaderType
}
}
// JSON类型
if (['.json'].includes(file.ext)) {
let jsonObject = {}
let jsonParsed = true
try {
jsonObject = JSON.parse(fileContent)
} catch (error) {
jsonParsed = false
Logger.warn('[KnowledgeBase] failed parsing json file, failling back to text processing:', file.path, error)
}
if (jsonParsed) {
const loaderReturn = await ragApplication.addLoader(new JsonLoader({ object: jsonObject }))
return {
entriesAdded: loaderReturn.entriesAdded,
uniqueId: loaderReturn.uniqueId,
uniqueIds: [loaderReturn.uniqueId],
loaderType: loaderReturn.loaderType
}
}
}
// 文本类型
const loaderReturn = await ragApplication.addLoader( const loaderReturn = await ragApplication.addLoader(
new TextLoader({ text: fileContent, chunkSize: base.chunkSize, chunkOverlap: base.chunkOverlap }) as any, new TextLoader({ text: fileContent, chunkSize: base.chunkSize, chunkOverlap: base.chunkOverlap }) as any,
forceReload forceReload

View File

@@ -1,11 +1,13 @@
import { UpdateInfo } from 'builder-util-runtime'
import { app, BrowserWindow, dialog } from 'electron' import { app, BrowserWindow, dialog } from 'electron'
import logger from 'electron-log' import logger from 'electron-log'
import { AppUpdater as _AppUpdater, autoUpdater, UpdateInfo } from 'electron-updater' import { AppUpdater as _AppUpdater, autoUpdater } from 'electron-updater'
import icon from '../../../build/icon.png?asset' import icon from '../../../build/icon.png?asset'
export default class AppUpdater { export default class AppUpdater {
autoUpdater: _AppUpdater = autoUpdater autoUpdater: _AppUpdater = autoUpdater
private releaseInfo: UpdateInfo | undefined
constructor(mainWindow: BrowserWindow) { constructor(mainWindow: BrowserWindow) {
logger.transports.file.level = 'info' logger.transports.file.level = 'info'
@@ -37,17 +39,26 @@ export default class AppUpdater {
// 当需要更新的内容下载完成后 // 当需要更新的内容下载完成后
autoUpdater.on('update-downloaded', (releaseInfo: UpdateInfo) => { autoUpdater.on('update-downloaded', (releaseInfo: UpdateInfo) => {
mainWindow.webContents.send('update-downloaded') mainWindow.webContents.send('update-downloaded', releaseInfo)
this.releaseInfo = releaseInfo
logger.info('下载完成', releaseInfo)
})
logger.info('下载完成,询问用户是否更新', releaseInfo) this.autoUpdater = autoUpdater
}
public async showUpdateDialog(mainWindow: BrowserWindow) {
if (!this.releaseInfo) {
return
}
dialog dialog
.showMessageBox({ .showMessageBox({
type: 'info', type: 'info',
title: '安装更新', title: '安装更新',
icon, icon,
message: `新版本 ${releaseInfo.version} 已准备就绪`, message: `新版本 ${this.releaseInfo.version} 已准备就绪`,
detail: this.formatReleaseNotes(releaseInfo.releaseNotes), detail: this.formatReleaseNotes(this.releaseInfo.releaseNotes),
buttons: ['稍后安装', '立即安装'], buttons: ['稍后安装', '立即安装'],
defaultId: 1, defaultId: 1,
cancelId: 0 cancelId: 0
@@ -60,9 +71,6 @@ export default class AppUpdater {
mainWindow.webContents.send('update-downloaded-cancelled') mainWindow.webContents.send('update-downloaded-cancelled')
} }
}) })
})
this.autoUpdater = autoUpdater
} }
private formatReleaseNotes(releaseNotes: string | ReleaseNoteInfo[] | null | undefined): string { private formatReleaseNotes(releaseNotes: string | ReleaseNoteInfo[] | null | undefined): string {

View File

@@ -1,11 +1,13 @@
import { WebDavConfig } from '@types' import { WebDavConfig } from '@types'
import AdmZip from 'adm-zip' import AdmZip from 'adm-zip'
import { exec } from 'child_process'
import { app } from 'electron' import { app } from 'electron'
import Logger from 'electron-log' import Logger from 'electron-log'
import * as fs from 'fs-extra' import * as fs from 'fs-extra'
import * as path from 'path' import * as path from 'path'
import WebDav from './WebDav' import WebDav from './WebDav'
import { windowService } from './WindowService'
class BackupManager { class BackupManager {
private tempDir = path.join(app.getPath('temp'), 'cherry-studio', 'backup', 'temp') private tempDir = path.join(app.getPath('temp'), 'cherry-studio', 'backup', 'temp')
@@ -18,23 +20,92 @@ class BackupManager {
this.restoreFromWebdav = this.restoreFromWebdav.bind(this) this.restoreFromWebdav = this.restoreFromWebdav.bind(this)
} }
private async setWritableRecursive(dirPath: string): Promise<void> {
try {
const items = await fs.readdir(dirPath, { withFileTypes: true })
for (const item of items) {
const fullPath = path.join(dirPath, item.name)
// 先处理子目录
if (item.isDirectory()) {
await this.setWritableRecursive(fullPath)
}
// 统一设置权限Windows需要特殊处理
await this.forceSetWritable(fullPath)
}
// 确保根目录权限
await this.forceSetWritable(dirPath)
} catch (error) {
Logger.error(`权限设置失败:${dirPath}`, error)
throw error
}
}
// 新增跨平台权限设置方法
private async forceSetWritable(targetPath: string): Promise<void> {
try {
// Windows系统需要先取消只读属性
if (process.platform === 'win32') {
await fs.chmod(targetPath, 0o666) // Windows会忽略权限位但能移除只读
} else {
const stats = await fs.stat(targetPath)
const mode = stats.isDirectory() ? 0o777 : 0o666
await fs.chmod(targetPath, mode)
}
// 双重保险使用文件属性命令Windows专用
if (process.platform === 'win32') {
await exec(`attrib -R "${targetPath}" /L /D`)
}
} catch (error) {
if ((error as NodeJS.ErrnoException).code !== 'ENOENT') {
Logger.warn(`权限设置警告:${targetPath}`, error)
}
}
}
async backup( async backup(
_: Electron.IpcMainInvokeEvent, _: Electron.IpcMainInvokeEvent,
fileName: string, fileName: string,
data: string, data: string,
destinationPath: string = this.backupDir destinationPath: string = this.backupDir
): Promise<string> { ): Promise<string> {
const mainWindow = windowService.getMainWindow()
const onProgress = (processData: { stage: string; progress: number; total: number }) => {
mainWindow?.webContents.send('backup-progress', processData)
Logger.log('[BackupManager] backup progress', processData)
}
try { try {
await fs.ensureDir(this.tempDir) await fs.ensureDir(this.tempDir)
onProgress({ stage: 'preparing', progress: 0, total: 100 })
// 将 data 写入临时文件 // 将 data 写入临时文件
const tempDataPath = path.join(this.tempDir, 'data.json') const tempDataPath = path.join(this.tempDir, 'data.json')
await fs.writeFile(tempDataPath, data) await fs.writeFile(tempDataPath, data)
onProgress({ stage: 'writing_data', progress: 20, total: 100 })
// 复制 Data 目录到临时目录 // 复制 Data 目录到临时目录
const sourcePath = path.join(app.getPath('userData'), 'Data') const sourcePath = path.join(app.getPath('userData'), 'Data')
const tempDataDir = path.join(this.tempDir, 'Data') const tempDataDir = path.join(this.tempDir, 'Data')
await fs.copy(sourcePath, tempDataDir)
// 获取源目录总大小
const totalSize = await this.getDirSize(sourcePath)
let copiedSize = 0
// 使用流式复制
await this.copyDirWithProgress(sourcePath, tempDataDir, (size) => {
copiedSize += size
const progress = Math.min(80, 20 + Math.floor((copiedSize / totalSize) * 60))
onProgress({ stage: 'copying_files', progress, total: 100 })
})
await this.setWritableRecursive(tempDataDir)
onProgress({ stage: 'compressing', progress: 80, total: 100 })
// 使用 adm-zip 创建压缩文件 // 使用 adm-zip 创建压缩文件
const zip = new AdmZip() const zip = new AdmZip()
@@ -44,6 +115,7 @@ class BackupManager {
// 清理临时目录 // 清理临时目录
await fs.remove(this.tempDir) await fs.remove(this.tempDir)
onProgress({ stage: 'completed', progress: 100, total: 100 })
Logger.log('Backup completed successfully') Logger.log('Backup completed successfully')
return backupedFilePath return backupedFilePath
@@ -54,34 +126,54 @@ class BackupManager {
} }
async restore(_: Electron.IpcMainInvokeEvent, backupPath: string): Promise<string> { async restore(_: Electron.IpcMainInvokeEvent, backupPath: string): Promise<string> {
const mainWindow = windowService.getMainWindow()
const onProgress = (processData: { stage: string; progress: number; total: number }) => {
mainWindow?.webContents.send('restore-progress', processData)
Logger.log('[BackupManager] restore progress', processData)
}
try { try {
// 创建临时目录 // 创建临时目录
await fs.ensureDir(this.tempDir) await fs.ensureDir(this.tempDir)
onProgress({ stage: 'preparing', progress: 0, total: 100 })
Logger.log('[backup] step 1: unzip backup file', this.tempDir) Logger.log('[backup] step 1: unzip backup file', this.tempDir)
// 使用 adm-zip 解压 // 使用 adm-zip 解压
const zip = new AdmZip(backupPath) const zip = new AdmZip(backupPath)
zip.extractAllTo(this.tempDir, true) // true 表示覆盖已存在的文件 zip.extractAllTo(this.tempDir, true) // true 表示覆盖已存在的文件
onProgress({ stage: 'extracting', progress: 20, total: 100 })
Logger.log('[backup] step 2: read data.json') Logger.log('[backup] step 2: read data.json')
// 读取 data.json // 读取 data.json
const dataPath = path.join(this.tempDir, 'data.json') const dataPath = path.join(this.tempDir, 'data.json')
const data = await fs.readFile(dataPath, 'utf-8') const data = await fs.readFile(dataPath, 'utf-8')
onProgress({ stage: 'reading_data', progress: 40, total: 100 })
Logger.log('[backup] step 3: restore Data directory') Logger.log('[backup] step 3: restore Data directory')
// 恢复 Data 目录 // 恢复 Data 目录
const sourcePath = path.join(this.tempDir, 'Data') const sourcePath = path.join(this.tempDir, 'Data')
const destPath = path.join(app.getPath('userData'), 'Data') const destPath = path.join(app.getPath('userData'), 'Data')
// 获取源目录总大小
const totalSize = await this.getDirSize(sourcePath)
let copiedSize = 0
await this.setWritableRecursive(destPath)
await fs.remove(destPath) await fs.remove(destPath)
await fs.copy(sourcePath, destPath)
// 使用流式复制
await this.copyDirWithProgress(sourcePath, destPath, (size) => {
copiedSize += size
const progress = Math.min(90, 40 + Math.floor((copiedSize / totalSize) * 50))
onProgress({ stage: 'copying_files', progress, total: 100 })
})
Logger.log('[backup] step 4: clean up temp directory') Logger.log('[backup] step 4: clean up temp directory')
// 清理临时目录 // 清理临时目录
await this.setWritableRecursive(this.tempDir)
await fs.remove(this.tempDir) await fs.remove(this.tempDir)
onProgress({ stage: 'completed', progress: 100, total: 100 })
Logger.log('[backup] step 5: Restore completed successfully') Logger.log('[backup] step 5: Restore completed successfully')
@@ -116,6 +208,44 @@ class BackupManager {
return await this.restore(_, backupedFilePath) return await this.restore(_, backupedFilePath)
} }
private async getDirSize(dirPath: string): Promise<number> {
let size = 0
const items = await fs.readdir(dirPath, { withFileTypes: true })
for (const item of items) {
const fullPath = path.join(dirPath, item.name)
if (item.isDirectory()) {
size += await this.getDirSize(fullPath)
} else {
const stats = await fs.stat(fullPath)
size += stats.size
}
}
return size
}
private async copyDirWithProgress(
source: string,
destination: string,
onProgress: (size: number) => void
): Promise<void> {
const items = await fs.readdir(source, { withFileTypes: true })
for (const item of items) {
const sourcePath = path.join(source, item.name)
const destPath = path.join(destination, item.name)
if (item.isDirectory()) {
await fs.ensureDir(destPath)
await this.copyDirWithProgress(sourcePath, destPath, onProgress)
} else {
const stats = await fs.stat(sourcePath)
await fs.copy(sourcePath, destPath)
onProgress(stats.size)
}
}
}
} }
export default BackupManager export default BackupManager

View File

@@ -1,7 +1,22 @@
/* eslint-disable no-case-declarations */ /* eslint-disable no-case-declarations */
// ExportService // ExportService
import { AlignmentType, BorderStyle, Document, HeadingLevel, Packer, Paragraph, ShadingType, TextRun } from 'docx' import {
AlignmentType,
BorderStyle,
Document,
ExternalHyperlink,
HeadingLevel,
Packer,
Paragraph,
ShadingType,
Table,
TableCell,
TableRow,
TextRun,
VerticalAlign,
WidthType
} from 'docx'
import { dialog } from 'electron' import { dialog } from 'electron'
import Logger from 'electron-log' import Logger from 'electron-log'
import MarkdownIt from 'markdown-it' import MarkdownIt from 'markdown-it'
@@ -21,13 +36,54 @@ export class ExportService {
const tokens = this.md.parse(markdown, {}) const tokens = this.md.parse(markdown, {})
const elements: any[] = [] const elements: any[] = []
let listLevel = 0 let listLevel = 0
let currentTable: Table | null = null
let currentRowCells: TableCell[] = []
let isHeaderRow = false
let tableColumnCount = 0
let tableRows: TableRow[] = [] // Store rows temporarily
const processInlineTokens = (tokens: any[]): TextRun[] => { const processInlineTokens = (tokens: any[], isHeaderRow: boolean): (TextRun | ExternalHyperlink)[] => {
const runs: TextRun[] = [] const runs: (TextRun | ExternalHyperlink)[] = []
for (const token of tokens) { let linkText = ''
let linkUrl = ''
let insideLink = false
for (let i = 0; i < tokens.length; i++) {
const token = tokens[i]
switch (token.type) { switch (token.type) {
case 'link_open':
insideLink = true
linkUrl = token.attrs.find((attr: [string, string]) => attr[0] === 'href')[1]
linkText = tokens[i + 1].content
i += 1
break
case 'link_close':
if (insideLink && linkUrl && linkText) {
// Handle any accumulated link text with the ExternalHyperlink
runs.push(
new ExternalHyperlink({
children: [
new TextRun({
text: linkText,
style: 'Hyperlink',
color: '0000FF',
underline: {
type: 'single'
}
})
],
link: linkUrl
})
)
// Reset link variables
linkText = ''
linkUrl = ''
insideLink = false
}
break
case 'text': case 'text':
runs.push(new TextRun(token.content)) runs.push(new TextRun({ text: token.content, bold: isHeaderRow ? true : false }))
break break
case 'strong': case 'strong':
runs.push(new TextRun({ text: token.content, bold: true })) runs.push(new TextRun({ text: token.content, bold: true }))
@@ -45,7 +101,6 @@ export class ExportService {
for (let i = 0; i < tokens.length; i++) { for (let i = 0; i < tokens.length; i++) {
const token = tokens[i] const token = tokens[i]
switch (token.type) { switch (token.type) {
case 'heading_open': case 'heading_open':
// 获取标题级别 (h1 -> h6) // 获取标题级别 (h1 -> h6)
@@ -68,7 +123,7 @@ export class ExportService {
const inlineTokens = tokens[i + 1].children || [] const inlineTokens = tokens[i + 1].children || []
elements.push( elements.push(
new Paragraph({ new Paragraph({
children: processInlineTokens(inlineTokens), children: processInlineTokens(inlineTokens, false),
spacing: { spacing: {
before: 120, before: 120,
after: 120 after: 120
@@ -93,7 +148,7 @@ export class ExportService {
children: [ children: [
new TextRun({ text: '•', bold: true }), new TextRun({ text: '•', bold: true }),
new TextRun({ text: '\t' }), new TextRun({ text: '\t' }),
...processInlineTokens(itemInlineTokens) ...processInlineTokens(itemInlineTokens, false)
], ],
indent: { indent: {
left: listLevel * 720 left: listLevel * 720
@@ -171,6 +226,116 @@ export class ExportService {
) )
i += 3 i += 3
break break
// 表格处理
case 'table_open':
tableRows = [] // Reset table rows for new table
break
case 'thead_open':
isHeaderRow = true
break
case 'tbody_open':
isHeaderRow = false
break
case 'tr_open':
currentRowCells = []
break
case 'tr_close':
const row = new TableRow({
children: currentRowCells,
tableHeader: isHeaderRow
})
tableRows.push(row)
// 计算表格有多少列(针对第一行)
if (tableColumnCount === 0) {
tableColumnCount = currentRowCells.length
}
break
case 'th_open':
case 'td_open':
const isFirstColumn = currentRowCells.length === 0 // 判断是否是第一列
const borders = {
top: {
style: BorderStyle.NONE
},
bottom: isHeaderRow
? {
style: BorderStyle.SINGLE,
size: 0.5,
color: '000000'
}
: {
style: BorderStyle.NONE
},
left: {
style: BorderStyle.NONE
},
right: {
style: BorderStyle.NONE
}
}
const cellContent = tokens[i + 1]
const cellOptions = {
children: [
new Paragraph({
children: cellContent.children
? processInlineTokens(cellContent.children, isHeaderRow || isFirstColumn)
: [new TextRun({ text: cellContent.content || '', bold: isHeaderRow || isFirstColumn })],
alignment: AlignmentType.CENTER
})
],
verticalAlign: VerticalAlign.CENTER,
borders: borders
}
currentRowCells.push(new TableCell(cellOptions))
i += 2 // 跳过内容和结束标记
break
case 'table_close':
// Create table with the collected rows - avoid using protected properties
// Create the table with all rows
currentTable = new Table({
width: {
size: 100,
type: WidthType.PERCENTAGE
},
rows: tableRows,
borders: {
top: {
style: BorderStyle.SINGLE,
size: 1,
color: '000000'
},
bottom: {
style: BorderStyle.SINGLE,
size: 1,
color: '000000'
},
left: {
style: BorderStyle.NONE
},
right: {
style: BorderStyle.NONE
},
insideHorizontal: {
style: BorderStyle.NONE
},
insideVertical: {
style: BorderStyle.NONE
}
}
})
elements.push(currentTable)
currentTable = null
tableColumnCount = 0
tableRows = []
currentRowCells = []
isHeaderRow = false
break
} }
} }

View File

@@ -1,3 +1,18 @@
/**
* Knowledge Service - Manages knowledge bases using RAG (Retrieval-Augmented Generation)
*
* This service handles creation, management, and querying of knowledge bases from various sources
* including files, directories, URLs, sitemaps, and notes.
*
* Features:
* - Concurrent task processing with workload management
* - Multiple data source support
* - Vector database integration
*
* For detailed documentation, see:
* @see {@link ../../../docs/technical/KnowledgeService.md}
*/
import * as fs from 'node:fs' import * as fs from 'node:fs'
import path from 'node:path' import path from 'node:path'
@@ -8,15 +23,77 @@ import { SitemapLoader } from '@llm-tools/embedjs-loader-sitemap'
import { WebLoader } from '@llm-tools/embedjs-loader-web' import { WebLoader } from '@llm-tools/embedjs-loader-web'
import { AzureOpenAiEmbeddings, OpenAiEmbeddings } from '@llm-tools/embedjs-openai' import { AzureOpenAiEmbeddings, OpenAiEmbeddings } from '@llm-tools/embedjs-openai'
import { addFileLoader } from '@main/loader' import { addFileLoader } from '@main/loader'
import { windowService } from '@main/services/WindowService'
import { getInstanceName } from '@main/utils' import { getInstanceName } from '@main/utils'
import { getAllFiles } from '@main/utils/file' import { getAllFiles } from '@main/utils/file'
import type { LoaderReturn } from '@shared/config/types' import type { LoaderReturn } from '@shared/config/types'
import { FileType, KnowledgeBaseParams, KnowledgeItem } from '@types' import { FileType, KnowledgeBaseParams, KnowledgeItem } from '@types'
import { app } from 'electron' import { app } from 'electron'
import Logger from 'electron-log'
import { v4 as uuidv4 } from 'uuid' import { v4 as uuidv4 } from 'uuid'
export interface KnowledgeBaseAddItemOptions {
base: KnowledgeBaseParams
item: KnowledgeItem
forceReload?: boolean
}
interface KnowledgeBaseAddItemOptionsNonNullableAttribute {
base: KnowledgeBaseParams
item: KnowledgeItem
forceReload: boolean
}
interface EvaluateTaskWorkload {
workload: number
}
type LoaderDoneReturn = LoaderReturn | null
enum LoaderTaskItemState {
PENDING,
PROCESSING,
DONE
}
interface LoaderTaskItem {
state: LoaderTaskItemState
task: () => Promise<unknown>
evaluateTaskWorkload: EvaluateTaskWorkload
}
interface LoaderTask {
loaderTasks: LoaderTaskItem[]
loaderDoneReturn: LoaderDoneReturn
}
interface LoaderTaskOfSet {
loaderTasks: Set<LoaderTaskItem>
loaderDoneReturn: LoaderDoneReturn
}
interface QueueTaskItem {
taskPromise: () => Promise<unknown>
resolve: () => void
evaluateTaskWorkload: EvaluateTaskWorkload
}
const loaderTaskIntoOfSet = (loaderTask: LoaderTask): LoaderTaskOfSet => {
return {
loaderTasks: new Set(loaderTask.loaderTasks),
loaderDoneReturn: loaderTask.loaderDoneReturn
}
}
class KnowledgeService { class KnowledgeService {
private storageDir = path.join(app.getPath('userData'), 'Data', 'KnowledgeBase') private storageDir = path.join(app.getPath('userData'), 'Data', 'KnowledgeBase')
// Byte based
private workload = 0
private processingItemCount = 0
private knowledgeItemProcessingQueueMappingPromise: Map<LoaderTaskOfSet, () => void> = new Map()
private static MAXIMUM_WORKLOAD = 1024 * 1024 * 80
private static MAXIMUM_PROCESSING_ITEM_COUNT = 30
private static ERROR_LOADER_RETURN: LoaderReturn = { entriesAdded: 0, uniqueId: '', uniqueIds: [''], loaderType: '' }
constructor() { constructor() {
this.initStorageDir() this.initStorageDir()
@@ -77,79 +154,312 @@ class KnowledgeService {
} }
} }
public add = async ( private maximumLoad() {
_: Electron.IpcMainInvokeEvent, return (
{ base, item, forceReload = false }: { base: KnowledgeBaseParams; item: KnowledgeItem; forceReload: boolean } this.processingItemCount >= KnowledgeService.MAXIMUM_PROCESSING_ITEM_COUNT ||
): Promise<LoaderReturn> => { this.workload >= KnowledgeService.MAXIMUM_WORKLOAD
const ragApplication = await this.getRagApplication(base) )
}
if (item.type === 'directory') { private fileTask(
ragApplication: RAGApplication,
options: KnowledgeBaseAddItemOptionsNonNullableAttribute
): LoaderTask {
const { base, item, forceReload } = options
const file = item.content as FileType
const loaderTask: LoaderTask = {
loaderTasks: [
{
state: LoaderTaskItemState.PENDING,
task: () =>
addFileLoader(ragApplication, file, base, forceReload)
.then((result) => {
loaderTask.loaderDoneReturn = result
return result
})
.catch((err) => {
Logger.error(err)
return KnowledgeService.ERROR_LOADER_RETURN
}),
evaluateTaskWorkload: { workload: file.size }
}
],
loaderDoneReturn: null
}
return loaderTask
}
private directoryTask(
ragApplication: RAGApplication,
options: KnowledgeBaseAddItemOptionsNonNullableAttribute
): LoaderTask {
const { base, item, forceReload } = options
const directory = item.content as string const directory = item.content as string
const files = getAllFiles(directory) const files = getAllFiles(directory)
const loaderPromises = files.map((file) => addFileLoader(ragApplication, file, base, forceReload)) const totalFiles = files.length
const loaderResults = await Promise.all(loaderPromises) let processedFiles = 0
const uniqueIds = loaderResults.map((result) => result.uniqueId)
return { const sendDirectoryProcessingPercent = (totalFiles: number, processedFiles: number) => {
entriesAdded: loaderResults.length, const mainWindow = windowService.getMainWindow()
mainWindow?.webContents.send('directory-processing-percent', {
itemId: item.id,
percent: (processedFiles / totalFiles) * 100
})
}
const loaderDoneReturn: LoaderDoneReturn = {
entriesAdded: 0,
uniqueId: `DirectoryLoader_${uuidv4()}`, uniqueId: `DirectoryLoader_${uuidv4()}`,
uniqueIds, uniqueIds: [],
loaderType: 'DirectoryLoader' loaderType: 'DirectoryLoader'
} as LoaderReturn }
const loaderTasks: LoaderTaskItem[] = []
for (const file of files) {
loaderTasks.push({
state: LoaderTaskItemState.PENDING,
task: () =>
addFileLoader(ragApplication, file, base, forceReload)
.then((result) => {
loaderDoneReturn.entriesAdded += 1
processedFiles += 1
sendDirectoryProcessingPercent(totalFiles, processedFiles)
loaderDoneReturn.uniqueIds.push(result.uniqueId)
return result
})
.catch((err) => {
Logger.error(err)
return KnowledgeService.ERROR_LOADER_RETURN
}),
evaluateTaskWorkload: { workload: file.size }
})
} }
if (item.type === 'url') {
const content = item.content as string
if (content.startsWith('http')) {
const loaderReturn = await ragApplication.addLoader(
new WebLoader({ urlOrContent: content, chunkSize: base.chunkSize, chunkOverlap: base.chunkOverlap }) as any,
forceReload
)
return { return {
entriesAdded: loaderReturn.entriesAdded, loaderTasks,
uniqueId: loaderReturn.uniqueId, loaderDoneReturn
uniqueIds: [loaderReturn.uniqueId],
loaderType: loaderReturn.loaderType
} as LoaderReturn
} }
} }
if (item.type === 'sitemap') { private urlTask(
ragApplication: RAGApplication,
options: KnowledgeBaseAddItemOptionsNonNullableAttribute
): LoaderTask {
const { base, item, forceReload } = options
const content = item.content as string const content = item.content as string
// @ts-ignore loader type
const loaderReturn = await ragApplication.addLoader( const loaderTask: LoaderTask = {
loaderTasks: [
{
state: LoaderTaskItemState.PENDING,
task: () => {
const loaderReturn = ragApplication.addLoader(
new WebLoader({
urlOrContent: content,
chunkSize: base.chunkSize,
chunkOverlap: base.chunkOverlap
}),
forceReload
) as Promise<LoaderReturn>
return loaderReturn
.then((result) => {
const { entriesAdded, uniqueId, loaderType } = result
loaderTask.loaderDoneReturn = {
entriesAdded: entriesAdded,
uniqueId: uniqueId,
uniqueIds: [uniqueId],
loaderType: loaderType
}
return result
})
.catch((err) => {
Logger.error(err)
return KnowledgeService.ERROR_LOADER_RETURN
})
},
evaluateTaskWorkload: { workload: 1024 * 1024 * 2 }
}
],
loaderDoneReturn: null
}
return loaderTask
}
private sitemapTask(
ragApplication: RAGApplication,
options: KnowledgeBaseAddItemOptionsNonNullableAttribute
): LoaderTask {
const { base, item, forceReload } = options
const content = item.content as string
const loaderTask: LoaderTask = {
loaderTasks: [
{
state: LoaderTaskItemState.PENDING,
task: () =>
ragApplication
.addLoader(
new SitemapLoader({ url: content, chunkSize: base.chunkSize, chunkOverlap: base.chunkOverlap }) as any, new SitemapLoader({ url: content, chunkSize: base.chunkSize, chunkOverlap: base.chunkOverlap }) as any,
forceReload forceReload
) )
return { .then((result) => {
entriesAdded: loaderReturn.entriesAdded, const { entriesAdded, uniqueId, loaderType } = result
uniqueId: loaderReturn.uniqueId, loaderTask.loaderDoneReturn = {
uniqueIds: [loaderReturn.uniqueId], entriesAdded: entriesAdded,
loaderType: loaderReturn.loaderType uniqueId: uniqueId,
} as LoaderReturn uniqueIds: [uniqueId],
loaderType: loaderType
}
return result
})
.catch((err) => {
Logger.error(err)
return KnowledgeService.ERROR_LOADER_RETURN
}),
evaluateTaskWorkload: { workload: 1024 * 1024 * 20 }
}
],
loaderDoneReturn: null
}
return loaderTask
} }
if (item.type === 'note') { private noteTask(
ragApplication: RAGApplication,
options: KnowledgeBaseAddItemOptionsNonNullableAttribute
): LoaderTask {
const { base, item, forceReload } = options
const content = item.content as string const content = item.content as string
console.debug('chunkSize', base.chunkSize) console.debug('chunkSize', base.chunkSize)
const loaderReturn = await ragApplication.addLoader(
const encoder = new TextEncoder()
const contentBytes = encoder.encode(content)
const loaderTask: LoaderTask = {
loaderTasks: [
{
state: LoaderTaskItemState.PENDING,
task: () => {
const loaderReturn = ragApplication.addLoader(
new TextLoader({ text: content, chunkSize: base.chunkSize, chunkOverlap: base.chunkOverlap }), new TextLoader({ text: content, chunkSize: base.chunkSize, chunkOverlap: base.chunkOverlap }),
forceReload forceReload
) ) as Promise<LoaderReturn>
return {
entriesAdded: loaderReturn.entriesAdded, return loaderReturn
uniqueId: loaderReturn.uniqueId, .then(({ entriesAdded, uniqueId, loaderType }) => {
uniqueIds: [loaderReturn.uniqueId], loaderTask.loaderDoneReturn = {
loaderType: loaderReturn.loaderType entriesAdded: entriesAdded,
} as LoaderReturn uniqueId: uniqueId,
uniqueIds: [uniqueId],
loaderType: loaderType
}
})
.catch((err) => {
Logger.error(err)
return KnowledgeService.ERROR_LOADER_RETURN
})
},
evaluateTaskWorkload: { workload: contentBytes.length }
}
],
loaderDoneReturn: null
}
return loaderTask
} }
if (item.type === 'file') { private processingQueueHandle() {
const file = item.content as FileType const getSubtasksUntilMaximumLoad = (): QueueTaskItem[] => {
const queueTaskList: QueueTaskItem[] = []
return await addFileLoader(ragApplication, file, base, forceReload) that: for (const [task, resolve] of this.knowledgeItemProcessingQueueMappingPromise) {
for (const item of task.loaderTasks) {
if (this.maximumLoad()) {
break that
} }
return { entriesAdded: 0, uniqueId: '', uniqueIds: [''], loaderType: '' } const { state, task: taskPromise, evaluateTaskWorkload } = item
if (state !== LoaderTaskItemState.PENDING) {
continue
}
const { workload } = evaluateTaskWorkload
this.workload += workload
this.processingItemCount += 1
item.state = LoaderTaskItemState.PROCESSING
queueTaskList.push({
taskPromise: () =>
taskPromise().then(() => {
this.workload -= workload
this.processingItemCount -= 1
task.loaderTasks.delete(item)
if (task.loaderTasks.size === 0) {
this.knowledgeItemProcessingQueueMappingPromise.delete(task)
resolve()
}
this.processingQueueHandle()
}),
resolve: () => {},
evaluateTaskWorkload
})
}
}
return queueTaskList
}
const subTasks = getSubtasksUntilMaximumLoad()
if (subTasks.length > 0) {
const subTaskPromises = subTasks.map(({ taskPromise }) => taskPromise())
Promise.all(subTaskPromises).then(() => {
subTasks.forEach(({ resolve }) => resolve())
})
}
}
private appendProcessingQueue(task: LoaderTask): Promise<LoaderReturn> {
return new Promise((resolve) => {
this.knowledgeItemProcessingQueueMappingPromise.set(loaderTaskIntoOfSet(task), () => {
resolve(task.loaderDoneReturn!)
})
})
}
public add = (_: Electron.IpcMainInvokeEvent, options: KnowledgeBaseAddItemOptions): Promise<LoaderReturn> => {
return new Promise((resolve) => {
const { base, item, forceReload = false } = options
const optionsNonNullableAttribute = { base, item, forceReload }
this.getRagApplication(base)
.then((ragApplication) => {
const task = (() => {
switch (item.type) {
case 'file':
return this.fileTask(ragApplication, optionsNonNullableAttribute)
case 'directory':
return this.directoryTask(ragApplication, optionsNonNullableAttribute)
case 'url':
return this.urlTask(ragApplication, optionsNonNullableAttribute)
case 'sitemap':
return this.sitemapTask(ragApplication, optionsNonNullableAttribute)
case 'note':
return this.noteTask(ragApplication, optionsNonNullableAttribute)
default:
return null
}
})()
if (task) {
this.appendProcessingQueue(task).then(() => {
resolve(task.loaderDoneReturn!)
})
this.processingQueueHandle()
} else {
resolve(KnowledgeService.ERROR_LOADER_RETURN)
}
})
.catch((err) => {
Logger.error(err)
resolve(KnowledgeService.ERROR_LOADER_RETURN)
})
})
} }
public remove = async ( public remove = async (

View File

@@ -22,7 +22,11 @@ function getShortcutHandler(shortcut: Shortcut) {
case 'show_app': case 'show_app':
return (window: BrowserWindow) => { return (window: BrowserWindow) => {
if (window.isVisible()) { if (window.isVisible()) {
if (window.isFocused()) {
window.hide() window.hide()
} else {
window.focus()
}
} else { } else {
window.show() window.show()
window.focus() window.focus()
@@ -43,8 +47,8 @@ function formatShortcutKey(shortcut: string[]): string {
function handleZoom(delta: number) { function handleZoom(delta: number) {
return (window: BrowserWindow) => { return (window: BrowserWindow) => {
const currentZoom = window.webContents.getZoomFactor() const currentZoom = configManager.getZoomFactor()
const newZoom = currentZoom + delta const newZoom = Number((currentZoom + delta).toFixed(1))
if (newZoom >= 0.1 && newZoom <= 5.0) { if (newZoom >= 0.1 && newZoom <= 5.0) {
window.webContents.setZoomFactor(newZoom) window.webContents.setZoomFactor(newZoom)
configManager.setZoomFactor(newZoom) configManager.setZoomFactor(newZoom)
@@ -52,8 +56,65 @@ function handleZoom(delta: number) {
} }
} }
const convertShortcutRecordedByKeyboardEventKeyValueToElectronGlobalShortcutFormat = (
shortcut: string | string[]
): string => {
const accelerator = (() => {
if (Array.isArray(shortcut)) {
return shortcut
} else {
return shortcut.split('+').map((key) => key.trim())
}
})()
return accelerator
.map((key) => {
switch (key) {
case 'Command':
return 'CommandOrControl'
case 'Control':
return 'Control'
case 'Ctrl':
return 'Control'
case 'ArrowUp':
return 'Up'
case 'ArrowDown':
return 'Down'
case 'ArrowLeft':
return 'Left'
case 'ArrowRight':
return 'Right'
case 'AltGraph':
return 'Alt'
case 'Slash':
return '/'
case 'Semicolon':
return ';'
case 'BracketLeft':
return '['
case 'BracketRight':
return ']'
case 'Backslash':
return '\\'
case 'Quote':
return "'"
case 'Comma':
return ','
case 'Minus':
return '-'
case 'Equal':
return '='
default:
return key
}
})
.join('+')
}
export function registerShortcuts(window: BrowserWindow) { export function registerShortcuts(window: BrowserWindow) {
window.once('ready-to-show', () => {
window.webContents.setZoomFactor(configManager.getZoomFactor()) window.webContents.setZoomFactor(configManager.getZoomFactor())
})
const register = () => { const register = () => {
if (window.isDestroyed()) return if (window.isDestroyed()) return
@@ -75,11 +136,11 @@ export function registerShortcuts(window: BrowserWindow) {
const accelerator = formatShortcutKey(shortcut.shortcut) const accelerator = formatShortcutKey(shortcut.shortcut)
if (shortcut.key === 'show_app') { if (shortcut.key === 'show_app' && shortcut.enabled) {
showAppAccelerator = accelerator showAppAccelerator = accelerator
} }
if (shortcut.key === 'mini_window') { if (shortcut.key === 'mini_window' && shortcut.enabled) {
showMiniWindowAccelerator = accelerator showMiniWindowAccelerator = accelerator
} }
@@ -100,7 +161,10 @@ export function registerShortcuts(window: BrowserWindow) {
} }
if (shortcut.enabled) { if (shortcut.enabled) {
globalShortcut.register(formatShortcutKey(shortcut.shortcut), () => handler(window)) const accelerator = convertShortcutRecordedByKeyboardEventKeyValueToElectronGlobalShortcutFormat(
shortcut.shortcut
)
globalShortcut.register(accelerator, () => handler(window))
} }
} catch (error) { } catch (error) {
Logger.error(`[ShortcutService] Failed to register shortcut ${shortcut.key}`) Logger.error(`[ShortcutService] Failed to register shortcut ${shortcut.key}`)
@@ -116,12 +180,16 @@ export function registerShortcuts(window: BrowserWindow) {
if (showAppAccelerator) { if (showAppAccelerator) {
const handler = getShortcutHandler({ key: 'show_app' } as Shortcut) const handler = getShortcutHandler({ key: 'show_app' } as Shortcut)
handler && globalShortcut.register(showAppAccelerator, () => handler(window)) const accelerator =
convertShortcutRecordedByKeyboardEventKeyValueToElectronGlobalShortcutFormat(showAppAccelerator)
handler && globalShortcut.register(accelerator, () => handler(window))
} }
if (showMiniWindowAccelerator) { if (showMiniWindowAccelerator) {
const handler = getShortcutHandler({ key: 'mini_window' } as Shortcut) const handler = getShortcutHandler({ key: 'mini_window' } as Shortcut)
handler && globalShortcut.register(showMiniWindowAccelerator, () => handler(window)) const accelerator =
convertShortcutRecordedByKeyboardEventKeyValueToElectronGlobalShortcutFormat(showMiniWindowAccelerator)
handler && globalShortcut.register(accelerator, () => handler(window))
} }
} catch (error) { } catch (error) {
Logger.error('[ShortcutService] Failed to unregister shortcuts') Logger.error('[ShortcutService] Failed to unregister shortcuts')

View File

@@ -28,6 +28,7 @@ export class WindowService {
public createMainWindow(): BrowserWindow { public createMainWindow(): BrowserWindow {
if (this.mainWindow && !this.mainWindow.isDestroyed()) { if (this.mainWindow && !this.mainWindow.isDestroyed()) {
this.mainWindow.show()
return this.mainWindow return this.mainWindow
} }
@@ -50,7 +51,7 @@ export class WindowService {
show: false, // 初始不显示 show: false, // 初始不显示
autoHideMenuBar: true, autoHideMenuBar: true,
transparent: isMac, transparent: isMac,
vibrancy: 'under-window', vibrancy: 'sidebar',
visualEffectState: 'active', visualEffectState: 'active',
titleBarStyle: isLinux ? 'default' : 'hidden', titleBarStyle: isLinux ? 'default' : 'hidden',
titleBarOverlay: theme === 'dark' ? titleBarOverlayDark : titleBarOverlayLight, titleBarOverlay: theme === 'dark' ? titleBarOverlayDark : titleBarOverlayLight,
@@ -149,6 +150,17 @@ export class WindowService {
this.wasFullScreen = false this.wasFullScreen = false
mainWindow.webContents.send('fullscreen-status-changed', false) mainWindow.webContents.send('fullscreen-status-changed', false)
}) })
// 添加Escape键退出全屏的支持
mainWindow.webContents.on('before-input-event', (event, input) => {
// 当按下Escape键且窗口处于全屏状态时退出全屏
if (input.key === 'Escape' && !input.alt && !input.control && !input.meta && !input.shift) {
if (mainWindow.isFullScreen()) {
event.preventDefault()
mainWindow.setFullScreen(false)
}
}
})
} }
private setupWebContentsHandlers(mainWindow: BrowserWindow) { private setupWebContentsHandlers(mainWindow: BrowserWindow) {
@@ -240,25 +252,45 @@ export class WindowService {
return app.quit() return app.quit()
} }
// 如果是全屏状态,直接退出 // 如果是Windows或Linux且处于全屏状态则退出应用
if (this.wasFullScreen) { if (this.wasFullScreen) {
if (isWin || isLinux) {
return app.quit() return app.quit()
} else {
event.preventDefault()
mainWindow.setFullScreen(false)
return
}
} }
event.preventDefault() event.preventDefault()
mainWindow.hide() mainWindow.hide()
}) })
mainWindow.on('closed', () => {
this.mainWindow = null
})
mainWindow.on('show', () => {
if (this.miniWindow && !this.miniWindow.isDestroyed()) {
this.miniWindow.hide()
}
})
} }
public showMainWindow() { public showMainWindow() {
if (this.mainWindow) { if (this.miniWindow && !this.miniWindow.isDestroyed()) {
this.miniWindow.hide()
}
if (this.mainWindow && !this.mainWindow.isDestroyed()) {
if (this.mainWindow.isMinimized()) { if (this.mainWindow.isMinimized()) {
return this.mainWindow.restore() this.mainWindow.restore()
} }
this.mainWindow.show() this.mainWindow.show()
this.mainWindow.focus() this.mainWindow.focus()
} else { } else {
this.createMainWindow() this.mainWindow = this.createMainWindow()
this.mainWindow.focus()
} }
} }
@@ -269,7 +301,10 @@ export class WindowService {
return return
} }
if (this.selectionMenuWindow) { if (this.mainWindow && !this.mainWindow.isDestroyed()) {
this.mainWindow.hide()
}
if (this.selectionMenuWindow && !this.selectionMenuWindow.isDestroyed()) {
this.selectionMenuWindow.hide() this.selectionMenuWindow.hide()
} }

View File

@@ -18,6 +18,10 @@ export function getAllFiles(dirPath: string, arrayOfFiles: FileType[] = []): Fil
const files = fs.readdirSync(dirPath) const files = fs.readdirSync(dirPath)
files.forEach((file) => { files.forEach((file) => {
if (file.startsWith('.')) {
return
}
const fullPath = path.join(dirPath, file) const fullPath = path.join(dirPath, file)
if (fs.statSync(fullPath).isDirectory()) { if (fs.statSync(fullPath).isDirectory()) {
arrayOfFiles = getAllFiles(fullPath, arrayOfFiles) arrayOfFiles = getAllFiles(fullPath, arrayOfFiles)

View File

@@ -15,6 +15,7 @@ declare global {
api: { api: {
getAppInfo: () => Promise<AppInfo> getAppInfo: () => Promise<AppInfo>
checkForUpdate: () => Promise<{ currentVersion: string; updateInfo: UpdateInfo | null }> checkForUpdate: () => Promise<{ currentVersion: string; updateInfo: UpdateInfo | null }>
showUpdateDialog: () => Promise<void>
openWebsite: (url: string) => void openWebsite: (url: string) => void
setProxy: (proxy: string | undefined) => void setProxy: (proxy: string | undefined) => void
setLanguage: (theme: LanguageVarious) => void setLanguage: (theme: LanguageVarious) => void
@@ -119,6 +120,9 @@ declare global {
encrypt: (text: string, secretKey: string, iv: string) => Promise<{ iv: string; encryptedData: string }> encrypt: (text: string, secretKey: string, iv: string) => Promise<{ iv: string; encryptedData: string }>
decrypt: (encryptedData: string, iv: string, secretKey: string) => Promise<string> decrypt: (encryptedData: string, iv: string, secretKey: string) => Promise<string>
} }
shell: {
openExternal: (url: string, options?: OpenExternalOptions) => Promise<void>
}
} }
} }
} }

View File

@@ -1,6 +1,6 @@
import { electronAPI } from '@electron-toolkit/preload' import { electronAPI } from '@electron-toolkit/preload'
import { FileType, KnowledgeBaseParams, KnowledgeItem, Shortcut, WebDavConfig } from '@types' import { FileType, KnowledgeBaseParams, KnowledgeItem, Shortcut, WebDavConfig } from '@types'
import { contextBridge, ipcRenderer, OpenDialogOptions } from 'electron' import { contextBridge, ipcRenderer, OpenDialogOptions, shell } from 'electron'
// Custom APIs for renderer // Custom APIs for renderer
const api = { const api = {
@@ -8,6 +8,7 @@ const api = {
reload: () => ipcRenderer.invoke('app:reload'), reload: () => ipcRenderer.invoke('app:reload'),
setProxy: (proxy: string) => ipcRenderer.invoke('app:proxy', proxy), setProxy: (proxy: string) => ipcRenderer.invoke('app:proxy', proxy),
checkForUpdate: () => ipcRenderer.invoke('app:check-for-update'), checkForUpdate: () => ipcRenderer.invoke('app:check-for-update'),
showUpdateDialog: () => ipcRenderer.invoke('app:show-update-dialog'),
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'), restartTray: () => ipcRenderer.invoke('app:restart-tray'),
@@ -104,6 +105,9 @@ const api = {
encrypt: (text: string, secretKey: string, iv: string) => ipcRenderer.invoke('aes:encrypt', text, secretKey, iv), encrypt: (text: string, secretKey: string, iv: string) => ipcRenderer.invoke('aes:encrypt', text, secretKey, iv),
decrypt: (encryptedData: string, iv: string, secretKey: string) => decrypt: (encryptedData: string, iv: string, secretKey: string) =>
ipcRenderer.invoke('aes:decrypt', encryptedData, iv, secretKey) ipcRenderer.invoke('aes:decrypt', encryptedData, iv, secretKey)
},
shell: {
openExternal: shell.openExternal
} }
} }

View File

@@ -6,6 +6,7 @@
<meta name="viewport" content="initial-scale=1, width=device-width" /> <meta name="viewport" content="initial-scale=1, width=device-width" />
<meta http-equiv="Content-Security-Policy" <meta http-equiv="Content-Security-Policy"
content="default-src 'self'; connect-src blob: *; script-src 'self' 'unsafe-eval' *; worker-src 'self' blob:; style-src 'self' 'unsafe-inline' *; font-src 'self' data: *; img-src 'self' data: file: * blob:; frame-src * file:" /> content="default-src 'self'; connect-src blob: *; script-src 'self' 'unsafe-eval' *; worker-src 'self' blob:; style-src 'self' 'unsafe-inline' *; font-src 'self' data: *; img-src 'self' data: file: * blob:; frame-src * file:" />
<title>Cherry Studio</title>
<style> <style>
html, html,

View File

@@ -8,8 +8,10 @@ import { PersistGate } from 'redux-persist/integration/react'
import Sidebar from './components/app/Sidebar' import Sidebar from './components/app/Sidebar'
import TopViewContainer from './components/TopView' import TopViewContainer from './components/TopView'
import AntdProvider from './context/AntdProvider' import AntdProvider from './context/AntdProvider'
import StyleSheetManager from './context/StyleSheetManager'
import { SyntaxHighlighterProvider } from './context/SyntaxHighlighterProvider' import { SyntaxHighlighterProvider } from './context/SyntaxHighlighterProvider'
import { ThemeProvider } from './context/ThemeProvider' import { ThemeProvider } from './context/ThemeProvider'
import NavigationHandler from './handler/NavigationHandler'
import AgentsPage from './pages/agents/AgentsPage' import AgentsPage from './pages/agents/AgentsPage'
import AppsPage from './pages/apps/AppsPage' import AppsPage from './pages/apps/AppsPage'
import FilesPage from './pages/files/FilesPage' import FilesPage from './pages/files/FilesPage'
@@ -22,12 +24,14 @@ import TranslatePage from './pages/translate/TranslatePage'
function App(): JSX.Element { function App(): JSX.Element {
return ( return (
<Provider store={store}> <Provider store={store}>
<StyleSheetManager>
<ThemeProvider> <ThemeProvider>
<AntdProvider> <AntdProvider>
<SyntaxHighlighterProvider> <SyntaxHighlighterProvider>
<PersistGate loading={null} persistor={persistor}> <PersistGate loading={null} persistor={persistor}>
<TopViewContainer> <TopViewContainer>
<HashRouter> <HashRouter>
<NavigationHandler />
<Sidebar /> <Sidebar />
<Routes> <Routes>
<Route path="/" element={<HomePage />} /> <Route path="/" element={<HomePage />} />
@@ -45,6 +49,7 @@ function App(): JSX.Element {
</SyntaxHighlighterProvider> </SyntaxHighlighterProvider>
</AntdProvider> </AntdProvider>
</ThemeProvider> </ThemeProvider>
</StyleSheetManager>
</Provider> </Provider>
) )
} }

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

View File

@@ -0,0 +1 @@
<svg fill="currentColor" fill-rule="evenodd" height="1em" style="flex:none;line-height:1" viewBox="0 0 24 24" width="1em" xmlns="http://www.w3.org/2000/svg"><title>Dify</title><clipPath id="lobe-icons-dify-fill"><path d="M1 0h10.286c6.627 0 12 5.373 12 12s-5.373 12-12 12H1V0z"></path></clipPath><foreignObject clip-path="url(#lobe-icons-dify-fill)" height="24" style="background:conic-gradient(from 180deg at 50% 50%, #0222C3, #8FB1F4, #FFFFFF)" width="24"></foreignObject></svg>

After

Width:  |  Height:  |  Size: 480 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

After

Width:  |  Height:  |  Size: 7.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 724 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 KiB

View File

Before

Width:  |  Height:  |  Size: 3.2 KiB

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

View File

@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="512px" height="512px" viewBox="0 0 512 512" version="1.1">
<g id="surface1">
<path style=" stroke:none;fill-rule:nonzero;fill:rgb(85.09804%,85.09804%,85.09804%);fill-opacity:1;" d="M 512 256 C 512 114.613281 397.386719 0 256 0 C 114.613281 0 0 114.613281 0 256 C 0 397.386719 114.613281 512 256 512 C 397.386719 512 512 397.386719 512 256 Z M 512 256 "/>
<path style=" stroke:none;fill-rule:nonzero;fill:rgb(0%,0%,0%);fill-opacity:1;" d="M 256.011719 114.753906 C 167.050781 114.753906 94.945312 186.261719 94.945312 274.507812 L 94.945312 350.988281 L 124.628906 350.988281 L 124.628906 343.359375 C 124.628906 307.574219 153.867188 278.558594 189.941406 278.558594 C 226.015625 278.558594 255.253906 307.585938 255.253906 343.359375 L 255.253906 350.988281 L 284.9375 350.988281 L 284.9375 343.359375 C 284.9375 291.308594 242.390625 249.140625 189.929688 249.140625 C 169.503906 249.140625 150.582031 255.53125 135.082031 266.433594 C 151.296875 234.464844 184.691406 212.535156 223.242188 212.535156 C 277.707031 212.535156 321.867188 256.339844 321.867188 310.355469 L 321.867188 350.996094 L 351.5625 350.996094 L 351.5625 310.355469 C 351.5625 240.074219 294.113281 183.082031 223.242188 183.082031 C 191.382812 183.082031 162.230469 194.601562 139.785156 213.683594 C 161.824219 172.375 205.578125 144.214844 256 144.214844 C 328.566406 144.214844 387.382812 202.550781 387.382812 274.515625 L 387.382812 350.996094 L 417.066406 350.996094 L 417.066406 274.515625 C 417.066406 186.28125 344.960938 114.761719 256 114.761719 Z M 256.011719 114.753906 "/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.1 KiB

After

Width:  |  Height:  |  Size: 7.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.4 KiB

After

Width:  |  Height:  |  Size: 7.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.1 KiB

View File

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.8 KiB

After

Width:  |  Height:  |  Size: 6.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

View File

@@ -0,0 +1,14 @@
<svg width="778" height="257" viewBox="0 0 778 257" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M97.1853 5.35901L127.346 53.1064C132.19 60.7745 126.68 70.7725 117.61 70.7725H105.279V142.278H87.4492V-0.00683594C91.1876 -0.00683594 94.926 1.78179 97.1853 5.35901Z" fill="#8FBCFA"/>
<path d="M47.5482 53.1064L77.7098 5.35901C79.9691 1.78179 83.7075 -0.00683594 87.4459 -0.00683594V142.279C81.0587 141.981 74.8755 143.829 69.616 147.544V70.7725H57.2849C48.2149 70.7725 42.7047 60.7745 47.5482 53.1064Z" fill="#468BFF"/>
<path d="M182.003 189.445L107.34 189.445C111.648 184.622 114.201 178.481 114.476 171.615H252.782C252.782 175.353 250.993 179.092 247.416 181.351L199.669 211.512C192.001 216.356 182.003 210.846 182.003 201.776V189.445Z" fill="#FDBB11"/>
<path d="M199.668 131.718L247.415 161.879C250.993 164.138 252.781 167.877 252.781 171.615H114.471C114.72 165.212 112.733 158.898 108.957 153.785H182.002V141.454C182.002 132.384 192 126.874 199.668 131.718Z" fill="#F6D785"/>
<path d="M46.9409 209.797L3.37891 253.359C6.02226 256.003 9.93035 257.381 14.0576 256.45L69.1472 244.014C77.9944 242.017 81.1678 231.051 74.7545 224.638L66.035 215.918L98.7916 183.055C105.771 176.075 105.462 164.899 98.6758 158.113L46.9409 209.797Z" fill="#FF9A9D"/>
<path d="M40.8221 190.708L73.6898 157.963C80.6694 150.983 91.8931 151.328 98.679 158.113L46.9436 209.802L3.38131 253.364C0.737954 250.721 -0.640662 246.812 0.291 242.685L12.7265 187.596C14.7236 178.748 25.6895 175.575 32.1028 181.988L40.8221 190.708Z" fill="#FE363B"/>
<path d="M777.344 93.6689L718.337 234.049H692.704L713.348 186.567L675.156 93.6689H702.166L726.766 160.246L751.711 93.6689H777.344Z" fill="#FFFFFF"/>
<path d="M664.096 70.1191V188.976H640.012V70.1191H664.096Z" fill="#FFFFFF"/>
<path d="M606.041 82.2736C601.797 82.2736 598.242 80.9547 595.375 78.3168C592.622 75.5643 591.246 72.181 591.246 68.1668C591.246 64.1527 592.622 60.8267 595.375 58.1889C598.242 55.4363 601.797 54.0601 606.041 54.0601C610.284 54.0601 613.783 55.4363 616.535 58.1889C619.402 60.8267 620.836 63.6942 620.836 67.7084C620.836 71.7225 619.402 75.5643 616.535 78.3168C613.783 80.9547 610.284 82.2736 606.041 82.2736ZM617.911 93.6279V188.978H593.827V93.6279H617.911Z" fill="#FFFFFF"/>
<path d="M532.3 166.783L556.385 93.6689H582.018L546.751 188.976H517.505L482.41 93.6689H508.215L532.3 166.783Z" fill="#FFFFFF"/>
<path d="M371.52 140.972C371.52 131.338 373.412 122.794 377.197 115.339C381.096 107.884 386.314 102.15 392.852 98.1355C399.504 94.1213 406.901 92.1143 415.044 92.1143C422.155 92.1143 428.348 93.5479 433.624 96.4151C439.014 99.2823 443.315 102.895 446.526 107.253V93.6626H470.783V188.969H446.526V175.035C443.43 179.507 439.129 183.235 433.624 186.217C428.233 189.084 421.983 190.518 414.872 190.518C406.844 190.518 399.504 188.453 392.852 184.324C386.314 180.196 381.096 174.404 377.197 166.949C373.412 159.38 371.52 150.72 371.52 140.972ZM446.526 141.316C446.526 135.467 445.379 130.478 443.086 126.349C440.792 122.105 437.695 118.894 433.796 116.715C429.896 114.421 425.71 113.274 421.237 113.274C416.764 113.274 412.636 114.364 408.851 116.543C405.066 118.722 401.97 121.933 399.561 126.177C397.267 130.306 396.12 135.237 396.12 140.972C396.12 146.706 397.267 151.753 399.561 156.111C401.97 160.354 405.066 163.623 408.851 165.917C412.75 168.211 416.879 169.357 421.237 169.357C425.71 169.357 429.896 168.268 433.796 166.089C437.695 163.795 440.792 160.584 443.086 156.455C445.379 152.211 446.526 147.165 446.526 141.316Z" fill="#FFFFFF"/>
<path d="M340.767 113.445V159.55C340.767 162.762 341.513 165.113 343.004 166.604C344.609 167.98 347.247 168.668 350.917 168.668H362.099V188.968H346.96C326.66 188.968 316.51 179.105 316.51 159.378V113.445H305.156V93.6614H316.51V70.0928H340.767V93.6614H362.099V113.445H340.767Z" fill="#FFFFFF"/>
</svg>

After

Width:  |  Height:  |  Size: 3.7 KiB

View File

@@ -0,0 +1,14 @@
<svg width="778" height="257" viewBox="0 0 778 257" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M97.1853 5.35901L127.346 53.1064C132.19 60.7745 126.68 70.7725 117.61 70.7725H105.279V142.278H87.4492V-0.00683594C91.1876 -0.00683594 94.926 1.78179 97.1853 5.35901Z" fill="#8FBCFA"/>
<path d="M47.5482 53.1064L77.7098 5.35901C79.9691 1.78179 83.7075 -0.00683594 87.4459 -0.00683594V142.279C81.0587 141.981 74.8755 143.829 69.616 147.544V70.7725H57.2849C48.2149 70.7725 42.7047 60.7745 47.5482 53.1064Z" fill="#468BFF"/>
<path d="M182.003 189.445L107.34 189.445C111.648 184.622 114.201 178.481 114.476 171.615H252.782C252.782 175.353 250.993 179.092 247.416 181.351L199.669 211.512C192.001 216.356 182.003 210.846 182.003 201.776V189.445Z" fill="#FDBB11"/>
<path d="M199.668 131.718L247.415 161.879C250.993 164.138 252.781 167.877 252.781 171.615H114.471C114.72 165.212 112.733 158.898 108.957 153.785H182.002V141.454C182.002 132.384 192 126.874 199.668 131.718Z" fill="#F6D785"/>
<path d="M46.9409 209.797L3.37891 253.359C6.02226 256.003 9.93035 257.381 14.0576 256.45L69.1472 244.014C77.9944 242.017 81.1678 231.051 74.7545 224.638L66.035 215.918L98.7916 183.055C105.771 176.075 105.462 164.899 98.6758 158.113L46.9409 209.797Z" fill="#FF9A9D"/>
<path d="M40.8221 190.708L73.6898 157.963C80.6694 150.983 91.8931 151.328 98.679 158.113L46.9436 209.802L3.38131 253.364C0.737954 250.721 -0.640662 246.812 0.291 242.685L12.7265 187.596C14.7236 178.748 25.6895 175.575 32.1028 181.988L40.8221 190.708Z" fill="#FE363B"/>
<path d="M777.344 93.6689L718.337 234.049H692.704L713.348 186.567L675.156 93.6689H702.166L726.766 160.246L751.711 93.6689H777.344Z" fill="#2C2F32"/>
<path d="M664.096 70.1191V188.976H640.012V70.1191H664.096Z" fill="#2C2F32"/>
<path d="M606.041 82.2736C601.797 82.2736 598.242 80.9547 595.375 78.3168C592.622 75.5643 591.246 72.181 591.246 68.1668C591.246 64.1527 592.622 60.8267 595.375 58.1889C598.242 55.4363 601.797 54.0601 606.041 54.0601C610.284 54.0601 613.783 55.4363 616.535 58.1889C619.402 60.8267 620.836 63.6942 620.836 67.7084C620.836 71.7225 619.402 75.5643 616.535 78.3168C613.783 80.9547 610.284 82.2736 606.041 82.2736ZM617.911 93.6279V188.978H593.827V93.6279H617.911Z" fill="#2C2F32"/>
<path d="M532.3 166.783L556.385 93.6689H582.018L546.751 188.976H517.505L482.41 93.6689H508.215L532.3 166.783Z" fill="#2C2F32"/>
<path d="M371.52 140.972C371.52 131.338 373.412 122.794 377.197 115.339C381.096 107.884 386.314 102.15 392.852 98.1355C399.504 94.1213 406.901 92.1143 415.044 92.1143C422.155 92.1143 428.348 93.5479 433.624 96.4151C439.014 99.2823 443.315 102.895 446.526 107.253V93.6626H470.783V188.969H446.526V175.035C443.43 179.507 439.129 183.235 433.624 186.217C428.233 189.084 421.983 190.518 414.872 190.518C406.844 190.518 399.504 188.453 392.852 184.324C386.314 180.196 381.096 174.404 377.197 166.949C373.412 159.38 371.52 150.72 371.52 140.972ZM446.526 141.316C446.526 135.467 445.379 130.478 443.086 126.349C440.792 122.105 437.695 118.894 433.796 116.715C429.896 114.421 425.71 113.274 421.237 113.274C416.764 113.274 412.636 114.364 408.851 116.543C405.066 118.722 401.97 121.933 399.561 126.177C397.267 130.306 396.12 135.237 396.12 140.972C396.12 146.706 397.267 151.753 399.561 156.111C401.97 160.354 405.066 163.623 408.851 165.917C412.75 168.211 416.879 169.357 421.237 169.357C425.71 169.357 429.896 168.268 433.796 166.089C437.695 163.795 440.792 160.584 443.086 156.455C445.379 152.211 446.526 147.165 446.526 141.316Z" fill="#2C2F32"/>
<path d="M340.767 113.445V159.55C340.767 162.762 341.513 165.113 343.004 166.604C344.609 167.98 347.247 168.668 350.917 168.668H362.099V188.968H346.96C326.66 188.968 316.51 179.105 316.51 159.378V113.445H305.156V93.6614H316.51V70.0928H340.767V93.6614H362.099V113.445H340.767Z" fill="#2C2F32"/>
</svg>

After

Width:  |  Height:  |  Size: 3.7 KiB

View File

@@ -1,3 +1,5 @@
@use './container.scss';
#inputbar { #inputbar {
resize: none; resize: none;
} }
@@ -10,6 +12,10 @@
outline: none; outline: none;
} }
.ant-tabs-tabpane:focus-visible {
outline: none;
}
.ant-segmented-group { .ant-segmented-group {
gap: 4px; gap: 4px;
} }

View File

@@ -0,0 +1,7 @@
#content-container {
background-color: var(--color-background);
border-top: 0.5px solid var(--color-border);
border-top-left-radius: 10px;
border-left: 0.5px solid var(--color-border);
box-shadow: -2px 0px 20px -4px rgba(0, 0, 0, 0.06);
}

View File

@@ -1,6 +1,7 @@
@import './markdown.scss'; @use './markdown.scss';
@import './ant.scss'; @use './ant.scss';
@import './scrollbar.scss'; @use './scrollbar.scss';
@use './container.scss';
@import '../fonts/icon-fonts/iconfont.css'; @import '../fonts/icon-fonts/iconfont.css';
@import '../fonts/ubuntu/ubuntu.css'; @import '../fonts/ubuntu/ubuntu.css';
@@ -88,7 +89,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-background-opacity: rgba(235, 235, 235, 0.7);
--color-primary: #00b96b; --color-primary: #00b96b;
--color-primary-soft: #00b96b99; --color-primary-soft: #00b96b99;
@@ -176,14 +177,6 @@ body,
flex: 1; flex: 1;
} }
#content-container {
background-color: var(--color-background);
border-top: 0.5px solid var(--color-border);
border-top-left-radius: 10px;
border-left: 0.5px solid var(--color-border);
box-shadow: -2px 0px 20px -4px rgba(0, 0, 0, 0.06);
}
.loader { .loader {
width: 16px; width: 16px;
height: 16px; height: 16px;
@@ -256,6 +249,9 @@ body,
border: 1px solid var(--color-background-mute); border: 1px solid var(--color-background-mute);
} }
} }
.group-menu-bar {
background-color: var(--color-background);
}
code { code {
color: var(--color-text); color: var(--color-text);
} }

View File

@@ -64,6 +64,10 @@
&:first-child { &:first-child {
margin-top: 0; margin-top: 0;
} }
&:has(+ ul) {
margin-bottom: 0;
}
} }
ul { ul {

View File

@@ -14,7 +14,15 @@ const ModelAvatar: FC<Props> = ({ model, size, props }) => {
return ( return (
<Avatar <Avatar
src={getModelLogo(model?.id || '')} src={getModelLogo(model?.id || '')}
style={{ width: size, height: size, display: 'flex', alignItems: 'center', justifyContent: 'center' }} style={{
width: size,
height: size,
minWidth: size,
minHeight: size,
display: 'flex',
alignItems: 'center',
justifyContent: 'center'
}}
{...props}> {...props}>
{first(model?.name)} {first(model?.name)}
</Avatar> </Avatar>

View File

@@ -46,23 +46,28 @@ const DragableList: FC<Props<any>> = ({
<DragDropContext onDragStart={onDragStart} onDragEnd={_onDragEnd}> <DragDropContext onDragStart={onDragStart} onDragEnd={_onDragEnd}>
<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) => {
const id = item.id || item const id = item.id || item
return ( return (
<Draggable key={`draggable_${id}_${index}`} draggableId={id} index={index} {...droppableProps}> <Draggable key={`draggable_${id}_${index}`} draggableId={id} index={index}>
{(provided) => ( {(provided) => (
<div <div
ref={provided.innerRef} ref={provided.innerRef}
{...provided.draggableProps} {...provided.draggableProps}
{...provided.dragHandleProps} {...provided.dragHandleProps}
style={{ ...provided.draggableProps.style, marginBottom: 8, ...listStyle }}> style={{
...listStyle,
...provided.draggableProps.style,
marginBottom: 8
}}>
{children(item, index)} {children(item, index)}
</div> </div>
)} )}
</Draggable> </Draggable>
) )
})} })}
{provided.placeholder}
</div> </div>
)} )}
</Droppable> </Droppable>

View File

@@ -0,0 +1,35 @@
import type { HTMLAttributes } from 'react'
import styled, { css } from 'styled-components'
type Props = {
maxLine?: number
} & HTMLAttributes<HTMLDivElement>
const Ellipsis = (props: Props) => {
const { maxLine = 1, children, ...rest } = props
return (
<EllipsisContainer $maxLine={maxLine} {...rest}>
{children}
</EllipsisContainer>
)
}
const multiLineEllipsis = css<{ $maxLine: number }>`
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: ${({ $maxLine }) => $maxLine};
overflow-wrap: break-word;
`
const singleLineEllipsis = css`
display: block;
white-space: nowrap;
`
const EllipsisContainer = styled.div<{ $maxLine: number }>`
overflow: hidden;
text-overflow: ellipsis;
${({ $maxLine }) => ($maxLine > 1 ? multiLineEllipsis : singleLineEllipsis)}
`
export default Ellipsis

View File

@@ -0,0 +1,134 @@
import { useEffect, useState } from 'react'
import styled from 'styled-components'
// FallbackFavicon component that tries multiple favicon sources
interface FallbackFaviconProps {
hostname: string
alt: string
}
const FallbackFavicon: React.FC<FallbackFaviconProps> = ({ hostname, alt }) => {
type FaviconState =
| { status: 'idle' }
| { status: 'loading' }
| { status: 'failed' }
| { status: 'loaded'; src: string }
const [faviconState, setFaviconState] = useState<FaviconState>({ status: 'idle' })
useEffect(() => {
// Reset state when hostname changes
setFaviconState({ status: 'loading' })
// Generate all possible favicon URLs
const faviconUrls = [
`https://favicon.splitbee.io/?url=${hostname}`,
`https://${hostname}/favicon.ico`,
`https://icon.horse/icon/${hostname}`,
`https://favicon.cccyun.cc/${hostname}`,
`https://favicon.im/${hostname}`,
`https://www.google.com/s2/favicons?domain=${hostname}`
]
// Main controller to abort all requests when needed
const controller = new AbortController()
const { signal } = controller
// Create a promise for each favicon URL
const faviconPromises = faviconUrls.map((url) =>
fetch(url, {
method: 'HEAD',
signal,
credentials: 'omit'
})
.then((response) => {
if (response.ok) {
return url
}
throw new Error(`Failed to fetch ${url}`)
})
.catch((error) => {
// Rethrow aborted errors but silence other failures
if (error.name === 'AbortError') {
throw error
}
console.debug(`Failed to fetch favicon from ${url}:`, error)
return null // Return null for failed requests
})
)
// Create a timeout promise
const timeoutPromise = new Promise<string>((resolve) => {
const timer = setTimeout(() => {
resolve(faviconUrls[0]) // Default to first URL after timeout
}, 2000)
// Clear timeout if signal is aborted
signal.addEventListener('abort', () => clearTimeout(timer))
})
// Use Promise.race to get the first successful result
Promise.race([
// Filter out failed requests (null results)
Promise.any(faviconPromises)
.then((result) => result || faviconUrls[0]) // Ensure we always have a string, not null
.catch(() => faviconUrls[0]),
timeoutPromise
])
.then((url) => {
setFaviconState({ status: 'loaded', src: url })
})
.catch((error) => {
console.debug('All favicon requests failed:', error)
setFaviconState({ status: 'loaded', src: faviconUrls[0] })
})
// Cleanup function
return () => {
controller.abort()
}
}, [hostname]) // Only depend on hostname
const handleError = () => {
setFaviconState({ status: 'failed' })
}
// Render based on current state
if (faviconState.status === 'failed') {
return <FaviconPlaceholder>{hostname.charAt(0).toUpperCase()}</FaviconPlaceholder>
}
if (faviconState.status === 'loaded') {
return <Favicon src={faviconState.src} alt={alt} onError={handleError} />
}
return <FaviconLoading />
}
const FaviconLoading = styled.div`
width: 16px;
height: 16px;
border-radius: 4px;
background-color: var(--color-background-mute);
`
const FaviconPlaceholder = styled.div`
width: 16px;
height: 16px;
border-radius: 4px;
background-color: var(--color-primary-1);
color: var(--color-primary-6);
font-size: 10px;
display: flex;
align-items: center;
justify-content: center;
font-weight: bold;
`
const Favicon = styled.img`
width: 16px;
height: 16px;
border-radius: 4px;
background-color: var(--color-background-mute);
`
export default FallbackFavicon

View File

@@ -24,6 +24,7 @@ const MinAppIcon: FC<Props> = ({ app, size = 48, style }) => {
width: `${size}px`, width: `${size}px`,
height: `${size}px`, height: `${size}px`,
backgroundColor: _app.background, backgroundColor: _app.background,
...app.style,
...style ...style
}} }}
/> />

View File

@@ -0,0 +1,17 @@
const UnWrapIcon = (props: React.SVGProps<SVGSVGElement>) => (
<svg
xmlns="http://www.w3.org/2000/svg"
width="1em"
height="1em"
fill="none"
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
className="unwrap_svg__lucide unwrap_svg__lucide-text unwrap_svg__size-4"
viewBox="0 0 24 24"
{...props}>
<path d="M17 6.1H3M21 12.1H3M15.1 18H3" />
</svg>
)
export default UnWrapIcon

View File

@@ -0,0 +1,20 @@
import React from 'react'
const WrapIcon = (props: React.SVGProps<SVGSVGElement>) => (
<svg
xmlns="http://www.w3.org/2000/svg"
width="1em"
height="1em"
fill="none"
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
className="wrap_svg__lucide wrap_svg__lucide-wrap-text wrap_svg__size-4"
viewBox="0 0 24 24"
{...props}>
<path d="M3 6h18M3 12h15a3 3 0 1 1 0 6h-4" />
<path d="m16 16-2 2 2 2M3 18h7" />
</svg>
)
export default WrapIcon

View File

@@ -0,0 +1,63 @@
import { StyleProvider } from '@ant-design/cssinjs'
import React, { useEffect, useRef } from 'react'
import { createPortal } from 'react-dom'
import { StyleSheetManager } from 'styled-components'
interface Props {
children: React.ReactNode
}
const ShadowDOMRenderer: React.FC<Props> = ({ children }) => {
const hostRef = useRef<HTMLDivElement>(null)
const [shadowRoot, setShadowRoot] = React.useState<ShadowRoot | null>(null)
useEffect(() => {
const host = hostRef.current
if (!host) return
// 创建 shadow root
const shadow = host.shadowRoot || host.attachShadow({ mode: 'open' })
// 获取原始样式表
const markdownStyleSheet = Array.from(document.styleSheets).find((sheet) => {
try {
return Array.from(sheet.cssRules).some((rule: CSSRule) => {
return rule.cssText?.includes('.markdown')
})
} catch {
return false
}
})
if (markdownStyleSheet) {
const style = document.createElement('style')
const cssRules = Array.from(markdownStyleSheet.cssRules)
.map((rule) => rule.cssText)
.join('\n')
style.textContent = cssRules
shadow.appendChild(style)
}
setShadowRoot(shadow)
}, [])
if (!shadowRoot) {
return <div ref={hostRef} />
}
return (
<div ref={hostRef}>
{createPortal(
<StyleSheetManager target={shadowRoot}>
<StyleProvider container={shadowRoot} layer>
{children}
</StyleProvider>
</StyleSheetManager>,
shadowRoot
)}
</div>
)
}
export default ShadowDOMRenderer

View File

@@ -1,6 +1,7 @@
/* eslint-disable react/no-unknown-property */ /* eslint-disable react/no-unknown-property */
import { CloseOutlined, ExportOutlined, PushpinOutlined, ReloadOutlined } from '@ant-design/icons' import { CloseOutlined, CodeOutlined, ExportOutlined, PushpinOutlined, ReloadOutlined } from '@ant-design/icons'
import { isMac, isWindows } from '@renderer/config/constant' import { isMac, isWindows } from '@renderer/config/constant'
import { AppLogo } from '@renderer/config/env'
import { DEFAULT_MIN_APPS } from '@renderer/config/minapps' 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 { useMinapps } from '@renderer/hooks/useMinapps'
@@ -41,7 +42,11 @@ const PopupContainer: React.FC<Props> = ({ app, resolve }) => {
} }
MinApp.onClose = onClose MinApp.onClose = onClose
const openDevTools = () => {
if (webviewRef.current) {
webviewRef.current.openDevTools()
}
}
const onReload = () => { const onReload = () => {
if (webviewRef.current) { if (webviewRef.current) {
webviewRef.current.src = app.url webviewRef.current.src = app.url
@@ -49,14 +54,17 @@ const PopupContainer: React.FC<Props> = ({ app, resolve }) => {
} }
const onOpenLink = () => { const onOpenLink = () => {
window.api.openWebsite(app.url) if (webviewRef.current) {
const currentUrl = webviewRef.current.getURL()
window.api.openWebsite(currentUrl)
}
} }
const onTogglePin = () => { const onTogglePin = () => {
const newPinned = isPinned ? pinned.filter((item) => item.id !== app.id) : [...pinned, app] const newPinned = isPinned ? pinned.filter((item) => item.id !== app.id) : [...pinned, app]
updatePinnedMinapps(newPinned) updatePinnedMinapps(newPinned)
} }
const isInDevelopment = process.env.NODE_ENV === 'development'
const Title = () => { const Title = () => {
return ( return (
<TitleContainer style={{ justifyContent: 'space-between' }}> <TitleContainer style={{ justifyContent: 'space-between' }}>
@@ -75,6 +83,11 @@ const PopupContainer: React.FC<Props> = ({ app, resolve }) => {
<ExportOutlined /> <ExportOutlined />
</Button> </Button>
)} )}
{isInDevelopment && (
<Button onClick={openDevTools}>
<CodeOutlined />
</Button>
)}
<Button onClick={() => onClose()}> <Button onClick={() => onClose()}>
<CloseOutlined /> <CloseOutlined />
</Button> </Button>
@@ -236,6 +249,10 @@ export default class MinApp {
await delay(0) await delay(0)
} }
if (!app.logo) {
app.logo = AppLogo
}
MinApp.app = app MinApp.app = app
store.dispatch(setMinappShow(true)) store.dispatch(setMinappShow(true))

View File

@@ -110,7 +110,7 @@ const PopupContainer: React.FC<Props> = ({ resolve }) => {
allowClear allowClear
autoFocus autoFocus
style={{ paddingLeft: 0 }} style={{ paddingLeft: 0 }}
bordered={false} variant="borderless"
size="middle" size="middle"
/> />
</HStack> </HStack>

View File

@@ -0,0 +1,100 @@
import { backup } from '@renderer/services/BackupService'
import { Modal, Progress } from 'antd'
import { useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { TopView } from '../TopView'
interface Props {
resolve: (data: any) => void
}
interface ProgressData {
stage: string
progress: number
total: number
}
const PopupContainer: React.FC<Props> = ({ resolve }) => {
const [open, setOpen] = useState(true)
const [progressData, setProgressData] = useState<ProgressData>()
const { t } = useTranslation()
useEffect(() => {
const removeListener = window.electron.ipcRenderer.on('backup-progress', (_, data: ProgressData) => {
setProgressData(data)
})
return () => {
removeListener()
}
}, [])
const onOk = async () => {
await backup()
setOpen(false)
}
const onCancel = () => {
setOpen(false)
}
const onClose = () => {
resolve({})
}
const getProgressText = () => {
if (!progressData) return ''
if (progressData.stage === 'copying_files') {
return t(`backup.progress.${progressData.stage}`, {
progress: Math.floor(progressData.progress)
})
}
return t(`backup.progress.${progressData.stage}`)
}
BackupPopup.hide = onCancel
return (
<Modal
title={t('backup.title')}
open={open}
onOk={onOk}
onCancel={onCancel}
afterClose={onClose}
transitionName="ant-move-down"
okText={t('backup.confirm.button')}
centered>
{!progressData && <div>{t('backup.content')}</div>}
{progressData && (
<div style={{ textAlign: 'center', padding: '20px 0' }}>
<Progress percent={Math.floor(progressData.progress)} strokeColor="var(--color-primary)" />
<div style={{ marginTop: 16 }}>{getProgressText()}</div>
</div>
)}
</Modal>
)
}
const TopViewKey = 'BackupPopup'
export default class BackupPopup {
static topviewId = 0
static hide() {
TopView.hide(TopViewKey)
}
static show() {
return new Promise<any>((resolve) => {
TopView.show(
<PopupContainer
resolve={(v) => {
resolve(v)
TopView.hide(TopViewKey)
}}
/>,
TopViewKey
)
})
}
}

View File

@@ -4,7 +4,7 @@ import App from '@renderer/pages/apps/App'
import { Popover } from 'antd' import { Popover } from 'antd'
import { Empty } from 'antd' import { Empty } from 'antd'
import { isEmpty } from 'lodash' import { isEmpty } from 'lodash'
import { FC, useState } from 'react' import { FC, useEffect, useState } from 'react'
import { useHotkeys } from 'react-hotkeys-hook' import { useHotkeys } from 'react-hotkeys-hook'
import styled from 'styled-components' import styled from 'styled-components'
@@ -26,8 +26,22 @@ const MinAppsPopover: FC<Props> = ({ children }) => {
setOpen(false) setOpen(false)
} }
const [maxHeight, setMaxHeight] = useState(window.innerHeight - 100)
useEffect(() => {
const handleResize = () => {
setMaxHeight(window.innerHeight - 100)
}
window.addEventListener('resize', handleResize)
return () => {
window.removeEventListener('resize', handleResize)
}
}, [])
const content = ( const content = (
<PopoverContent> <PopoverContent maxHeight={maxHeight}>
<AppsContainer> <AppsContainer>
{minapps.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} />
@@ -54,11 +68,14 @@ const MinAppsPopover: FC<Props> = ({ children }) => {
) )
} }
const PopoverContent = styled(Scrollbar)`` const PopoverContent = styled(Scrollbar)<{ maxHeight: number }>`
max-height: ${(props) => props.maxHeight}px;
overflow-y: auto;
`
const AppsContainer = styled.div` const AppsContainer = styled.div`
display: grid; display: grid;
grid-template-columns: repeat(6, minmax(90px, 1fr)); grid-template-columns: repeat(8, minmax(90px, 1fr));
gap: 18px; gap: 18px;
` `

View File

@@ -1,6 +1,6 @@
import { Input, Modal } from 'antd' import { Input, Modal } from 'antd'
import { TextAreaProps } from 'antd/es/input' import { TextAreaProps } from 'antd/es/input'
import { useState } from 'react' import { useRef, useState } from 'react'
import { Box } from '../Layout' import { Box } from '../Layout'
import { TopView } from '../TopView' import { TopView } from '../TopView'
@@ -27,6 +27,7 @@ const PromptPopupContainer: React.FC<Props> = ({
}) => { }) => {
const [value, setValue] = useState(defaultValue) const [value, setValue] = useState(defaultValue)
const [open, setOpen] = useState(true) const [open, setOpen] = useState(true)
const textAreaRef = useRef<any>(null)
const onOk = () => { const onOk = () => {
setOpen(false) setOpen(false)
@@ -41,17 +42,35 @@ const PromptPopupContainer: React.FC<Props> = ({
resolve(null) resolve(null)
} }
const handleAfterOpenChange = (visible: boolean) => {
if (visible) {
const textArea = textAreaRef.current?.resizableTextArea?.textArea
if (textArea) {
textArea.focus()
const length = textArea.value.length
textArea.setSelectionRange(length, length)
}
}
}
PromptPopup.hide = onCancel PromptPopup.hide = onCancel
return ( return (
<Modal title={title} open={open} onOk={onOk} onCancel={onCancel} afterClose={onClose} centered> <Modal
title={title}
open={open}
onOk={onOk}
onCancel={onCancel}
afterClose={onClose}
afterOpenChange={handleAfterOpenChange}
centered>
<Box mb={8}>{message}</Box> <Box mb={8}>{message}</Box>
<Input.TextArea <Input.TextArea
ref={textAreaRef}
placeholder={inputPlaceholder} placeholder={inputPlaceholder}
value={value} value={value}
onChange={(e) => setValue(e.target.value)} onChange={(e) => setValue(e.target.value)}
allowClear allowClear
autoFocus
onPressEnter={onOk} onPressEnter={onOk}
rows={1} rows={1}
{...inputProps} {...inputProps}

View File

@@ -0,0 +1,100 @@
import { restore } from '@renderer/services/BackupService'
import { Modal, Progress } from 'antd'
import { useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { TopView } from '../TopView'
interface Props {
resolve: (data: any) => void
}
interface ProgressData {
stage: string
progress: number
total: number
}
const PopupContainer: React.FC<Props> = ({ resolve }) => {
const [open, setOpen] = useState(true)
const [progressData, setProgressData] = useState<ProgressData>()
const { t } = useTranslation()
useEffect(() => {
const removeListener = window.electron.ipcRenderer.on('restore-progress', (_, data: ProgressData) => {
setProgressData(data)
})
return () => {
removeListener()
}
}, [])
const onOk = async () => {
await restore()
setOpen(false)
}
const onCancel = () => {
setOpen(false)
}
const onClose = () => {
resolve({})
}
const getProgressText = () => {
if (!progressData) return ''
if (progressData.stage === 'copying_files') {
return t(`restore.progress.${progressData.stage}`, {
progress: Math.floor(progressData.progress)
})
}
return t(`restore.progress.${progressData.stage}`)
}
RestorePopup.hide = onCancel
return (
<Modal
title={t('restore.title')}
open={open}
onOk={onOk}
onCancel={onCancel}
afterClose={onClose}
transitionName="ant-move-down"
okText={t('restore.confirm.button')}
centered>
{!progressData && <div>{t('restore.content')}</div>}
{progressData && (
<div style={{ textAlign: 'center', padding: '20px 0' }}>
<Progress percent={Math.floor(progressData.progress)} strokeColor="var(--color-primary)" />
<div style={{ marginTop: 16 }}>{getProgressText()}</div>
</div>
)}
</Modal>
)
}
const TopViewKey = 'RestorePopup'
export default class RestorePopup {
static topviewId = 0
static hide() {
TopView.hide(TopViewKey)
}
static show() {
return new Promise<any>((resolve) => {
TopView.show(
<PopupContainer
resolve={(v) => {
resolve(v)
TopView.hide(TopViewKey)
}}
/>,
TopViewKey
)
})
}
}

View File

@@ -7,7 +7,7 @@ import { getModelUniqId } from '@renderer/services/ModelService'
import { Model } from '@renderer/types' import { Model } from '@renderer/types'
import { Avatar, Divider, Empty, Input, InputRef, Menu, MenuProps, Modal } from 'antd' import { Avatar, Divider, Empty, Input, InputRef, Menu, MenuProps, Modal } from 'antd'
import { first, sortBy } from 'lodash' import { first, sortBy } from 'lodash'
import { useEffect, useRef, useState } from 'react' import { useCallback, 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'
@@ -33,6 +33,7 @@ const PopupContainer: React.FC<PopupContainerProps> = ({ model, resolve }) => {
const { providers } = useProviders() const { providers } = useProviders()
const [pinnedModels, setPinnedModels] = useState<string[]>([]) const [pinnedModels, setPinnedModels] = useState<string[]>([])
const scrollContainerRef = useRef<HTMLDivElement>(null) const scrollContainerRef = useRef<HTMLDivElement>(null)
const [keyboardSelectedId, setKeyboardSelectedId] = useState<string>('')
useEffect(() => { useEffect(() => {
const loadPinnedModels = async () => { const loadPinnedModels = async () => {
@@ -62,15 +63,33 @@ const PopupContainer: React.FC<PopupContainerProps> = ({ model, resolve }) => {
setPinnedModels(sortBy(newPinnedModels, ['group', 'name'])) setPinnedModels(sortBy(newPinnedModels, ['group', 'name']))
} }
// 根据输入的文本筛选模型
const getFilteredModels = useCallback(
(provider) => {
const nonEmbeddingModels = provider.models.filter((m) => !isEmbeddingModel(m))
if (!searchText.trim()) {
return sortBy(nonEmbeddingModels, ['group', 'name'])
}
const keywords = searchText.toLowerCase().split(/\s+/).filter(Boolean)
return sortBy(nonEmbeddingModels, ['group', 'name']).filter((m) => {
const fullName = provider.isSystem
? `${m.name}${m.provider}${t('provider.' + provider.id)}`
: `${m.name}${m.provider}`
const lowerFullName = fullName.toLowerCase()
return keywords.every((keyword) => lowerFullName.includes(keyword))
})
},
[searchText, t]
)
const filteredItems: MenuItem[] = providers const filteredItems: MenuItem[] = providers
.filter((p) => p.models && p.models.length > 0) .filter((p) => p.models && p.models.length > 0)
.map((p) => { .map((p) => {
const filteredModels = sortBy(p.models, ['group', 'name']) const filteredModels = getFilteredModels(p).map((m) => ({
.filter((m) => !isEmbeddingModel(m))
.filter((m) =>
[m.name + m.provider + t('provider.' + p.id)].join('').toLowerCase().includes(searchText.toLowerCase())
)
.map((m) => ({
key: getModelUniqId(m), key: getModelUniqId(m),
label: ( label: (
<ModelItem> <ModelItem>
@@ -153,10 +172,12 @@ const PopupContainer: React.FC<PopupContainerProps> = ({ model, resolve }) => {
} }
const onCancel = () => { const onCancel = () => {
setKeyboardSelectedId('')
setOpen(false) setOpen(false)
} }
const onClose = async () => { const onClose = async () => {
setKeyboardSelectedId('')
resolve(undefined) resolve(undefined)
SelectModelPopup.hide() SelectModelPopup.hide()
} }
@@ -176,6 +197,85 @@ const PopupContainer: React.FC<PopupContainerProps> = ({ model, resolve }) => {
} }
}, [open, model]) }, [open, model])
// 获取所有可见的模型项
const getVisibleModelItems = useCallback(() => {
const items: { key: string; model: Model }[] = []
// 如果有置顶模型且没有搜索文本,添加置顶模型
if (pinnedModels.length > 0 && searchText.length === 0) {
providers
.flatMap((p) => p.models || [])
.filter((m) => pinnedModels.includes(getModelUniqId(m)))
.forEach((m) => items.push({ key: getModelUniqId(m) + '_pinned', model: m }))
}
// 添加其他过滤后的模型
providers.forEach((p) => {
if (p.models) {
getFilteredModels(p).forEach((m) => {
const modelId = getModelUniqId(m)
const isPinned = pinnedModels.includes(modelId)
// 如果是搜索状态,或者不是固定模型,才添加到列表中
if (searchText.length > 0 || !isPinned) {
items.push({
key: isPinned ? modelId + '_pinned' : modelId,
model: m
})
}
})
}
})
return items
}, [pinnedModels, searchText, providers, getFilteredModels])
// 处理键盘导航
const handleKeyDown = useCallback(
(e: KeyboardEvent) => {
const items = getVisibleModelItems()
if (items.length === 0) return
if (e.key === 'ArrowDown' || e.key === 'ArrowUp') {
e.preventDefault()
const currentIndex = items.findIndex((item) => item.key === keyboardSelectedId)
let nextIndex
if (currentIndex === -1) {
nextIndex = e.key === 'ArrowDown' ? 0 : items.length - 1
} else {
nextIndex =
e.key === 'ArrowDown' ? (currentIndex + 1) % items.length : (currentIndex - 1 + items.length) % items.length
}
const nextItem = items[nextIndex]
setKeyboardSelectedId(nextItem.key)
const element = document.querySelector(`[data-menu-id="${nextItem.key}"]`)
element?.scrollIntoView({ block: 'nearest', behavior: 'smooth' })
} else if (e.key === 'Enter') {
e.preventDefault() // 阻止回车的默认行为
if (keyboardSelectedId) {
const selectedItem = items.find((item) => item.key === keyboardSelectedId)
if (selectedItem) {
resolve(selectedItem.model)
setOpen(false)
}
}
}
},
[keyboardSelectedId, getVisibleModelItems, resolve, setOpen]
)
useEffect(() => {
window.addEventListener('keydown', handleKeyDown)
return () => window.removeEventListener('keydown', handleKeyDown)
}, [handleKeyDown])
// 搜索文本改变时重置键盘选中状态
useEffect(() => {
setKeyboardSelectedId('')
}, [searchText])
return ( return (
<Modal <Modal
centered centered
@@ -208,20 +308,21 @@ const PopupContainer: React.FC<PopupContainerProps> = ({ model, resolve }) => {
allowClear allowClear
autoFocus autoFocus
style={{ paddingLeft: 0 }} style={{ paddingLeft: 0 }}
bordered={false} variant="borderless"
size="middle" size="middle"
onKeyDown={(e) => {
// 防止上下键移动光标
if (e.key === 'ArrowUp' || e.key === 'ArrowDown') {
e.preventDefault()
}
}}
/> />
</HStack> </HStack>
<Divider style={{ margin: 0, borderBlockStartWidth: 0.5 }} /> <Divider style={{ margin: 0, borderBlockStartWidth: 0.5 }} />
<Scrollbar style={{ height: '50vh' }} ref={scrollContainerRef}> <Scrollbar style={{ height: '50vh' }} ref={scrollContainerRef}>
<Container> <Container>
{filteredItems.length > 0 ? ( {filteredItems.length > 0 ? (
<StyledMenu <StyledMenu items={filteredItems} selectedKeys={[keyboardSelectedId]} mode="inline" inlineIndent={6} />
items={filteredItems}
selectedKeys={model ? [getModelUniqId(model)] : []}
mode="inline"
inlineIndent={6}
/>
) : ( ) : (
<EmptyState> <EmptyState>
<Empty image={Empty.PRESENTED_IMAGE_SIMPLE} /> <Empty image={Empty.PRESENTED_IMAGE_SIMPLE} />

View File

@@ -51,6 +51,17 @@ const PopupContainer: React.FC<Props> = ({ text, textareaProps, modalProps, reso
setTimeout(resizeTextArea, 0) setTimeout(resizeTextArea, 0)
}, []) }, [])
const handleAfterOpenChange = (visible: boolean) => {
if (visible) {
const textArea = textareaRef.current?.resizableTextArea?.textArea
if (textArea) {
textArea.focus()
const length = textArea.value.length
textArea.setSelectionRange(length, length)
}
}
}
TextEditPopup.hide = onCancel TextEditPopup.hide = onCancel
return ( return (
@@ -65,6 +76,7 @@ const PopupContainer: React.FC<Props> = ({ text, textareaProps, modalProps, reso
onOk={onOk} onOk={onOk}
onCancel={onCancel} onCancel={onCancel}
afterClose={onClose} afterClose={onClose}
afterOpenChange={handleAfterOpenChange}
centered> centered>
<TextArea <TextArea
ref={textareaRef} ref={textareaRef}

View File

@@ -1,16 +1,18 @@
import DefaultAvatar from '@renderer/assets/images/avatar.png'
import useAvatar from '@renderer/hooks/useAvatar' import useAvatar from '@renderer/hooks/useAvatar'
import { useSettings } from '@renderer/hooks/useSettings' import { useSettings } from '@renderer/hooks/useSettings'
import ImageStorage from '@renderer/services/ImageStorage' import ImageStorage from '@renderer/services/ImageStorage'
import { useAppDispatch } from '@renderer/store' import { useAppDispatch } from '@renderer/store'
import { setAvatar } from '@renderer/store/runtime' import { setAvatar } from '@renderer/store/runtime'
import { setUserName } from '@renderer/store/settings' import { setUserName } from '@renderer/store/settings'
import { compressImage } from '@renderer/utils' import { compressImage, isEmoji } from '@renderer/utils'
import { Avatar, Input, Modal, Upload } from 'antd' import { Avatar, Dropdown, Input, Modal, Popover, Upload } from 'antd'
import { useState } from 'react' import { useState } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import styled from 'styled-components' import styled from 'styled-components'
import { Center, HStack } from '../Layout' import EmojiPicker from '../EmojiPicker'
import { Center, HStack, VStack } from '../Layout'
import { TopView } from '../TopView' import { TopView } from '../TopView'
interface Props { interface Props {
@@ -19,6 +21,8 @@ interface Props {
const PopupContainer: React.FC<Props> = ({ resolve }) => { const PopupContainer: React.FC<Props> = ({ resolve }) => {
const [open, setOpen] = useState(true) const [open, setOpen] = useState(true)
const [emojiPickerOpen, setEmojiPickerOpen] = useState(false)
const [dropdownOpen, setDropdownOpen] = useState(false)
const { t } = useTranslation() const { t } = useTranslation()
const { userName } = useSettings() const { userName } = useSettings()
const dispatch = useAppDispatch() const dispatch = useAppDispatch()
@@ -36,6 +40,85 @@ const PopupContainer: React.FC<Props> = ({ resolve }) => {
resolve({}) resolve({})
} }
const handleEmojiClick = async (emoji: string) => {
try {
// set emoji string
await ImageStorage.set('avatar', emoji)
// update avatar display
dispatch(setAvatar(emoji))
setEmojiPickerOpen(false)
} catch (error: any) {
window.message.error(error.message)
}
}
const handleReset = async () => {
try {
await ImageStorage.set('avatar', DefaultAvatar)
dispatch(setAvatar(DefaultAvatar))
setDropdownOpen(false)
} catch (error: any) {
window.message.error(error.message)
}
}
const items = [
{
key: 'upload',
label: (
<div style={{ width: '100%', textAlign: 'center' }}>
<Upload
customRequest={() => {}}
accept="image/png, image/jpeg, image/gif"
itemRender={() => null}
maxCount={1}
onChange={async ({ file }) => {
try {
const _file = file.originFileObj as File
if (_file.type === 'image/gif') {
await ImageStorage.set('avatar', _file)
} else {
const compressedFile = await compressImage(_file)
await ImageStorage.set('avatar', compressedFile)
}
dispatch(setAvatar(await ImageStorage.get('avatar')))
setDropdownOpen(false)
} catch (error: any) {
window.message.error(error.message)
}
}}>
{t('settings.general.image_upload')}
</Upload>
</div>
)
},
{
key: 'emoji',
label: (
<div
style={{ width: '100%', textAlign: 'center' }}
onClick={(e) => {
e.stopPropagation()
setEmojiPickerOpen(true)
setDropdownOpen(false)
}}>
{t('settings.general.emoji_picker')}
</div>
)
},
{
key: 'reset',
label: (
<div
style={{ width: '100%', textAlign: 'center' }}
onClick={(e) => {
e.stopPropagation()
handleReset()
}}>
{t('settings.general.avatar.reset')}
</div>
)
}
]
return ( return (
<Modal <Modal
width="300px" width="300px"
@@ -47,29 +130,40 @@ const PopupContainer: React.FC<Props> = ({ resolve }) => {
transitionName="ant-move-down" transitionName="ant-move-down"
centered> centered>
<Center mt="30px"> <Center mt="30px">
<Upload <VStack alignItems="center" gap="10px">
customRequest={() => {}} <Dropdown
accept="image/png, image/jpeg" menu={{ items }}
itemRender={() => null} trigger={['click']}
maxCount={1} open={dropdownOpen}
onChange={async ({ file }) => { align={{ offset: [0, 4] }}
try { placement="bottom"
const _file = file.originFileObj as File onOpenChange={(visible) => {
const compressedFile = await compressImage(_file) setDropdownOpen(visible)
await ImageStorage.set('avatar', compressedFile) if (visible) {
dispatch(setAvatar(await ImageStorage.get('avatar'))) setEmojiPickerOpen(false)
} catch (error: any) {
window.message.error(error.message)
} }
}}> }}>
<UserAvatar src={avatar} /> <Popover
</Upload> content={<EmojiPicker onEmojiClick={handleEmojiClick} />}
trigger="click"
open={emojiPickerOpen}
onOpenChange={(visible) => {
setEmojiPickerOpen(visible)
if (visible) {
setDropdownOpen(false)
}
}}
placement="bottom">
{isEmoji(avatar) ? <EmojiAvatar>{avatar}</EmojiAvatar> : <UserAvatar src={avatar} />}
</Popover>
</Dropdown>
</VStack>
</Center> </Center>
<HStack alignItems="center" gap="10px" p="20px"> <HStack alignItems="center" gap="10px" p="20px">
<Input <Input
placeholder={t('settings.general.user_name.placeholder')} placeholder={t('settings.general.user_name.placeholder')}
value={userName} value={userName}
onChange={(e) => dispatch(setUserName(e.target.value))} onChange={(e) => dispatch(setUserName(e.target.value.trim()))}
style={{ flex: 1, textAlign: 'center', width: '100%' }} style={{ flex: 1, textAlign: 'center', width: '100%' }}
maxLength={30} maxLength={30}
/> />
@@ -88,6 +182,23 @@ const UserAvatar = styled(Avatar)`
} }
` `
const EmojiAvatar = styled.div`
cursor: pointer;
width: 80px;
height: 80px;
border-radius: 20%;
background-color: var(--color-background-soft);
display: flex;
align-items: center;
justify-content: center;
font-size: 40px;
transition: opacity 0.3s ease;
border: 0.5px solid var(--color-border);
&:hover {
opacity: 0.8;
}
`
export default class UserPopup { export default class UserPopup {
static topviewId = 0 static topviewId = 0
static hide() { static hide() {

View File

@@ -12,6 +12,7 @@ import useAvatar from '@renderer/hooks/useAvatar'
import { useMinapps } from '@renderer/hooks/useMinapps' import { useMinapps } from '@renderer/hooks/useMinapps'
import { modelGenerating, useRuntime } from '@renderer/hooks/useRuntime' import { modelGenerating, useRuntime } from '@renderer/hooks/useRuntime'
import { useSettings } from '@renderer/hooks/useSettings' import { useSettings } from '@renderer/hooks/useSettings'
import { isEmoji } from '@renderer/utils'
import type { MenuProps } from 'antd' import type { MenuProps } from 'antd'
import { Tooltip } from 'antd' import { Tooltip } from 'antd'
import { Avatar } from 'antd' import { Avatar } from 'antd'
@@ -50,6 +51,7 @@ const Sidebar: FC = () => {
const onOpenDocs = () => { const onOpenDocs = () => {
MinApp.start({ MinApp.start({
id: 'docs',
name: t('docs.title'), name: t('docs.title'),
url: 'https://docs.cherry-ai.com/', url: 'https://docs.cherry-ai.com/',
logo: AppLogo logo: AppLogo
@@ -63,7 +65,11 @@ const Sidebar: FC = () => {
backgroundColor: sidebarBgColor, backgroundColor: sidebarBgColor,
zIndex: minappShow ? 10000 : 'initial' zIndex: minappShow ? 10000 : 'initial'
}}> }}>
{isEmoji(avatar) ? (
<EmojiAvatar onClick={onEditUser}>{avatar}</EmojiAvatar>
) : (
<AvatarImg src={avatar || UserAvatar} draggable={false} className="nodrag" onClick={onEditUser} /> <AvatarImg src={avatar || UserAvatar} draggable={false} className="nodrag" onClick={onEditUser} />
)}
<MainMenusContainer> <MainMenusContainer>
<Menus onClick={MinApp.onClose}> <Menus onClick={MinApp.onClose}>
<MainMenus /> <MainMenus />
@@ -77,9 +83,11 @@ const Sidebar: FC = () => {
</AppsContainer> </AppsContainer>
)} )}
</MainMenusContainer> </MainMenusContainer>
<Menus onClick={MinApp.onClose}> <Menus>
<Tooltip title={t('docs.title')} mouseEnterDelay={0.8} placement="right"> <Tooltip title={t('docs.title')} mouseEnterDelay={0.8} placement="right">
<Icon onClick={onOpenDocs}> <Icon
onClick={onOpenDocs}
className={minappShow && MinApp.app?.url === 'https://docs.cherry-ai.com/' ? 'active' : ''}>
<QuestionCircleOutlined /> <QuestionCircleOutlined />
</Icon> </Icon>
</Tooltip> </Tooltip>
@@ -93,8 +101,14 @@ const Sidebar: FC = () => {
</Icon> </Icon>
</Tooltip> </Tooltip>
<Tooltip title={t('settings.title')} mouseEnterDelay={0.8} placement="right"> <Tooltip title={t('settings.title')} mouseEnterDelay={0.8} placement="right">
<StyledLink onClick={() => to(isLocalAi ? '/settings/assistant' : '/settings/provider')}> <StyledLink
<Icon className={pathname.startsWith('/settings') ? 'active' : ''}> onClick={async () => {
if (minappShow) {
await MinApp.close()
}
await to(isLocalAi ? '/settings/assistant' : '/settings/provider')
}}>
<Icon className={pathname.startsWith('/settings') && !minappShow ? 'active' : ''}>
<i className="iconfont icon-setting" /> <i className="iconfont icon-setting" />
</Icon> </Icon>
</StyledLink> </StyledLink>
@@ -108,10 +122,11 @@ const MainMenus: FC = () => {
const { t } = useTranslation() const { t } = useTranslation()
const { pathname } = useLocation() const { pathname } = useLocation()
const { sidebarIcons } = useSettings() const { sidebarIcons } = useSettings()
const { minappShow } = useRuntime()
const navigate = useNavigate() const navigate = useNavigate()
const isRoute = (path: string): string => (pathname === path ? 'active' : '') const isRoute = (path: string): string => (pathname === path && !minappShow ? 'active' : '')
const isRoutes = (path: string): string => (pathname.startsWith(path) ? 'active' : '') const isRoutes = (path: string): string => (pathname.startsWith(path) && !minappShow ? 'active' : '')
const iconMap = { const iconMap = {
assistants: <i className="iconfont icon-chat" />, assistants: <i className="iconfont icon-chat" />,
@@ -139,7 +154,13 @@ const MainMenus: FC = () => {
return ( return (
<Tooltip key={icon} title={t(`${icon}.title`)} mouseEnterDelay={0.8} placement="right"> <Tooltip key={icon} title={t(`${icon}.title`)} mouseEnterDelay={0.8} placement="right">
<StyledLink onClick={() => navigate(path)}> <StyledLink
onClick={async () => {
if (minappShow) {
await MinApp.close()
}
navigate(path)
}}>
<Icon className={isActive}>{iconMap[icon]}</Icon> <Icon className={isActive}>{iconMap[icon]}</Icon>
</StyledLink> </StyledLink>
</Tooltip> </Tooltip>
@@ -150,6 +171,7 @@ const MainMenus: FC = () => {
const PinnedApps: FC = () => { const PinnedApps: FC = () => {
const { pinned, updatePinnedMinapps } = useMinapps() const { pinned, updatePinnedMinapps } = useMinapps()
const { t } = useTranslation() const { t } = useTranslation()
const { minappShow } = useRuntime()
return ( return (
<DragableList list={pinned} onUpdate={updatePinnedMinapps} listStyle={{ marginBottom: 5 }}> <DragableList list={pinned} onUpdate={updatePinnedMinapps} listStyle={{ marginBottom: 5 }}>
@@ -164,11 +186,12 @@ const PinnedApps: FC = () => {
} }
} }
] ]
const isActive = minappShow && MinApp.app?.id === app.id
return ( return (
<Tooltip key={app.id} title={app.name} mouseEnterDelay={0.8} placement="right"> <Tooltip key={app.id} title={app.name} mouseEnterDelay={0.8} placement="right">
<StyledLink> <StyledLink>
<Dropdown menu={{ items: menuItems }} trigger={['contextMenu']}> <Dropdown menu={{ items: menuItems }} trigger={['contextMenu']}>
<Icon onClick={() => MinApp.start(app)}> <Icon onClick={() => MinApp.start(app)} className={isActive ? 'active' : ''}>
<MinAppIcon size={20} app={app} style={{ borderRadius: 6 }} /> <MinAppIcon size={20} app={app} style={{ borderRadius: 6 }} />
</Icon> </Icon>
</Dropdown> </Dropdown>
@@ -202,6 +225,24 @@ const AvatarImg = styled(Avatar)`
border: none; border: none;
cursor: pointer; cursor: pointer;
` `
const EmojiAvatar = styled.div`
width: 31px;
height: 31px;
background-color: var(--color-background-soft);
margin-bottom: ${isMac ? '12px' : '12px'};
margin-top: ${isMac ? '0px' : '2px'};
border-radius: 20%;
display: flex;
align-items: center;
justify-content: center;
font-size: 16px;
cursor: pointer;
-webkit-app-region: none;
border: 0.5px solid var(--color-border);
font-size: 20px;
`
const MainMenusContainer = styled.div` const MainMenusContainer = styled.div`
display: flex; display: flex;
flex: 1; flex: 1;

View File

@@ -2,6 +2,7 @@ export const DEFAULT_TEMPERATURE = 1.0
export const DEFAULT_CONTEXTCOUNT = 5 export const DEFAULT_CONTEXTCOUNT = 5
export const DEFAULT_MAX_TOKENS = 4096 export const DEFAULT_MAX_TOKENS = 4096
export const DEFAULT_KNOWLEDGE_DOCUMENT_COUNT = 6 export const DEFAULT_KNOWLEDGE_DOCUMENT_COUNT = 6
export const DEFAULT_KNOWLEDGE_THRESHOLD = 0.0
export const FONT_FAMILY = export const FONT_FAMILY =
"Ubuntu, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif" "Ubuntu, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif"

View File

@@ -226,6 +226,22 @@ export const EMBEDDING_MODELS = [
{ {
id: 'text-embedding-004', id: 'text-embedding-004',
max_context: 2048 max_context: 2048
},
{
id: 'deepset-mxbai-embed-de-large-v1',
max_context: 512
},
{
id: 'mxbai-embed-large-v1',
max_context: 512
},
{
id: 'mxbai-embed-2d-large-v1',
max_context: 512
},
{
id: 'mistral-embed',
max_context: 8000
} }
] ]

View File

@@ -1,9 +1,14 @@
import ThreeMinTopAppLogo from '@renderer/assets/images/apps/3mintop.png?url' import ThreeMinTopAppLogo from '@renderer/assets/images/apps/3mintop.png?url'
import AbacusLogo from '@renderer/assets/images/apps/abacus.webp?url'
import AIStudioLogo from '@renderer/assets/images/apps/aistudio.svg?url' import AIStudioLogo from '@renderer/assets/images/apps/aistudio.svg?url'
import BaiduAiAppLogo from '@renderer/assets/images/apps/baidu-ai.png?url' import BaiduAiAppLogo from '@renderer/assets/images/apps/baidu-ai.png?url'
import BaiduAiSearchLogo from '@renderer/assets/images/apps/baidu-ai-search.webp?url'
import BaicuanAppLogo from '@renderer/assets/images/apps/baixiaoying.webp?url' import BaicuanAppLogo from '@renderer/assets/images/apps/baixiaoying.webp?url'
import BoltAppLogo from '@renderer/assets/images/apps/bolt.svg?url' import BoltAppLogo from '@renderer/assets/images/apps/bolt.svg?url'
import CiciAppLogo from '@renderer/assets/images/apps/cici.webp?url'
import CozeAppLogo from '@renderer/assets/images/apps/coze.webp?url'
import DevvAppLogo from '@renderer/assets/images/apps/devv.png?url' import DevvAppLogo from '@renderer/assets/images/apps/devv.png?url'
import DifyAppLogo from '@renderer/assets/images/apps/dify.svg?url'
import DoubaoAppLogo from '@renderer/assets/images/apps/doubao.png?url' import DoubaoAppLogo from '@renderer/assets/images/apps/doubao.png?url'
import DuckDuckGoAppLogo from '@renderer/assets/images/apps/duckduckgo.webp?url' import DuckDuckGoAppLogo from '@renderer/assets/images/apps/duckduckgo.webp?url'
import FeloAppLogo from '@renderer/assets/images/apps/felo.png?url' import FeloAppLogo from '@renderer/assets/images/apps/felo.png?url'
@@ -14,19 +19,27 @@ import GithubCopilotLogo from '@renderer/assets/images/apps/github-copilot.webp?
import GrokAppLogo from '@renderer/assets/images/apps/grok.png?url' import GrokAppLogo from '@renderer/assets/images/apps/grok.png?url'
import HikaLogo from '@renderer/assets/images/apps/hika.webp?url' import HikaLogo from '@renderer/assets/images/apps/hika.webp?url'
import HuggingChatLogo from '@renderer/assets/images/apps/huggingchat.svg?url' import HuggingChatLogo from '@renderer/assets/images/apps/huggingchat.svg?url'
import KimiAppLogo from '@renderer/assets/images/apps/kimi.jpg?url' import KimiAppLogo from '@renderer/assets/images/apps/kimi.webp?url'
import LambdaChatLogo from '@renderer/assets/images/apps/lambdachat.webp?url'
import LeChatLogo from '@renderer/assets/images/apps/lechat.png?url'
import MetasoAppLogo from '@renderer/assets/images/apps/metaso.webp?url' import MetasoAppLogo from '@renderer/assets/images/apps/metaso.webp?url'
import NamiAiSearchLogo from '@renderer/assets/images/apps/nm.webp?url' import MonicaLogo from '@renderer/assets/images/apps/monica.webp?url'
import NamiAiLogo from '@renderer/assets/images/apps/nm.png?url'
import NamiAiSearchLogo from '@renderer/assets/images/apps/nm-search.webp?url'
import NotebookLMAppLogo from '@renderer/assets/images/apps/notebooklm.svg?url'
import PerplexityAppLogo from '@renderer/assets/images/apps/perplexity.webp?url' import PerplexityAppLogo from '@renderer/assets/images/apps/perplexity.webp?url'
import PoeAppLogo from '@renderer/assets/images/apps/poe.webp?url' import PoeAppLogo from '@renderer/assets/images/apps/poe.webp?url'
import ZhipuProviderLogo from '@renderer/assets/images/apps/qingyan.png?url' import ZhipuProviderLogo from '@renderer/assets/images/apps/qingyan.png?url'
import QwenlmAppLogo from '@renderer/assets/images/apps/qwenlm.webp?url' import QwenlmAppLogo from '@renderer/assets/images/apps/qwenlm.webp?url'
import SensetimeAppLogo from '@renderer/assets/images/apps/sensetime.png?url' import SensetimeAppLogo from '@renderer/assets/images/apps/sensetime.png?url'
import SparkDeskAppLogo from '@renderer/assets/images/apps/sparkdesk.png?url' import SparkDeskAppLogo from '@renderer/assets/images/apps/sparkdesk.webp?url'
import ThinkAnyLogo from '@renderer/assets/images/apps/thinkany.webp?url' import ThinkAnyLogo from '@renderer/assets/images/apps/thinkany.webp?url'
import TiangongAiLogo from '@renderer/assets/images/apps/tiangong.png?url' import TiangongAiLogo from '@renderer/assets/images/apps/tiangong.png?url'
import WanZhiAppLogo from '@renderer/assets/images/apps/wanzhi.jpg?url' import WanZhiAppLogo from '@renderer/assets/images/apps/wanzhi.jpg?url'
import TencentYuanbaoAppLogo from '@renderer/assets/images/apps/yuanbao.png?url' import WPSLingXiLogo from '@renderer/assets/images/apps/wpslingxi.webp?url'
import XiaoYiAppLogo from '@renderer/assets/images/apps/xiaoyi.webp?url'
import YouLogo from '@renderer/assets/images/apps/you.jpg?url'
import TencentYuanbaoAppLogo from '@renderer/assets/images/apps/yuanbao.webp?url'
import YuewenAppLogo from '@renderer/assets/images/apps/yuewen.png?url' import YuewenAppLogo from '@renderer/assets/images/apps/yuewen.png?url'
import ZhihuAppLogo from '@renderer/assets/images/apps/zhihu.png?url' import ZhihuAppLogo from '@renderer/assets/images/apps/zhihu.png?url'
import ClaudeAppLogo from '@renderer/assets/images/models/claude.png?url' import ClaudeAppLogo from '@renderer/assets/images/models/claude.png?url'
@@ -109,6 +122,12 @@ export const DEFAULT_MIN_APPS: MinAppType[] = [
url: 'https://www.doubao.com/chat/', url: 'https://www.doubao.com/chat/',
logo: DoubaoAppLogo logo: DoubaoAppLogo
}, },
{
id: 'cici',
name: 'Cici',
url: 'https://www.cici.com/chat/',
logo: CiciAppLogo
},
{ {
id: 'minimax', id: 'minimax',
name: '海螺', name: '海螺',
@@ -133,6 +152,16 @@ export const DEFAULT_MIN_APPS: MinAppType[] = [
logo: BaiduAiAppLogo, logo: BaiduAiAppLogo,
url: 'https://yiyan.baidu.com/' url: 'https://yiyan.baidu.com/'
}, },
{
id: 'baidu-ai-search',
name: '百度AI搜索',
logo: BaiduAiSearchLogo,
url: 'https://chat.baidu.com/',
bodered: true,
style: {
padding: 5
}
},
{ {
id: 'tencent-yuanbao', id: 'tencent-yuanbao',
name: '腾讯元宝', name: '腾讯元宝',
@@ -167,7 +196,7 @@ export const DEFAULT_MIN_APPS: MinAppType[] = [
}, },
{ {
id: 'perplexity', id: 'perplexity',
name: 'perplexity', name: 'Perplexity',
logo: PerplexityAppLogo, logo: PerplexityAppLogo,
url: 'https://www.perplexity.ai/' url: 'https://www.perplexity.ai/'
}, },
@@ -184,13 +213,6 @@ export const DEFAULT_MIN_APPS: MinAppType[] = [
url: 'https://www.tiangong.cn/', url: 'https://www.tiangong.cn/',
bodered: true bodered: true
}, },
{
id: 'zhihu-zhiada',
name: '知乎直答',
logo: ZhihuAppLogo,
url: 'https://zhida.zhihu.com/',
bodered: true
},
{ {
id: 'hugging-chat', id: 'hugging-chat',
name: 'HuggingChat', name: 'HuggingChat',
@@ -220,6 +242,13 @@ export const DEFAULT_MIN_APPS: MinAppType[] = [
}, },
{ {
id: 'nm', id: 'nm',
name: '纳米AI',
logo: NamiAiLogo,
url: 'https://bot.n.cn/',
bodered: true
},
{
id: 'nm-search',
name: '纳米AI搜索', name: '纳米AI搜索',
logo: NamiAiSearchLogo, logo: NamiAiSearchLogo,
url: 'https://www.n.cn/', url: 'https://www.n.cn/',
@@ -230,7 +259,10 @@ export const DEFAULT_MIN_APPS: MinAppType[] = [
name: 'ThinkAny', name: 'ThinkAny',
logo: ThinkAnyLogo, logo: ThinkAnyLogo,
url: 'https://thinkany.ai/', url: 'https://thinkany.ai/',
bodered: true bodered: true,
style: {
padding: 5
}
}, },
{ {
id: 'hika', id: 'hika',
@@ -283,6 +315,84 @@ export const DEFAULT_MIN_APPS: MinAppType[] = [
name: 'AI Studio', name: 'AI Studio',
logo: AIStudioLogo, logo: AIStudioLogo,
url: 'https://aistudio.google.com/' url: 'https://aistudio.google.com/'
},
{
id: 'xiaoyi',
name: '小艺',
logo: XiaoYiAppLogo,
url: 'https://xiaoyi.huawei.com/chat/',
bodered: true
},
{
id: 'notebooklm',
name: 'NotebookLM',
logo: NotebookLMAppLogo,
url: 'https://notebooklm.google.com/'
},
{
id: 'coze',
name: 'Coze',
logo: CozeAppLogo,
url: 'https://www.coze.com/space',
bodered: true
},
{
id: 'dify',
name: 'Dify',
logo: DifyAppLogo,
url: 'https://cloud.dify.ai/apps',
bodered: true,
style: {
padding: 5
}
},
{
id: 'wpslingxi',
name: 'WPS灵犀',
logo: WPSLingXiLogo,
url: 'https://copilot.wps.cn/',
bodered: true
},
{
id: 'lechat',
name: 'LeChat',
logo: LeChatLogo,
url: 'https://chat.mistral.ai/chat',
bodered: true
},
{
id: 'abacus',
name: 'Abacus',
logo: AbacusLogo,
url: 'https://apps.abacus.ai/chatllm',
bodered: true
},
{
id: 'lambdachat',
name: 'Lambda Chat',
logo: LambdaChatLogo,
url: 'https://lambda.chat/',
bodered: true
},
{
id: 'monica',
name: 'Monica',
logo: MonicaLogo,
url: 'https://monica.im/home/',
bodered: true
},
{
id: 'you',
name: 'You',
logo: YouLogo,
url: 'https://you.com/'
},
{
id: 'zhihu',
name: '知乎直答',
logo: ZhihuAppLogo,
url: 'https://zhida.zhihu.com/',
bodered: true
} }
] ]

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