Compare commits

...

73 Commits

Author SHA1 Message Date
MyPrototypeWhat
0ba76af300 refactor: reorganize routing and clean up unused components in Discover and MCP Servers pages
- Introduced a new routers.ts file to centralize routing logic for the Discover page.
- Updated DiscoverContent to utilize dynamic routing based on the new routers configuration.
- Removed commented-out routes and unnecessary imports from various components.
- Simplified navigation logic in MCP Servers pages by adjusting route paths.
- Cleaned up Navbar components in multiple pages for better maintainability.
2025-06-09 19:38:11 +08:00
MyPrototypeWhat
f2c52dfe89 Merge remote-tracking branch 'origin/feat/sidebar' into feat/cherry-store-render 2025-06-09 17:49:42 +08:00
MyPrototypeWhat
880d325028 fix: add missing icon import in AppsPage component 2025-06-09 17:49:30 +08:00
MyPrototypeWhat
2eb421a1de Merge tag 'feat/sidebar' into feat/cherry-store-render 2025-06-09 17:48:54 +08:00
MyPrototypeWhat
4a0924ce15 Merge branch 'feat/cherry-store-render' into feat/sidebar 2025-06-09 17:42:18 +08:00
MyPrototypeWhat
b7eef3b753 refactor: update ESLint configuration and clean up useDiscoverCategories logic
- Added new path to ESLint configuration for better linting coverage.
- Refactored useDiscoverCategories to simplify category handling and improve URL path matching logic.
- Removed unnecessary items from initialCategories for clarity and maintainability.
2025-06-09 16:19:00 +08:00
MyPrototypeWhat
d3f5887980 refactor: update Tailwind CSS integration in electron.vite.config.ts
- Changed the import of Tailwind CSS plugin to a dynamic import for improved performance.
- Ensured compatibility with the existing Vite configuration while maintaining functionality.
2025-06-09 16:08:03 +08:00
Teo
8db2059605 style(color.scss): update border color to improve UI consistency 2025-06-09 15:54:20 +08:00
Teo
d11b98dfbb feat(Inputbar): add SettingButton component for settings access 2025-06-09 15:54:16 +08:00
MyPrototypeWhat
38330c4c81 refactor: update layout of AgentsPage and AppsPage, remove Navbar components
- Refactored AgentsPage and AppsPage to enhance layout by replacing Navbar components with div wrappers.
- Integrated Tailwind CSS for improved styling consistency.
- Adjusted input components for better alignment and spacing.
2025-06-09 15:43:34 +08:00
kangfenmao
b762cfd60b feat: enhance summarization prompt and add topic sidebar visibility toggle 2025-06-09 15:39:30 +08:00
MyPrototypeWhat
278397f7c8 fix: update sidebar icons and enhance DiscoverPage layout
- Updated DiscoverPage to include full height and width styling for better layout.
- Modified sidebar icon visibility logic in migration to ensure 'discover' is added correctly while filtering out specific icons.
- Changed default sidebar icons to replace 'store' with 'discover' for consistency.
2025-06-09 15:27:48 +08:00
MyPrototypeWhat
c6d5faff73 feat: update TypeScript configuration and enhance discover page layout
- Added support for the @modelcontextprotocol/sdk in tsconfig.node.json.
- Updated import paths in provider.ts to include .js extensions.
- Enhanced AgentsPage layout by integrating Navbar and Input components.
- Refactored DiscoverPage to remove DialogManagerProvider and related components, simplifying the structure.
- Removed unused dialog components and hooks to streamline the discover functionality.
- Minor adjustments to AssistantSettings and Vercel tabs for improved code clarity.
2025-06-09 15:22:25 +08:00
kangfenmao
9cac8fba56 feat: add event listener to MainSidebar for topic tab navigation 2025-06-09 15:04:44 +08:00
MyPrototypeWhat
b7d9949832 Merge remote-tracking branch 'origin/main' into feat/cherry-store-render 2025-06-09 14:47:41 +08:00
kangfenmao
b4665509ab fix: set WindowService transparency to false for consistent behavior across platforms 2025-06-09 14:45:12 +08:00
kangfenmao
21e88b02ea refactor: simplify HomeTabs component by removing unused imports and commented code, update AssistantAddItem hover styles 2025-06-09 14:39:29 +08:00
kangfenmao
10caef2c4c refactor: clean up MainSidebar and useChat hooks, remove unused state handling and improve topic selection logic 2025-06-09 14:19:11 +08:00
kangfenmao
6ea1bcc7d1 fix: invert transparency setting for WindowService based on OS 2025-06-09 13:49:40 +08:00
kangfenmao
06a60c4871 feat: implement ChatNavbar component and enhance MainNavbar with search functionality 2025-06-09 12:05:41 +08:00
kangfenmao
684367bf7c fix: adjust navbar and title bar dimensions, update icon handling 2025-06-09 11:50:30 +08:00
kangfenmao
75b9e2f408 feat: new app sidebar 2025-06-09 11:20:41 +08:00
Wang Jiyuan
e13b136484 feat: add prompt variables description (#6991)
* feat: add prompt variables description

* fix: remove comment
2025-06-09 10:41:26 +08:00
自由的世界人
9c5fa57936 fix: update README files to enhance navigation and add project badges (#6982)
* fix: update README files to enhance navigation and add project badges

* fix: english version

* fix: sponsor link error
2025-06-08 21:47:22 +08:00
kangfenmao
7e201522d0 fix: remove topic or message did not delte releated files
This reverts commit df35f25502.
2025-06-08 13:23:48 +08:00
自由的世界人
df35f25502 fix: streamline file selection and ensure deletion of topic-related f… (#6872)
* fix: streamline file selection and ensure deletion of topic-related files

* fix: improve file deletion logic
2025-06-08 12:42:53 +08:00
George·Dong
f9e557763e fix(migrate): old translateModel incorrect (#6965)
* fix(migrate): old translateModel incorrect

* fix(migrate): old translateModel incorrect

* feat(models): improve default model init

* fix(migrate): update translateModel check

* fix(migrate): update translateModel check
2025-06-08 12:38:50 +08:00
beyondkmp
eafd814caf fix(BackupManager): add content length to WebDAV file upload options (#6977)
feat(BackupManager): add content length to WebDAV file upload options
2025-06-08 12:36:20 +08:00
kangfenmao
b84f7bf596 fix: prevent textarea from focusing when in fullscreen mode 2025-06-08 11:37:38 +08:00
kangfenmao
c1d753b7fe refactor: update input tools configuration to hide unused tools and set initial collapsed state 2025-06-08 11:30:50 +08:00
kangfenmao
3350f58422 fix: cannot remove assistat tag 2025-06-08 11:11:52 +08:00
自由的世界人
8c617872e0 fix: Implement label folding, drag-and-drop sorting of assistants within labels, and drag-and-drop sorting of labels (#6735)
* fix: add collapsible tags in AssistantsTab for better organization

* fix: implement drag-and-drop functionality for reordering assistants in tags

* fix: implement drag-and-drop functionality for reordering tags in AssistantTagsPopup

* fix: eslint error
2025-06-08 11:03:39 +08:00
purefkh
a333c635cb fix: prevent emoji picker from closing unexpectedly with IME 2025-06-08 10:55:54 +08:00
Caelan
a244057b3a feat: dmxapi images to image (#6935)
新增改图,合并图
2025-06-08 10:54:46 +08:00
tommyzhang100504
79d7ffcbad build: 增加自动更新文档中版本号的github workflow (#6971)
更新程序
2025-06-08 10:49:56 +08:00
Wang Jiyuan
2d985c1f91 refactor: better semantic of obsidian export options (#6926) 2025-06-08 00:02:15 +08:00
fullex
5879ccbeb2 fix: update default translate model to deepseek-v3 (#6960)
fix: update translate model to new default in llm state and migration logic
2025-06-07 21:44:28 +08:00
Wang Jiyuan
7887f4867d fix: voyage ai can't be used on text embedding (#6950) 2025-06-07 21:40:14 +08:00
Doekin
c38a6cdfbf feat(restoreFromWebdav): make credentials and path optional (#6922)
* feat(BackupService): add feedback messages for backup operations

* feat(WebDAV): Allow optional username, password, and path for unauthenticated access
2025-06-07 21:25:08 +08:00
Wang Jiyuan
ea7766db44 fix: update silicon docs and models (#6953) 2025-06-07 17:29:07 +08:00
fullex
a5012ce49e fix: set message translate dropdown height (#6954)
fix: improve dropdown menu styling and placement in MessageMenubar component

- Set a maximum height and overflow behavior for the dropdown menu to enhance usability.
- Changed the dropdown placement from "topRight" to "top" for better alignment with the UI.
2025-06-07 17:17:02 +08:00
Wang Jiyuan
d3da4f4623 fix: couldn't edit text when sent file-only message (#6930) 2025-06-07 00:19:21 +08:00
purefkh
7f12c2f8b8 fix: set thinking budget to 0 for gemini-2.5-flash when reasoning effort is off (#6917) 2025-06-06 22:11:18 +08:00
Wang Jiyuan
9ba2dea148 fix: message editor doesn't resize (#6924)
* fix: message editor doesn't resize

* fix: remove console log

* fix: optimize useEffect dependencies and improve textarea resizing logic

---------

Co-authored-by: Pleasurecruise <3196812536@qq.com>
2025-06-06 22:02:48 +08:00
Doekin
653bfa1f17 refactor: unified image viewer with integrated context menu (#6892)
* fix(Markdown): eliminate hydration error from image `<div>` nested in `<p>`

Signed-off-by: Chan Lee <Leetimemp@gmail.com>

* feat: add support for reading local files in binary format

Signed-off-by: Chan Lee <Leetimemp@gmail.com>

* refactor(ImageViewer): Consolidate image rendering for unified display and context menu

Signed-off-by: Chan Lee <Leetimemp@gmail.com>

---------

Signed-off-by: Chan Lee <Leetimemp@gmail.com>
2025-06-06 18:52:45 +08:00
one
fa00b5b173 feat(SelectionAssistant): add the "quote" action (#6868)
* feat(SelectionAssistant): add the "quote" action

* fix: i18n for "高级"

* refactor: move quote-to-main to WindowService

* refactor: move formatQuotedText to renderer
2025-06-06 18:12:38 +08:00
fullex
70fb6393b6 fix(SelectionAssistant): add explorer.exe to filterlist 2025-06-06 17:32:16 +08:00
kangfenmao
5b379666f4 refactor: move minapp settings to minapp page 2025-06-06 17:30:16 +08:00
kangfenmao
3cb34d30a9 refactor: remove isPreset messages and assistant.messages 2025-06-06 15:54:32 +08:00
beyondkmp
d47c93b4d8 feat: add set feed url functionality for early access (#5723)
* feat: add update channel functionality for beta testing

- Introduced a new IPC channel for setting the update channel.
- Implemented logic in AppUpdater to handle update channel changes.
- Updated settings to include a beta testing toggle, allowing users to switch between stable and beta update channels.
- Enhanced the settings UI to reflect the new beta testing option.

* add i18n

* update i18n

* update i18n

* refactor: rename update channel to feed URL and update related functionality

- Changed IPC channel from App_SetUpdateChannel to App_SetFeedUrl.
- Updated AppUpdater to set feed URL instead of update channel.
- Modified preload and settings to reflect the new feed URL functionality.
- Added constants for production and early access feed URLs.

* refactor: remove setAutoUpdate method from API

- Eliminated the setAutoUpdate method from the API object in preload index, streamlining the IPC communication interface.

* refactor: update early access feed URL and improve tooltip descriptions

- Changed EARLY_ACCESS_FEED_URL to point to the latest GitHub release.
- Simplified the setEarlyAccess function to directly set the feed URL.
- Added tooltips for early access settings in multiple languages to inform users about potential instability and the need for data backup.

* feat(migrate): add early access setting to state configuration

- Introduced a new state setting 'earlyAccess' and initialized it to false in the migration configuration.

* fix(i18n): update early access tooltip translations for clarity

- Revised the tooltip descriptions for the early access feature in English, Simplified Chinese, and Traditional Chinese to enhance clarity and ensure consistency in messaging regarding potential instability and the importance of data backup.

* feat: introduce FeedUrl enum for centralized feed URL management

- Added a new enum `FeedUrl` in the constants file to define production and early access feed URLs.
- Updated relevant IPC handlers and services to utilize the `FeedUrl` enum for type safety and consistency.
- Refactored the configuration manager to include methods for getting and setting the feed URL using the new enum.

* feat(settings): initialize early access and auto-update settings in AboutSettings component

- Added initialization for early access and auto-check update settings in the AboutSettings component to enhance user configuration options.

---------

Co-authored-by: beyondkmp <beyondkmkp@gmail.com>
2025-06-06 15:48:54 +08:00
SuYao
bc5cc4bf02 hotfix: enhance OpenAI stream handling and error management (#6541)
fix: enhance OpenAI stream handling and error management

- Updated the `openAIChunkToTextDelta` function to include error handling with a try-catch block, improving robustness during stream processing.
- Refined the `readableStreamAsyncIterable` function to ensure proper handling of stream completion and errors, including a return method for cleanup.
- Adjusted type definitions for better clarity and consistency in the handling of async iterables.
2025-06-06 15:18:16 +08:00
SuYao
8efa7d25f8 fix(Inputbar): remove unnecessary flex properties from Inputbar styles (#6902) 2025-06-06 15:16:12 +08:00
fullex
59195fec1a fix(SelectionAssistant): default disabled (#6897)
fix: selection default off
2025-06-06 14:19:55 +08:00
one
14e6a80049 fix(SelectionToolbar): prevent dragging the demo (#6888) 2025-06-06 12:34:02 +08:00
one
67ab36e0ea refactor(SelectionToolbar): add transition effects to action buttons (#6869)
* refactor(SelectionToolbar): add transition effects to action buttons

* refactor: reduce transition duration
2025-06-06 09:24:11 +08:00
fullex
dfc32967ed fix(SelectionAssistant): support selection when alt key pressed (#6865)
fix: support alt key selection
2025-06-06 08:29:00 +08:00
rainnoon
aa3c376def fix(Inputbar): fix textarea expansion and collapse issues with long text (CherryHQ#6857) (#6873)
fix(Inputbar): fix textarea expansion and collapse issues with long text (#6857)
2025-06-06 04:05:17 +08:00
自由的世界人
61c58caf78 hotfix: gemini-2.5-pro-preview-06-05 using error (#6870) 2025-06-06 03:42:52 +08:00
one
b402cdf7ff perf: improve responsiveness on streaming formulas (#6659)
* perf: improve performance on streaming formulas

* refactor: create throttlers for blocks

* refactor: use LRU cache for better memory management
2025-06-06 03:07:59 +08:00
one
d80513d011 refactor(CodePreview): improve the triggering timing for highlighting (#6866) 2025-06-06 00:41:03 +08:00
Doekin
4bcfbf785f feat: enable rendering and download of inline base64-encoded images (#6669)
This commit introduces support for displaying and downloading
inline base64-encoded images (specifically PNG and JPEG formats)
within Markdown content.

Key changes:
- Modified 'urlTransform' in the Markdown component to allow 'data:image/png'
  and 'data:image/jpeg' URLs, enabling their rendering.
- Updated the 'download' utility to handle 'data:' URLs,
  allowing users to save these inline images.

Signed-off-by: Chan Lee <Leetimemp@gmail.com>
2025-06-06 00:29:47 +08:00
SuYao
b722dab56b fix(OpenAIProvider): ensure tool_calls are only yielded when present (#6861)
This update modifies the OpenAIProvider to yield tool_calls only if they exist and have a length greater than zero, improving the handling of delta content. Additionally, a minor cleanup was performed by removing an unnecessary blank line in the code.
2025-06-05 22:49:29 +08:00
fullex
6165e4a47f fix(SelectionToolbar): prevent CSS updates in demo mode 2025-06-05 19:50:49 +08:00
fullex
b829abed2d fix(SelectionAssistant): ignore CtrlKey mode when ctrl+click (#6843)
fix(SelectionService): add mouse-down listener for multi-selection in ctrlkey mode
2025-06-05 19:05:17 +08:00
lizhixuan
475c1e38df feat: refactor store to discover transition and enhance UI components
- Updated package.json to include 'usehooks-ts' and upgraded 'lucide-react' to version 0.511.0.
- Replaced 'store' with 'discover' in the routing and sidebar components for improved navigation.
- Introduced new DiscoverPage and related components for better organization of content.
- Enhanced localization support by adding Chinese translations for the discover feature.
- Removed deprecated store components to streamline the codebase and improve maintainability.
2025-05-18 16:08:26 +08:00
MyPrototypeWhat
80289f1dc3 feat: update store components and add dialog management functionality
- Updated package.json to use the latest version of the 'motion' library.
- Refactored store components to improve organization and user experience, including the addition of AssistantCard and MiniAppCard components.
- Introduced a DialogManager for handling dialog states and interactions.
- Enhanced StoreContent and StoreSidebar components to support new item types and improved layout.
- Added new JSON data for mini-apps and updated store categories for better accessibility.
2025-05-16 19:11:46 +08:00
MyPrototypeWhat
ef16558947 feat: enhance Prettier configuration and update store components
- Added Tailwind CSS support to Prettier configuration with new settings for styles and functions.
- Updated package.json to include the prettier-plugin-tailwindcss dependency.
- Refactored various store components for improved layout and organization, including adjustments to error handling and component structure.
- Enhanced CSS styles for better responsiveness and visual consistency across components.
2025-05-15 18:18:54 +08:00
MyPrototypeWhat
c799f15fcc feat: update store components and enhance assistant functionality
- Refactored store components to improve organization and user experience, including the introduction of new GridView and ListView components.
- Implemented a detail dialog for displaying item information and installation options.
- Enhanced the store sidebar with collapsible categories for better navigation.
- Updated data structures to support dynamic subcategory handling and improved filtering capabilities.
- Added utility functions for dialog and collapsible components to streamline UI interactions.
2025-05-14 17:17:24 +08:00
MyPrototypeWhat
802402e922 feat: add store categories and items with enhanced filtering functionality
- Introduced new JSON files for store categories and assistant items to improve organization and accessibility.
- Implemented a conversion script to dynamically generate the assistant items list from agents data.
- Refactored store components to utilize the new data structure, enhancing the store layout and user experience.
- Added loading states and error handling for category and item fetching processes.
- Created new GridView and ListView components for displaying store items in different formats.
2025-05-13 19:25:37 +08:00
lizhixuan
37482bca7b feat: refactor electron and vitest configuration for dynamic imports and improved structure
- Updated electron.vite.config.ts to use dynamic imports for Tailwind CSS.
- Refactored vitest.config.ts to asynchronously retrieve renderer configuration from electron.vite.config.
- Enhanced plugin and alias management for better maintainability and performance.
2025-05-12 23:37:11 +08:00
lizhixuan
184713dba8 feat: enhance store page with new components and functionality
- Updated component paths in components.json for better organization.
- Added 'motion' library to package.json for animations.
- Refactored TypeScript configuration to include new renderer paths.
- Implemented new StoreContent and StoreSidebar components for improved store layout.
- Integrated store categories and items with filtering capabilities.
- Enhanced UI with Tailwind CSS animations and styles for a better user experience.
2025-05-12 22:58:54 +08:00
MyPrototypeWhat
0a0956cfc4 feat: update store page and integrate new UI components
- Updated Tailwind CSS configuration and styles for the store page.
- Added new UI components including Card, Badge, DropdownMenu, and Sidebar for enhanced user experience.
- Implemented store categories and items with filtering functionality.
- Introduced mobile responsiveness with a custom hook for detecting mobile devices.
- Enhanced theme management to support dynamic theme changes.
- Added new utility functions for improved class name management.
2025-05-12 19:28:23 +08:00
MyPrototypeWhat
0a0bbad77f feat: integrate Tailwind CSS and add store page functionality
- Introduced Tailwind CSS for styling by adding a new configuration file and global styles.
- Created a new Store page with a button to test configuration success.
- Updated routing to include the Store page and added corresponding sidebar icon.
- Enhanced the settings store to include the new Store icon in the sidebar.
- Updated translations for the Store page in Chinese.
- Added utility functions for class name management using Tailwind CSS.
2025-05-09 16:36:50 +08:00
176 changed files with 8795 additions and 3378 deletions

View File

@@ -115,3 +115,38 @@ jobs:
tag: ${{ steps.get-tag.outputs.tag }}
artifacts: 'dist/*.exe,dist/*.zip,dist/*.dmg,dist/*.AppImage,dist/*.snap,dist/*.deb,dist/*.rpm,dist/*.tar.gz,dist/latest*.yml,dist/rc*.yml,dist/*.blockmap'
token: ${{ secrets.GITHUB_TOKEN }}
dispatch-docs-update:
needs: release
if: success() && github.repository == 'CherryHQ/cherry-studio' # 确保所有构建成功且在主仓库中运行
runs-on: ubuntu-latest
steps:
- name: Get release tag
id: get-tag
shell: bash
run: |
if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
echo "tag=${{ github.event.inputs.tag }}" >> $GITHUB_OUTPUT
else
echo "tag=${GITHUB_REF#refs/tags/}" >> $GITHUB_OUTPUT
fi
- name: Check if tag is pre-release
id: check-tag
shell: bash
run: |
TAG="${{ steps.get-tag.outputs.tag }}"
if [[ "$TAG" == *"rc"* || "$TAG" == *"pre-release"* ]]; then
echo "is_pre_release=true" >> $GITHUB_OUTPUT
else
echo "is_pre_release=false" >> $GITHUB_OUTPUT
fi
- name: Dispatch update-download-version workflow to cherry-studio-docs
if: steps.check-tag.outputs.is_pre_release == 'false'
uses: peter-evans/repository-dispatch@v3
with:
token: ${{ secrets.REPO_DISPATCH_TOKEN }}
repository: CherryHQ/cherry-studio-docs
event-type: update-download-version
client-payload: '{"version": "${{ steps.get-tag.outputs.tag }}"}'

View File

@@ -4,5 +4,8 @@
"printWidth": 120,
"trailingComma": "none",
"endOfLine": "lf",
"bracketSameLine": true
"bracketSameLine": true,
"tailwindStylesheet": "./src/renderer/src/assets/styles/tailwind.css",
"tailwindFunctions": ["clsx"],
"plugins": ["prettier-plugin-tailwindcss"],
}

View File

@@ -3,10 +3,42 @@
<img src="https://github.com/CherryHQ/cherry-studio/blob/main/build/icon.png?raw=true" width="150" height="150" alt="banner" /><br>
</a>
</h1>
<p align="center">English | <a href="./docs/README.zh.md">中文</a> | <a href="./docs/README.ja.md">日本語</a><br></p>
<p align="center">English | <a href="./docs/README.zh.md">中文</a> | <a href="./docs/README.ja.md">日本語</a> | <a href="https://cherry-ai.com">Official Site</a> | <a href="https://docs.cherry-ai.com/cherry-studio-wen-dang/en-us">Documents</a> | <a href="./docs/dev.md">Development</a> | <a href="https://github.com/CherryHQ/cherry-studio/issues">Feedback</a><br></p>
<!-- 题头徽章组合 -->
<div align="center">
[![][deepwiki-shield]][deepwiki-link]
[![][twitter-shield]][twitter-link]
[![][discord-shield]][discord-link]
[![][telegram-shield]][telegram-link]
</div>
<!-- 项目统计徽章 -->
<div align="center">
[![][github-stars-shield]][github-stars-link]
[![][github-forks-shield]][github-forks-link]
[![][github-release-shield]][github-release-link]
[![][github-contributors-shield]][github-contributors-link]
</div>
<div align="center">
[![][license-shield]][license-link]
[![][commercial-shield]][commercial-link]
[![][sponsor-shield]][sponsor-link]
</div>
<div align="center">
<a href="https://hellogithub.com/repository/1605492e1e2a4df3be07abfa4578dd37" target="_blank"><img src="https://api.hellogithub.com/v1/widgets/recommend.svg?rid=1605492e1e2a4df3be07abfa4578dd37" alt="FeaturedHelloGitHub" style="width: 200px; height: 43px;" width="200" height="43" /></a>
<a href="https://trendshift.io/repositories/11772" target="_blank"><img src="https://trendshift.io/api/badge/repositories/11772" alt="kangfenmao%2Fcherry-studio | Trendshift" style="width: 250px; height: 55px;" width="250" height="55"/></a>
<a href="https://www.producthunt.com/posts/cherry-studio?embed=true&utm_source=badge-featured&utm_medium=badge&utm_souce=badge-cherry&#0045;studio" target="_blank"><img src="https://api.producthunt.com/widgets/embed-image/v1/featured.svg?post_id=496640&theme=light" alt="Cherry&#0032;Studio - AI&#0032;Chatbots&#0044;&#0032;AI&#0032;Desktop&#0032;Client | Product Hunt" style="width: 250px; height: 54px;" width="250" height="54" /></a>
<a href="https://www.producthunt.com/posts/cherry-studio?embed=true&utm_source=badge-featured&utm_medium=badge&utm_souce=badge-cherry&#0045;studio" target="_blank"><img src="https://api.producthunt.com/widgets/embed-image/v1/featured.svg?post_id=496640&theme=light" alt="Cherry&#0032;Studio - AI&#0032;Chatbots&#0044;&#0032;AI&#0032;Desktop&#0032;Client | Product Hunt" style="width: 200px; height: 43px;" width="200" height="43" /></a>
</div>
# 🍒 Cherry Studio
@@ -17,10 +49,6 @@ Cherry Studio is a desktop client that supports for multiple LLM providers, avai
❤️ Like Cherry Studio? Give it a star 🌟 or [Sponsor](docs/sponsor.md) to support the development!
# 📖 Guide
<https://docs.cherry-ai.com>
# 🌠 Screenshot
![](https://github.com/user-attachments/assets/36dddb2c-e0fb-4a5f-9411-91447bab6e18)
@@ -114,14 +142,6 @@ Want to influence our roadmap? Join our [GitHub Discussions](https://github.com/
Welcome PR for more themes
# 🖥️ Develop
Refer to the [development documentation](docs/dev.md)
Refer to the [Architecture overview documentation](https://deepwiki.com/CherryHQ/cherry-studio)
Refer to the [Branching Strategy](docs/branching-strategy-en.md) for contribution guidelines
# 🤝 Contributing
We welcome contributions to Cherry Studio! Here are some ways you can contribute:
@@ -134,6 +154,8 @@ We welcome contributions to Cherry Studio! Here are some ways you can contribute
6. **Community Engagement**: Join discussions and help users.
7. **Promote Usage**: Spread the word about Cherry Studio.
Refer to the [Branching Strategy](docs/branching-strategy-en.md) for contribution guidelines
## Getting Started
1. **Fork the Repository**: Fork and clone it to your local machine.
@@ -158,22 +180,34 @@ Thank you for your support and contributions!
</a>
<br /><br />
# 🌐 Community
[Telegram](https://t.me/CherryStudioAI) | [Email](mailto:support@cherry-ai.com) | [Twitter](https://x.com/kangfenmao)
# ☕ Sponsor
[Buy Me a Coffee](docs/sponsor.md)
# 📃 License
[LICENSE](./LICENSE)
# ✉️ Contact
<yinsenho@cherry-ai.com>
# ⭐️ Star History
[![Star History Chart](https://api.star-history.com/svg?repos=kangfenmao/cherry-studio&type=Timeline)](https://star-history.com/#kangfenmao/cherry-studio&Timeline)
[![Star History Chart](https://api.star-history.com/svg?repos=CherryHQ/cherry-studio&type=Timeline)](https://star-history.com/#CherryHQ/cherry-studio&Timeline)
<!-- Links & Images -->
[deepwiki-shield]: https://img.shields.io/badge/Deepwiki-CherryHQ-0088CC?style=plastic
[deepwiki-link]: https://deepwiki.com/CherryHQ/cherry-studio
[twitter-shield]: https://img.shields.io/badge/Twitter-CherryStudioApp-0088CC?style=plastic&logo=x
[twitter-link]: https://twitter.com/CherryStudioApp
[discord-shield]: https://img.shields.io/badge/Discord-@CherryStudio-0088CC?style=plastic&logo=discord
[discord-link]: https://discord.gg/wez8HtpxqQ
[telegram-shield]: https://img.shields.io/badge/Telegram-@CherryStudioAI-0088CC?style=plastic&logo=telegram
[telegram-link]: https://t.me/CherryStudioAI
<!-- Links & Images -->
[github-stars-shield]: https://img.shields.io/github/stars/CherryHQ/cherry-studio?style=social
[github-stars-link]: https://github.com/CherryHQ/cherry-studio/stargazers
[github-forks-shield]: https://img.shields.io/github/forks/CherryHQ/cherry-studio?style=social
[github-forks-link]: https://github.com/CherryHQ/cherry-studio/network
[github-release-shield]: https://img.shields.io/github/v/release/CherryHQ/cherry-studio
[github-release-link]: https://github.com/CherryHQ/cherry-studio/releases
[github-contributors-shield]: https://img.shields.io/github/contributors/CherryHQ/cherry-studio
[github-contributors-link]: https://github.com/CherryHQ/cherry-studio/graphs/contributors
<!-- Links & Images -->
[license-shield]: https://img.shields.io/badge/License-AGPLv3-important.svg?style=plastic&logo=gnu
[license-link]: https://www.gnu.org/licenses/agpl-3.0
[commercial-shield]: https://img.shields.io/badge/License-Contact-white.svg?style=plastic&logoColor=white&logo=telegram&color=blue
[commercial-link]: mailto:license@cherry-ai.com?subject=Commercial%20License%20Inquiry
[sponsor-shield]: https://img.shields.io/badge/Sponsor-FF6699.svg?style=plastic&logo=githubsponsors&logoColor=white
[sponsor-link]: https://github.com/CherryHQ/cherry-studio/blob/main/docs/sponsor.md

21
components.json Normal file
View File

@@ -0,0 +1,21 @@
{
"$schema": "https://ui.shadcn.com/schema.json",
"style": "new-york",
"rsc": false,
"tsx": true,
"tailwind": {
"config": "",
"css": "src/renderer/src/assets/styles/tailwind.css",
"baseColor": "zinc",
"cssVariables": true,
"prefix": ""
},
"aliases": {
"components": "@renderer/ui/third-party",
"utils": "@renderer/utils",
"ui": "@renderer/ui",
"lib": "@renderer/lib",
"hooks": "@renderer/hooks"
},
"iconLibrary": "lucide"
}

View File

@@ -1,15 +1,46 @@
<h1 align="center">
<a href="https://github.com/CherryHQ/cherry-studio/releases">
<img src="https://github.com/CherryHQ/cherry-studio/blob/main/build/icon.png?raw=true" width="150" height="150" alt="banner" />
<img src="https://github.com/CherryHQ/cherry-studio/blob/main/build/icon.png?raw=true" width="150" height="150" alt="banner" /><br>
</a>
</h1>
<p align="center">
<a href="https://github.com/CherryHQ/cherry-studio">English</a> | <a href="./README.zh.md">中文</a> | 日本語 <br>
<a href="https://github.com/CherryHQ/cherry-studio">English</a> | <a href="./README.zh.md">中文</a> | 日本語 | <a href="https://cherry-ai.com">公式サイト</a> | <a href="https://docs.cherry-ai.com/cherry-studio-wen-dang/ja">ドキュメント</a> | <a href="./dev.md">開発</a> | <a href="https://github.com/CherryHQ/cherry-studio/issues">フィードバック</a><br>
</p>
<!-- バッジコレクション -->
<div align="center">
[![][deepwiki-shield]][deepwiki-link]
[![][twitter-shield]][twitter-link]
[![][discord-shield]][discord-link]
[![][telegram-shield]][telegram-link]
</div>
<!-- プロジェクト統計 -->
<div align="center">
[![][github-stars-shield]][github-stars-link]
[![][github-forks-shield]][github-forks-link]
[![][github-release-shield]][github-release-link]
[![][github-contributors-shield]][github-contributors-link]
</div>
<div align="center">
[![][license-shield]][license-link]
[![][commercial-shield]][commercial-link]
[![][sponsor-shield]][sponsor-link]
</div>
<div align="center">
<a href="https://hellogithub.com/repository/1605492e1e2a4df3be07abfa4578dd37" target="_blank"><img src="https://api.hellogithub.com/v1/widgets/recommend.svg?rid=1605492e1e2a4df3be07abfa4578dd37" alt="FeaturedHelloGitHub" style="width: 200px; height: 43px;" width="200" height="43" /></a>
<a href="https://trendshift.io/repositories/11772" target="_blank"><img src="https://trendshift.io/api/badge/repositories/11772" alt="kangfenmao%2Fcherry-studio | Trendshift" style="width: 250px; height: 55px;" width="250" height="55"/></a>
<a href="https://www.producthunt.com/posts/cherry-studio?embed=true&utm_source=badge-featured&utm_medium=badge&utm_souce=badge-cherry&#0045;studio" target="_blank"><img src="https://api.producthunt.com/widgets/embed-image/v1/featured.svg?post_id=496640&theme=light" alt="Cherry&#0032;Studio - AI&#0032;Chatbots&#0044;&#0032;AI&#0032;Desktop&#0032;Client | Product Hunt" style="width: 250px; height: 54px;" width="250" height="54" /></a>
<a href="https://www.producthunt.com/posts/cherry-studio?embed=true&utm_source=badge-featured&utm_medium=badge&utm_souce=badge-cherry&#0045;studio" target="_blank"><img src="https://api.producthunt.com/widgets/embed-image/v1/featured.svg?post_id=496640&theme=light" alt="Cherry&#0032;Studio - AI&#0032;Chatbots&#0044;&#0032;AI&#0032;Desktop&#0032;Client | Product Hunt" style="width: 200px; height: 43px;" width="200" height="43" /></a>
</div>
# 🍒 Cherry Studio
@@ -20,10 +51,6 @@ Cherry Studio は、複数の LLM プロバイダーをサポートするデス
❤️ Cherry Studio をお気に入りにしましたか?小さな星をつけてください 🌟 または [スポンサー](sponsor.md) をして開発をサポートしてください!
# 📖 ガイド
https://docs.cherry-ai.com
# 🌠 スクリーンショット
![](https://github.com/user-attachments/assets/36dddb2c-e0fb-4a5f-9411-91447bab6e18)
@@ -117,14 +144,6 @@ https://docs.cherry-ai.com
より多くのテーマの PR を歓迎します
# 🖥️ 開発
[開発ドキュメント](dev.md)を参照してください
[アーキテクチャ概要ドキュメント](https://deepwiki.com/CherryHQ/cherry-studio)を参照してください
[ブランチ戦略](branching-strategy-en.md)を参照して貢献ガイドラインを確認してください
# 🤝 貢献
Cherry Studio への貢献を歓迎します!以下の方法で貢献できます:
@@ -137,6 +156,8 @@ Cherry Studio への貢献を歓迎します!以下の方法で貢献できま
6. **コミュニティの参加**:ディスカッションに参加し、ユーザーを支援します
7. **使用の促進**Cherry Studio を広めます
[ブランチ戦略](branching-strategy-en.md)を参照して貢献ガイドラインを確認してください
## 始め方
1. **リポジトリをフォーク**:フォークしてローカルマシンにクローンします
@@ -161,22 +182,34 @@ Cherry Studio への貢献を歓迎します!以下の方法で貢献できま
</a>
<br /><br />
# 🌐 コミュニティ
[Telegram](https://t.me/CherryStudioAI) | [Email](mailto:support@cherry-ai.com) | [Twitter](https://x.com/kangfenmao)
# ☕ スポンサー
[開発者を支援する](sponsor.md)
# 📃 ライセンス
[LICENSE](../LICENSE)
# ✉️ お問い合わせ
yinsenho@cherry-ai.com
# ⭐️ スター履歴
[![Star History Chart](https://api.star-history.com/svg?repos=kangfenmao/cherry-studio&type=Timeline)](https://star-history.com/#kangfenmao/cherry-studio&Timeline)
[![Star History Chart](https://api.star-history.com/svg?repos=CherryHQ/cherry-studio&type=Timeline)](https://star-history.com/#CherryHQ/cherry-studio&Timeline)
<!-- リンクと画像 -->
[deepwiki-shield]: https://img.shields.io/badge/Deepwiki-CherryHQ-0088CC?style=plastic
[deepwiki-link]: https://deepwiki.com/CherryHQ/cherry-studio
[twitter-shield]: https://img.shields.io/badge/Twitter-CherryStudioApp-0088CC?style=plastic&logo=x
[twitter-link]: https://twitter.com/CherryStudioApp
[discord-shield]: https://img.shields.io/badge/Discord-@CherryStudio-0088CC?style=plastic&logo=discord
[discord-link]: https://discord.gg/wez8HtpxqQ
[telegram-shield]: https://img.shields.io/badge/Telegram-@CherryStudioAI-0088CC?style=plastic&logo=telegram
[telegram-link]: https://t.me/CherryStudioAI
<!-- プロジェクト統計 -->
[github-stars-shield]: https://img.shields.io/github/stars/CherryHQ/cherry-studio?style=social
[github-stars-link]: https://github.com/CherryHQ/cherry-studio/stargazers
[github-forks-shield]: https://img.shields.io/github/forks/CherryHQ/cherry-studio?style=social
[github-forks-link]: https://github.com/CherryHQ/cherry-studio/network
[github-release-shield]: https://img.shields.io/github/v/release/CherryHQ/cherry-studio
[github-release-link]: https://github.com/CherryHQ/cherry-studio/releases
[github-contributors-shield]: https://img.shields.io/github/contributors/CherryHQ/cherry-studio
[github-contributors-link]: https://github.com/CherryHQ/cherry-studio/graphs/contributors
<!-- ライセンスとスポンサー -->
[license-shield]: https://img.shields.io/badge/License-AGPLv3-important.svg?style=plastic&logo=gnu
[license-link]: https://www.gnu.org/licenses/agpl-3.0
[commercial-shield]: https://img.shields.io/badge/商用ライセンス-お問い合わせ-white.svg?style=plastic&logoColor=white&logo=telegram&color=blue
[commercial-link]: mailto:license@cherry-ai.com?subject=商業ライセンスについて
[sponsor-shield]: https://img.shields.io/badge/スポンサー-FF6699.svg?style=plastic&logo=githubsponsors&logoColor=white
[sponsor-link]: https://github.com/CherryHQ/cherry-studio/blob/main/docs/sponsor.md

View File

@@ -1,14 +1,46 @@
<h1 align="center">
<a href="https://github.com/CherryHQ/cherry-studio/releases">
<img src="https://github.com/CherryHQ/cherry-studio/blob/main/build/icon.png?raw=true" width="150" height="150" alt="banner" />
<img src="https://github.com/CherryHQ/cherry-studio/blob/main/build/icon.png?raw=true" width="150" height="150" alt="banner" /><br>
</a>
</h1>
<p align="center">
<a href="https://github.com/CherryHQ/cherry-studio">English</a> | 中文 | <a href="./README.ja.md">日本語</a><br>
<a href="https://github.com/CherryHQ/cherry-studio">English</a> | 中文 | <a href="./README.ja.md">日本語</a> | <a href="https://cherry-ai.com">官方网站</a> | <a href="https://docs.cherry-ai.com/cherry-studio-wen-dang/zh-cn">文档</a> | <a href="./dev.md">开发</a> | <a href="https://github.com/CherryHQ/cherry-studio/issues">反馈</a><br>
</p>
<!-- 题头徽章组合 -->
<div align="center">
[![][deepwiki-shield]][deepwiki-link]
[![][twitter-shield]][twitter-link]
[![][discord-shield]][discord-link]
[![][telegram-shield]][telegram-link]
</div>
<!-- 项目统计徽章 -->
<div align="center">
[![][github-stars-shield]][github-stars-link]
[![][github-forks-shield]][github-forks-link]
[![][github-release-shield]][github-release-link]
[![][github-contributors-shield]][github-contributors-link]
</div>
<div align="center">
[![][license-shield]][license-link]
[![][commercial-shield]][commercial-link]
[![][sponsor-shield]][sponsor-link]
</div>
<div align="center">
<a href="https://hellogithub.com/repository/1605492e1e2a4df3be07abfa4578dd37" target="_blank"><img src="https://api.hellogithub.com/v1/widgets/recommend.svg?rid=1605492e1e2a4df3be07abfa4578dd37" alt="FeaturedHelloGitHub" style="width: 200px; height: 43px;" width="200" height="43" /></a>
<a href="https://trendshift.io/repositories/11772" target="_blank"><img src="https://trendshift.io/api/badge/repositories/11772" alt="kangfenmao%2Fcherry-studio | Trendshift" style="width: 250px; height: 55px;" width="250" height="55"/></a>
<a href="https://www.producthunt.com/posts/cherry-studio?embed=true&utm_source=badge-featured&utm_medium=badge&utm_souce=badge-cherry&#0045;studio" target="_blank"><img src="https://api.producthunt.com/widgets/embed-image/v1/featured.svg?post_id=496640&theme=light" alt="Cherry&#0032;Studio - AI&#0032;Chatbots&#0044;&#0032;AI&#0032;Desktop&#0032;Client | Product Hunt" style="width: 250px; height: 54px;" width="250" height="54" /></a>
<a href="https://www.producthunt.com/posts/cherry-studio?embed=true&utm_source=badge-featured&utm_medium=badge&utm_souce=badge-cherry&#0045;studio" target="_blank"><img src="https://api.producthunt.com/widgets/embed-image/v1/featured.svg?post_id=496640&theme=light" alt="Cherry&#0032;Studio - AI&#0032;Chatbots&#0044;&#0032;AI&#0032;Desktop&#0032;Client | Product Hunt" style="width: 200px; height: 43px;" width="200" height="43" /></a>
</div>
# 🍒 Cherry Studio
@@ -124,14 +156,6 @@ https://docs.cherry-ai.com
欢迎 PR 更多主题
# 🖥️ 开发
参考[开发文档](dev.md)
参考[架构概览文档](https://deepwiki.com/CherryHQ/cherry-studio)
参考[分支策略](branching-strategy-zh.md)了解贡献指南
# 🤝 贡献
我们欢迎对 Cherry Studio 的贡献!您可以通过以下方式贡献:
@@ -144,6 +168,8 @@ https://docs.cherry-ai.com
6. **社区参与**:加入讨论并帮助用户
7. **推广使用**:宣传 Cherry Studio
参考[分支策略](branching-strategy-zh.md)了解贡献指南
## 入门
1. **Fork 仓库**Fork 并克隆到您的本地机器
@@ -168,22 +194,34 @@ https://docs.cherry-ai.com
</a>
<br /><br />
# 🌐 社区
[Telegram](https://t.me/CherryStudioAI) | [Email](mailto:support@cherry-ai.com) | [Twitter](https://x.com/kangfenmao)
# ☕ 赞助
[赞助开发者](sponsor.md)
# 📃 许可证
[LICENSE](../LICENSE)
# ✉️ 联系我们
yinsenho@cherry-ai.com
# ⭐️ Star 记录
[![Star History Chart](https://api.star-history.com/svg?repos=kangfenmao/cherry-studio&type=Timeline)](https://star-history.com/#kangfenmao/cherry-studio&Timeline)
[![Star History Chart](https://api.star-history.com/svg?repos=CherryHQ/cherry-studio&type=Timeline)](https://star-history.com/#CherryHQ/cherry-studio&Timeline)
<!-- Links & Images -->
[deepwiki-shield]: https://img.shields.io/badge/Deepwiki-CherryHQ-0088CC?style=plastic
[deepwiki-link]: https://deepwiki.com/CherryHQ/cherry-studio
[twitter-shield]: https://img.shields.io/badge/Twitter-CherryStudioApp-0088CC?style=plastic&logo=x
[twitter-link]: https://twitter.com/CherryStudioApp
[discord-shield]: https://img.shields.io/badge/Discord-@CherryStudio-0088CC?style=plastic&logo=discord
[discord-link]: https://discord.gg/wez8HtpxqQ
[telegram-shield]: https://img.shields.io/badge/Telegram-@CherryStudioAI-0088CC?style=plastic&logo=telegram
[telegram-link]: https://t.me/CherryStudioAI
<!-- 项目统计徽章 -->
[github-stars-shield]: https://img.shields.io/github/stars/CherryHQ/cherry-studio?style=social
[github-stars-link]: https://github.com/CherryHQ/cherry-studio/stargazers
[github-forks-shield]: https://img.shields.io/github/forks/CherryHQ/cherry-studio?style=social
[github-forks-link]: https://github.com/CherryHQ/cherry-studio/network
[github-release-shield]: https://img.shields.io/github/v/release/CherryHQ/cherry-studio
[github-release-link]: https://github.com/CherryHQ/cherry-studio/releases
[github-contributors-shield]: https://img.shields.io/github/contributors/CherryHQ/cherry-studio
[github-contributors-link]: https://github.com/CherryHQ/cherry-studio/graphs/contributors
<!-- 许可和赞助徽章 -->
[license-shield]: https://img.shields.io/badge/License-AGPLv3-important.svg?style=plastic&logo=gnu
[license-link]: https://www.gnu.org/licenses/agpl-3.0
[commercial-shield]: https://img.shields.io/badge/商用授权-联系-white.svg?style=plastic&logoColor=white&logo=telegram&color=blue
[commercial-link]: mailto:license@cherry-ai.com?subject=商业授权咨询
[sponsor-shield]: https://img.shields.io/badge/赞助支持-FF6699.svg?style=plastic&logo=githubsponsors&logoColor=white
[sponsor-link]: https://github.com/CherryHQ/cherry-studio/blob/main/docs/sponsor.md

View File

@@ -40,6 +40,7 @@ export default defineConfig({
},
renderer: {
plugins: [
(async () => (await import('@tailwindcss/vite')).default())(),
react({
plugins: [
[

View File

@@ -62,7 +62,8 @@ export default defineConfig([
'.yarn/**',
'.gitignore',
'scripts/cloudflare-worker.js',
'src/main/integration/nutstore/sso/lib/**'
'src/main/integration/nutstore/sso/lib/**',
'src/renderer/src/ui/**'
]
}
])

View File

@@ -92,7 +92,7 @@
"officeparser": "^4.1.1",
"os-proxy-config": "^1.1.2",
"proxy-agent": "^6.5.0",
"selection-hook": "^0.9.21",
"selection-hook": "^0.9.22",
"tar": "^7.4.3",
"turndown": "^7.2.0",
"webdav": "^5.8.0",
@@ -119,9 +119,18 @@
"@mozilla/readability": "^0.6.0",
"@notionhq/client": "^2.2.15",
"@playwright/test": "^1.52.0",
"@radix-ui/react-collapsible": "^1.1.10",
"@radix-ui/react-dialog": "^1.1.13",
"@radix-ui/react-dropdown-menu": "^2.1.14",
"@radix-ui/react-separator": "^1.1.6",
"@radix-ui/react-slot": "^1.2.2",
"@radix-ui/react-tabs": "^1.1.11",
"@radix-ui/react-tooltip": "^1.2.6",
"@reduxjs/toolkit": "^2.2.5",
"@shikijs/markdown-it": "^3.4.2",
"@swc/plugin-styled-components": "^7.1.5",
"@tailwindcss/vite": "^4.1.5",
"@tavily/core": "patch:@tavily/core@npm%3A0.3.1#~/.yarn/patches/@tavily-core-npm-0.3.1-fe69bf2bea.patch",
"@testing-library/dom": "^10.4.0",
"@testing-library/jest-dom": "^6.6.3",
"@testing-library/react": "^16.3.0",
@@ -150,6 +159,8 @@
"antd": "^5.22.5",
"axios": "^1.7.3",
"browser-image-compression": "^2.0.2",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"color": "^5.0.0",
"dayjs": "^1.11.11",
"dexie": "^4.0.8",
@@ -173,15 +184,17 @@
"lint-staged": "^15.5.0",
"lodash": "^4.17.21",
"lru-cache": "^11.1.0",
"lucide-react": "^0.487.0",
"lucide-react": "^0.511.0",
"mermaid": "^11.6.0",
"mime": "^4.0.4",
"motion": "^12.10.5",
"motion": "^12.12.1",
"next-themes": "^0.4.6",
"npx-scope-finder": "^1.2.0",
"openai": "patch:openai@npm%3A5.1.0#~/.yarn/patches/openai-npm-5.1.0-0e7b3ccb07.patch",
"p-queue": "^8.1.0",
"playwright": "^1.52.0",
"prettier": "^3.5.3",
"prettier-plugin-tailwindcss": "^0.6.11",
"rc-virtual-list": "^3.18.6",
"react": "^19.0.0",
"react-dom": "^19.0.0",
@@ -205,11 +218,16 @@
"rollup-plugin-visualizer": "^5.12.0",
"sass": "^1.88.0",
"shiki": "^3.4.2",
"sonner": "^2.0.3",
"string-width": "^7.2.0",
"styled-components": "^6.1.11",
"tailwind-merge": "^3.2.0",
"tailwindcss": "^4.1.5",
"tiny-pinyin": "^1.3.2",
"tokenx": "^0.4.1",
"tw-animate-css": "^1.2.9",
"typescript": "^5.6.2",
"usehooks-ts": "^3.1.1",
"uuid": "^10.0.0",
"vite": "6.2.6",
"vitest": "^3.1.4"

View File

@@ -13,6 +13,7 @@ export enum IpcChannel {
App_SetTrayOnClose = 'app:set-tray-on-close',
App_SetTheme = 'app:set-theme',
App_SetAutoUpdate = 'app:set-auto-update',
App_SetFeedUrl = 'app:set-feed-url',
App_HandleZoomFactor = 'app:handle-zoom-factor',
App_IsBinaryExist = 'app:is-binary-exist',
@@ -20,6 +21,8 @@ export enum IpcChannel {
App_InstallUvBinary = 'app:install-uv-binary',
App_InstallBunBinary = 'app:install-bun-binary',
App_QuoteToMain = 'app:quote-to-main',
Notification_Send = 'notification:send',
Notification_OnClick = 'notification:on-click',

View File

@@ -403,3 +403,8 @@ export const KB = 1024
export const MB = 1024 * KB
export const GB = 1024 * MB
export const defaultLanguage = 'en-US'
export enum FeedUrl {
PRODUCTION = 'https://releases.cherry-ai.com',
EARLY_ACCESS = 'https://github.com/CherryHQ/cherry-studio/releases/latest/download'
}

View File

@@ -11,13 +11,13 @@ if (isDev) {
export const DATA_PATH = getDataPath()
export const titleBarOverlayDark = {
height: 40,
height: 42,
color: 'rgba(255,255,255,0)',
symbolColor: '#fff'
}
export const titleBarOverlayLight = {
height: 40,
height: 42,
color: 'rgba(255,255,255,0)',
symbolColor: '#000'
}

View File

@@ -20,6 +20,7 @@ interface IFinetunedList {
*************************************************************************/
export const SELECTION_PREDEFINED_BLACKLIST: IFilterList = {
WINDOWS: [
'explorer.exe',
// Screenshot
'snipaste.exe',
'pixpin.exe',

View File

@@ -34,6 +34,7 @@ import { calculateDirectorySize, getResourcePath } from './utils'
import { decrypt, encrypt } from './utils/aes'
import { getCacheDir, getConfigDir, getFilesDir } from './utils/file'
import { compress, decompress } from './utils/zip'
import { FeedUrl } from '@shared/config/constant'
const fileManager = new FileStorage()
const backupManager = new BackupManager()
@@ -112,6 +113,10 @@ export function registerIpc(mainWindow: BrowserWindow, app: Electron.App) {
configManager.setAutoUpdate(isActive)
})
ipcMain.handle(IpcChannel.App_SetFeedUrl, (_, feedUrl: FeedUrl) => {
appUpdater.setFeedUrl(feedUrl)
})
ipcMain.handle(IpcChannel.Config_Set, (_, key: string, value: any, isNotify: boolean = false) => {
configManager.set(key, value, isNotify)
})
@@ -346,4 +351,6 @@ export function registerIpc(mainWindow: BrowserWindow, app: Electron.App) {
// selection assistant
SelectionService.registerIpcHandler()
ipcMain.handle(IpcChannel.App_QuoteToMain, (_, text: string) => windowService.quoteToMainWindow(text))
}

View File

@@ -1,6 +1,7 @@
import { isWin } from '@main/constant'
import { locales } from '@main/utils/locales'
import { IpcChannel } from '@shared/IpcChannel'
import { FeedUrl } from '@shared/config/constant'
import { UpdateInfo } from 'builder-util-runtime'
import { app, BrowserWindow, dialog } from 'electron'
import logger from 'electron-log'
@@ -20,6 +21,7 @@ export default class AppUpdater {
autoUpdater.forceDevUpdateConfig = !app.isPackaged
autoUpdater.autoDownload = configManager.getAutoUpdate()
autoUpdater.autoInstallOnAppQuit = configManager.getAutoUpdate()
autoUpdater.setFeedURL(configManager.getFeedUrl())
// 检测下载错误
autoUpdater.on('error', (error) => {
@@ -62,6 +64,11 @@ export default class AppUpdater {
autoUpdater.autoInstallOnAppQuit = isActive
}
public setFeedUrl(feedUrl: FeedUrl) {
autoUpdater.setFeedURL(feedUrl)
configManager.setFeedUrl(feedUrl)
}
public async checkForUpdates() {
if (isWin && 'PORTABLE_EXECUTABLE_DIR' in process.env) {
return {

View File

@@ -295,10 +295,12 @@ class BackupManager {
async backupToWebdav(_: Electron.IpcMainInvokeEvent, data: string, webdavConfig: WebDavConfig) {
const filename = webdavConfig.fileName || 'cherry-studio.backup.zip'
const backupedFilePath = await this.backup(_, filename, data, undefined, webdavConfig.skipBackupFile)
const contentLength = (await fs.stat(backupedFilePath)).size
const webdavClient = new WebDav(webdavConfig)
try {
const result = await webdavClient.putFileContents(filename, fs.createReadStream(backupedFilePath), {
overwrite: true
overwrite: true,
contentLength
})
// 上传成功后删除本地备份文件
await fs.remove(backupedFilePath)

View File

@@ -1,4 +1,4 @@
import { defaultLanguage, ZOOM_SHORTCUTS } from '@shared/config/constant'
import { defaultLanguage, FeedUrl, ZOOM_SHORTCUTS } from '@shared/config/constant'
import { LanguageVarious, Shortcut, ThemeMode } from '@types'
import { app } from 'electron'
import Store from 'electron-store'
@@ -16,6 +16,7 @@ export enum ConfigKeys {
ClickTrayToShowQuickAssistant = 'clickTrayToShowQuickAssistant',
EnableQuickAssistant = 'enableQuickAssistant',
AutoUpdate = 'autoUpdate',
FeedUrl = 'feedUrl',
EnableDataCollection = 'enableDataCollection',
SelectionAssistantEnabled = 'selectionAssistantEnabled',
SelectionAssistantTriggerMode = 'selectionAssistantTriggerMode',
@@ -141,6 +142,14 @@ export class ConfigManager {
this.set(ConfigKeys.AutoUpdate, value)
}
getFeedUrl(): string {
return this.get<string>(ConfigKeys.FeedUrl, FeedUrl.PRODUCTION)
}
setFeedUrl(value: FeedUrl) {
this.set(ConfigKeys.FeedUrl, value)
}
getEnableDataCollection(): boolean {
return this.get<boolean>(ConfigKeys.EnableDataCollection, true)
}
@@ -151,7 +160,7 @@ export class ConfigManager {
// Selection Assistant: is enabled the selection assistant
getSelectionAssistantEnabled(): boolean {
return this.get<boolean>(ConfigKeys.SelectionAssistantEnabled, true)
return this.get<boolean>(ConfigKeys.SelectionAssistantEnabled, false)
}
setSelectionAssistantEnabled(value: boolean) {

View File

@@ -1,7 +1,9 @@
import fs from 'node:fs'
import fs from 'fs/promises'
export default class FileService {
public static async readFile(_: Electron.IpcMainInvokeEvent, path: string) {
return fs.readFileSync(path, 'utf8')
public static async readFile(_: Electron.IpcMainInvokeEvent, pathOrUrl: string, encoding?: BufferEncoding) {
const path = pathOrUrl.startsWith('file://') ? new URL(pathOrUrl) : pathOrUrl
if (encoding) return fs.readFile(path, { encoding })
return fs.readFile(path)
}
}

View File

@@ -812,8 +812,8 @@ export class SelectionService {
if (this.triggerMode === TriggerMode.Ctrlkey && this.isCtrlkey(data.vkCode)) {
return
}
//dont hide toolbar when shiftkey is pressed, because it's used for selection
if (this.isShiftkey(data.vkCode)) {
//dont hide toolbar when shiftkey or altkey is pressed, because it's used for selection
if (this.isShiftkey(data.vkCode) || this.isAltkey(data.vkCode)) {
return
}
@@ -841,8 +841,9 @@ export class SelectionService {
//ctrlkey pressed
if (this.lastCtrlkeyDownTime === 0) {
this.lastCtrlkeyDownTime = Date.now()
//add the mouse-wheel listener, detect if user is zooming in/out
//add the mouse-wheel&mouse-down listener, detect if user is zooming in/out or multi-selecting
this.selectionHook!.on('mouse-wheel', this.handleMouseWheelCtrlkeyMode)
this.selectionHook!.on('mouse-down', this.handleMouseDownCtrlkeyMode)
return
}
@@ -866,8 +867,9 @@ export class SelectionService {
*/
private handleKeyUpCtrlkeyMode = (data: KeyboardEventData) => {
if (!this.isCtrlkey(data.vkCode)) return
//remove the mouse-wheel listener
//remove the mouse-wheel&mouse-down listener
this.selectionHook!.off('mouse-wheel', this.handleMouseWheelCtrlkeyMode)
this.selectionHook!.off('mouse-down', this.handleMouseDownCtrlkeyMode)
this.lastCtrlkeyDownTime = 0
}
@@ -880,6 +882,15 @@ export class SelectionService {
this.lastCtrlkeyDownTime = -1
}
/**
* Handle mouse down events in ctrlkey trigger mode
* ignore CtrlKey pressing when mouse down is used
* because user is multi-selecting
*/
private handleMouseDownCtrlkeyMode = () => {
this.lastCtrlkeyDownTime = -1
}
//check if the key is ctrl key
private isCtrlkey(vkCode: number) {
return vkCode === 162 || vkCode === 163
@@ -890,6 +901,11 @@ export class SelectionService {
return vkCode === 160 || vkCode === 161
}
//check if the key is alt key
private isAltkey(vkCode: number) {
return vkCode === 164 || vkCode === 165
}
/**
* Create a preloaded action window for quick response
* Action windows handle specific operations on selected text

View File

@@ -1,7 +1,8 @@
import { WebDavConfig } from '@types'
import Logger from 'electron-log'
import Stream from 'stream'
import https from 'https'
import path from 'path'
import Stream from 'stream'
import {
BufferLike,
createClient,
@@ -15,7 +16,7 @@ export default class WebDav {
private webdavPath: string
constructor(params: WebDavConfig) {
this.webdavPath = params.webdavPath
this.webdavPath = params.webdavPath || '/'
this.instance = createClient(params.webdavHost, {
username: params.webdavUser,
@@ -51,7 +52,7 @@ export default class WebDav {
throw error
}
const remoteFilePath = `${this.webdavPath}/${filename}`
const remoteFilePath = path.posix.join(this.webdavPath, filename)
try {
return await this.instance.putFileContents(remoteFilePath, data, options)
@@ -66,7 +67,7 @@ export default class WebDav {
throw new Error('WebDAV client not initialized')
}
const remoteFilePath = `${this.webdavPath}/${filename}`
const remoteFilePath = path.posix.join(this.webdavPath, filename)
try {
return await this.instance.getFileContents(remoteFilePath, options)
@@ -120,7 +121,7 @@ export default class WebDav {
throw new Error('WebDAV client not initialized')
}
const remoteFilePath = `${this.webdavPath}/${filename}`
const remoteFilePath = path.posix.join(this.webdavPath, filename)
try {
return await this.instance.deleteFile(remoteFilePath)

View File

@@ -56,14 +56,14 @@ export class WindowService {
minHeight: 600,
show: false,
autoHideMenuBar: true,
transparent: isMac,
transparent: false,
vibrancy: 'sidebar',
visualEffectState: 'active',
titleBarStyle: 'hidden',
titleBarOverlay: nativeTheme.shouldUseDarkColors ? titleBarOverlayDark : titleBarOverlayLight,
backgroundColor: isMac ? undefined : nativeTheme.shouldUseDarkColors ? '#181818' : '#FFFFFF',
darkTheme: nativeTheme.shouldUseDarkColors,
trafficLightPosition: { x: 8, y: 12 },
trafficLightPosition: { x: 12, y: 12 },
...(isLinux ? { icon } : {}),
webPreferences: {
preload: join(__dirname, '../preload/index.js'),
@@ -544,6 +544,25 @@ export class WindowService {
public setPinMiniWindow(isPinned) {
this.isPinnedMiniWindow = isPinned
}
/**
* 引用文本到主窗口
* @param text 原始文本(未格式化)
*/
public quoteToMainWindow(text: string): void {
try {
this.showMainWindow()
const mainWindow = this.getMainWindow()
if (mainWindow && !mainWindow.isDestroyed()) {
setTimeout(() => {
mainWindow.webContents.send(IpcChannel.App_QuoteToMain, text)
}, 100)
}
} catch (error) {
Logger.error('Failed to quote to main window:', error as Error)
}
}
}
export const windowService = WindowService.getInstance()

View File

@@ -1,8 +1,12 @@
import path from 'node:path'
import { getConfigDir } from '@main/utils/file'
import { OAuthClientProvider } from '@modelcontextprotocol/sdk/client/auth'
import { OAuthClientInformation, OAuthClientInformationFull, OAuthTokens } from '@modelcontextprotocol/sdk/shared/auth'
import { OAuthClientProvider } from '@modelcontextprotocol/sdk/client/auth.js'
import {
OAuthClientInformation,
OAuthClientInformationFull,
OAuthTokens
} from '@modelcontextprotocol/sdk/shared/auth.js'
import Logger from 'electron-log'
import open from 'open'

View File

@@ -65,7 +65,7 @@ export function handleMcpProtocolUrl(url: URL) {
const mainWindow = windowService.getMainWindow()
if (mainWindow && !mainWindow.isDestroyed()) {
mainWindow.webContents.executeJavaScript("window.navigate('/settings/mcp')")
mainWindow.webContents.executeJavaScript("window.navigate('/mcp-servers')")
}
break
}

View File

@@ -1,5 +1,6 @@
import type { ExtractChunkData } from '@cherrystudio/embedjs-interfaces'
import { electronAPI } from '@electron-toolkit/preload'
import { FeedUrl } from '@shared/config/constant'
import { IpcChannel } from '@shared/IpcChannel'
import { FileType, KnowledgeBaseParams, KnowledgeItem, MCPServer, Shortcut, ThemeMode, WebDavConfig } from '@types'
import { contextBridge, ipcRenderer, OpenDialogOptions, shell, webUtils } from 'electron'
@@ -20,6 +21,7 @@ const api = {
setLaunchToTray: (isActive: boolean) => ipcRenderer.invoke(IpcChannel.App_SetLaunchToTray, isActive),
setTray: (isActive: boolean) => ipcRenderer.invoke(IpcChannel.App_SetTray, isActive),
setTrayOnClose: (isActive: boolean) => ipcRenderer.invoke(IpcChannel.App_SetTrayOnClose, isActive),
setFeedUrl: (feedUrl: FeedUrl) => ipcRenderer.invoke(IpcChannel.App_SetFeedUrl, feedUrl),
setTheme: (theme: ThemeMode) => ipcRenderer.invoke(IpcChannel.App_SetTheme, theme),
handleZoomFactor: (delta: number, reset: boolean = false) =>
ipcRenderer.invoke(IpcChannel.App_HandleZoomFactor, delta, reset),
@@ -84,7 +86,7 @@ const api = {
getPathForFile: (file: File) => webUtils.getPathForFile(file)
},
fs: {
read: (path: string) => ipcRenderer.invoke(IpcChannel.Fs_Read, path)
read: (pathOrUrl: string, encoding?: BufferEncoding) => ipcRenderer.invoke(IpcChannel.Fs_Read, pathOrUrl, encoding)
},
export: {
toWord: (markdown: string, fileName: string) => ipcRenderer.invoke(IpcChannel.Export_Word, markdown, fileName)
@@ -226,7 +228,8 @@ const api = {
closeActionWindow: () => ipcRenderer.invoke(IpcChannel.Selection_ActionWindowClose),
minimizeActionWindow: () => ipcRenderer.invoke(IpcChannel.Selection_ActionWindowMinimize),
pinActionWindow: (isPinned: boolean) => ipcRenderer.invoke(IpcChannel.Selection_ActionWindowPin, isPinned)
}
},
quoteToMainWindow: (text: string) => ipcRenderer.invoke(IpcChannel.App_QuoteToMain, text)
}
// Use `contextBridge` APIs to expose Electron APIs to

View File

@@ -5,7 +5,7 @@ import { Provider } from 'react-redux'
import { HashRouter, Route, Routes } from 'react-router-dom'
import { PersistGate } from 'redux-persist/integration/react'
import Sidebar from './components/app/Sidebar'
import MainSidebar from './components/app/MainSidebar'
import TopViewContainer from './components/TopView'
import AntdProvider from './context/AntdProvider'
import { CodeStyleProvider } from './context/CodeStyleProvider'
@@ -13,14 +13,9 @@ import { NotificationProvider } from './context/NotificationProvider'
import StyleSheetManager from './context/StyleSheetManager'
import { ThemeProvider } from './context/ThemeProvider'
import NavigationHandler from './handler/NavigationHandler'
import AgentsPage from './pages/agents/AgentsPage'
import AppsPage from './pages/apps/AppsPage'
import FilesPage from './pages/files/FilesPage'
import DiscoverPage from './pages/discover'
import HomePage from './pages/home/HomePage'
import KnowledgePage from './pages/knowledge/KnowledgePage'
import PaintingsRoutePage from './pages/paintings/PaintingsRoutePage'
import SettingsPage from './pages/settings/SettingsPage'
import TranslatePage from './pages/translate/TranslatePage'
function App(): React.ReactElement {
return (
@@ -34,16 +29,18 @@ function App(): React.ReactElement {
<TopViewContainer>
<HashRouter>
<NavigationHandler />
<Sidebar />
<MainSidebar />
<Routes>
<Route path="/" element={<HomePage />} />
<Route path="/agents" element={<AgentsPage />} />
<Route path="/paintings/*" element={<PaintingsRoutePage />} />
<Route path="/translate" element={<TranslatePage />} />
<Route path="/files" element={<FilesPage />} />
<Route path="/knowledge" element={<KnowledgePage />} />
<Route path="/apps" element={<AppsPage />} />
{/* <Route path="/agents" element={<AgentsPage />} /> */}
{/* <Route path="/paintings/*" element={<PaintingsRoutePage />} /> */}
{/* <Route path="/translate" element={<TranslatePage />} /> */}
{/* <Route path="/files" element={<FilesPage />} /> */}
{/* <Route path="/knowledge" element={<KnowledgePage />} /> */}
{/* <Route path="/apps" element={<AppsPage />} /> */}
{/* <Route path="/mcp-servers/*" element={<McpServersPage />} /> */}
<Route path="/settings/*" element={<SettingsPage />} />
<Route path="/discover/*" element={<DiscoverPage />} />
</Routes>
</HashRouter>
</TopViewContainer>

View File

@@ -25,7 +25,6 @@
}
.minapp-drawer {
max-width: calc(100vw - var(--sidebar-width));
.ant-drawer-content-wrapper {
box-shadow: none;
}
@@ -33,7 +32,7 @@
position: absolute;
-webkit-app-region: drag;
min-height: calc(var(--navbar-height) + 0.5px);
width: calc(100vw - var(--sidebar-width));
width: 100%;
margin-top: -0.5px;
border-bottom: none;
}

View File

@@ -29,7 +29,7 @@
--color-text-secondary: rgba(235, 235, 245, 0.7);
--color-icon: #ffffff99;
--color-icon-white: #ffffff;
--color-border: #ffffff19;
--color-border: #383838;
--color-border-soft: #ffffff10;
--color-border-mute: #ffffff05;
--color-error: #f44336;
@@ -44,8 +44,8 @@
--color-reference-text: #ffffff;
--color-reference-background: #0b0e12;
--color-list-item: #222;
--color-list-item-hover: #1e1e1e;
--color-list-item: rgba(255, 255, 255, 0.1);
--color-list-item-hover: rgba(255, 255, 255, 0.05);
--modal-background: #1f1f1f;
@@ -56,7 +56,7 @@
--navbar-background-mac: rgba(20, 20, 20, 0.55);
--navbar-background: #1f1f1f;
--navbar-height: 40px;
--navbar-height: 42px;
--sidebar-width: 50px;
--status-bar-height: 40px;
--input-bar-height: 100px;
@@ -71,7 +71,8 @@
--chat-background-assistant: #2c2c2c;
--chat-text-user: var(--color-black);
--list-item-border-radius: 20px;
--list-item-border-radius: 8px;
--border-width: 0.5px;
}
[theme-mode='light'] {
@@ -120,8 +121,8 @@
--color-reference-text: #000000;
--color-reference-background: #f1f7ff;
--color-list-item: #eee;
--color-list-item-hover: #f5f5f5;
--color-list-item: rgba(255, 255, 255, 0.9);
--color-list-item-hover: rgba(255, 255, 255, 0.5);
--modal-background: var(--color-white);
@@ -136,4 +137,6 @@
--chat-background-user: #95ec69;
--chat-background-assistant: #ffffff;
--chat-text-user: var(--color-text);
--border-width: 0.5px;
}

View File

@@ -1,8 +1,6 @@
#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);
}
.group-container {

View File

@@ -12,7 +12,7 @@
*::before,
*::after {
box-sizing: border-box;
margin: 0;
// margin: 0;
font-weight: normal;
}

View File

@@ -0,0 +1,146 @@
@import 'tailwindcss' source('../../../src');
@import 'tw-animate-css';
@custom-variant dark (&:is(.dark *));
/* 如需自定义:
1. 清晰地组织自定义 CSS 到相应的层中。
2. 基础样式(如全局重置、链接样式)放入 base 层;
3. 可复用的组件样式(如果仍使用 @apply 或原生 CSS 嵌套创建)放入 components 层;
4. 新的自定义工具类放入 utilities 层。
*/
:root {
--radius: 0.625rem;
--background: oklch(1 0 0);
--foreground: oklch(0.141 0.005 285.823);
--card: oklch(1 0 0);
--card-foreground: oklch(0.141 0.005 285.823);
--popover: oklch(1 0 0);
--popover-foreground: oklch(0.141 0.005 285.823);
--primary: oklch(0.21 0.006 285.885);
--primary-foreground: oklch(0.985 0 0);
--secondary: oklch(0.967 0.001 286.375);
--secondary-foreground: oklch(0.21 0.006 285.885);
--muted: oklch(0.967 0.001 286.375);
--muted-foreground: oklch(0.552 0.016 285.938);
--accent: oklch(0.967 0.001 286.375);
--accent-foreground: oklch(0.21 0.006 285.885);
--destructive: oklch(0.577 0.245 27.325);
--border: oklch(0.92 0.004 286.32);
--input: oklch(0.92 0.004 286.32);
--ring: oklch(0.705 0.015 286.067);
--chart-1: oklch(0.646 0.222 41.116);
--chart-2: oklch(0.6 0.118 184.704);
--chart-3: oklch(0.398 0.07 227.392);
--chart-4: oklch(0.828 0.189 84.429);
--chart-5: oklch(0.769 0.188 70.08);
--sidebar: oklch(0.985 0 0);
--sidebar-foreground: oklch(0.141 0.005 285.823);
--sidebar-primary: oklch(0.21 0.006 285.885);
--sidebar-primary-foreground: oklch(0.985 0 0);
--sidebar-accent: oklch(0.967 0.001 286.375);
--sidebar-accent-foreground: oklch(0.21 0.006 285.885);
--sidebar-border: oklch(0.92 0.004 286.32);
--sidebar-ring: oklch(0.705 0.015 286.067);
}
.dark {
--background: oklch(0.141 0.005 285.823);
--foreground: oklch(0.985 0 0);
--card: oklch(0.21 0.006 285.885);
--card-foreground: oklch(0.985 0 0);
--popover: oklch(0.21 0.006 285.885);
--popover-foreground: oklch(0.985 0 0);
--primary: oklch(0.92 0.004 286.32);
--primary-foreground: oklch(0.21 0.006 285.885);
--secondary: oklch(0.274 0.006 286.033);
--secondary-foreground: oklch(0.985 0 0);
--muted: oklch(0.274 0.006 286.033);
--muted-foreground: oklch(0.705 0.015 286.067);
--accent: oklch(0.274 0.006 286.033);
--accent-foreground: oklch(0.985 0 0);
--destructive: oklch(0.704 0.191 22.216);
--border: oklch(1 0 0 / 10%);
--input: oklch(1 0 0 / 15%);
--ring: oklch(0.552 0.016 285.938);
--chart-1: oklch(0.488 0.243 264.376);
--chart-2: oklch(0.696 0.17 162.48);
--chart-3: oklch(0.769 0.188 70.08);
--chart-4: oklch(0.627 0.265 303.9);
--chart-5: oklch(0.645 0.246 16.439);
--sidebar: oklch(0.21 0.006 285.885);
--sidebar-foreground: oklch(0.985 0 0);
--sidebar-primary: oklch(0.488 0.243 264.376);
--sidebar-primary-foreground: oklch(0.985 0 0);
--sidebar-accent: oklch(0.274 0.006 286.033);
--sidebar-accent-foreground: oklch(0.985 0 0);
--sidebar-border: oklch(1 0 0 / 10%);
--sidebar-ring: oklch(0.552 0.016 285.938);
}
@theme inline {
--color-background: var(--background);
--color-foreground: var(--foreground);
--color-card: var(--card);
--color-card-foreground: var(--card-foreground);
--color-popover: var(--popover);
--color-popover-foreground: var(--popover-foreground);
--color-primary: var(--primary);
--color-primary-foreground: var(--primary-foreground);
--color-secondary: var(--secondary);
--color-secondary-foreground: var(--secondary-foreground);
--color-muted: var(--muted);
--color-muted-foreground: var(--muted-foreground);
--color-accent: var(--accent);
--color-accent-foreground: var(--accent-foreground);
--color-destructive: var(--destructive);
--color-destructive-foreground: var(--destructive-foreground);
--color-border: var(--border);
--color-input: var(--input);
--color-ring: var(--ring);
--color-chart-1: var(--chart-1);
--color-chart-2: var(--chart-2);
--color-chart-3: var(--chart-3);
--color-chart-4: var(--chart-4);
--color-chart-5: var(--chart-5);
--radius-sm: calc(var(--radius) - 4px);
--radius-md: calc(var(--radius) - 2px);
--radius-lg: var(--radius);
--radius-xl: calc(var(--radius) + 4px);
--color-sidebar: var(--sidebar);
--color-sidebar-foreground: var(--sidebar-foreground);
--color-sidebar-primary: var(--sidebar-primary);
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
--color-sidebar-accent: var(--sidebar-accent);
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
--color-sidebar-border: var(--sidebar-border);
--color-sidebar-ring: var(--sidebar-ring);
--animate-marquee: marquee var(--duration) infinite linear;
--animate-marquee-vertical: marquee-vertical var(--duration) linear infinite;
@keyframes marquee {
from {
transform: translateX(0);
}
to {
transform: translateX(calc(-100% - var(--gap)));
}
}
@keyframes marquee-vertical {
from {
transform: translateY(0);
}
to {
transform: translateY(calc(-100% - var(--gap)));
}
}
}
@layer base {
* {
@apply border-border outline-ring/50;
}
body {
@apply bg-background text-foreground;
}
}

View File

@@ -134,26 +134,31 @@ const CodePreview = ({ children, language, setTools }: CodePreviewProps) => {
return () => cleanupTokenizers(callerId)
}, [callerId, cleanupTokenizers])
// 处理第二次开始的代码高亮
// 触发代码高亮
// - 进入视口后触发第一次高亮
// - 内容变化后触发之后的高亮
useEffect(() => {
if (prevCodeLengthRef.current > 0) {
setTimeout(highlightCode, 0)
}
}, [highlightCode])
// 视口检测逻辑,只处理第一次代码高亮
useEffect(() => {
const codeElement = codeContentRef.current
if (!codeElement || prevCodeLengthRef.current > 0) return
let isMounted = true
const observer = new IntersectionObserver((entries) => {
if (entries[0].isIntersecting && isMounted) {
setTimeout(highlightCode, 0)
observer.disconnect()
if (prevCodeLengthRef.current > 0) {
setTimeout(highlightCode, 0)
return
}
const codeElement = codeContentRef.current
if (!codeElement) return
const observer = new IntersectionObserver(
(entries) => {
if (entries[0].intersectionRatio > 0 && isMounted) {
setTimeout(highlightCode, 0)
observer.disconnect()
}
},
{
rootMargin: '50px 0px 50px 0px'
}
})
)
observer.observe(codeElement)

View File

@@ -1,4 +1,3 @@
import { EVENT_NAMES, EventEmitter } from '@renderer/services/EventService'
import { Dropdown } from 'antd'
import { useCallback, useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next'
@@ -12,7 +11,6 @@ interface ContextMenuProps {
const ContextMenu: React.FC<ContextMenuProps> = ({ children, onContextMenu }) => {
const { t } = useTranslation()
const [contextMenuPosition, setContextMenuPosition] = useState<{ x: number; y: number } | null>(null)
const [selectedQuoteText, setSelectedQuoteText] = useState<string>('')
const [selectedText, setSelectedText] = useState<string>('')
const handleContextMenu = useCallback(
@@ -20,12 +18,6 @@ const ContextMenu: React.FC<ContextMenuProps> = ({ children, onContextMenu }) =>
e.preventDefault()
const _selectedText = window.getSelection()?.toString()
if (_selectedText) {
const quotedText =
_selectedText
.split('\n')
.map((line) => `> ${line}`)
.join('\n') + '\n-------------'
setSelectedQuoteText(quotedText)
setContextMenuPosition({ x: e.clientX, y: e.clientY })
setSelectedText(_selectedText)
}
@@ -45,7 +37,7 @@ const ContextMenu: React.FC<ContextMenuProps> = ({ children, onContextMenu }) =>
}, [])
// 获取右键菜单项
const getContextMenuItems = (t: (key: string) => string, selectedQuoteText: string, selectedText: string) => [
const getContextMenuItems = (t: (key: string) => string, selectedText: string) => [
{
key: 'copy',
label: t('common.copy'),
@@ -66,8 +58,8 @@ const ContextMenu: React.FC<ContextMenuProps> = ({ children, onContextMenu }) =>
key: 'quote',
label: t('chat.message.quote'),
onClick: () => {
if (selectedQuoteText) {
EventEmitter.emit(EVENT_NAMES.QUOTE_TEXT, selectedQuoteText)
if (selectedText) {
window.api?.quoteToMainWindow(selectedText)
}
}
}
@@ -78,7 +70,7 @@ const ContextMenu: React.FC<ContextMenuProps> = ({ children, onContextMenu }) =>
{contextMenuPosition && (
<Dropdown
overlayStyle={{ position: 'fixed', left: contextMenuPosition.x, top: contextMenuPosition.y, zIndex: 1000 }}
menu={{ items: getContextMenuItems(t, selectedQuoteText, selectedText) }}
menu={{ items: getContextMenuItems(t, selectedText) }}
open={true}
trigger={['contextMenu']}>
<div />

View File

@@ -0,0 +1,141 @@
import {
CopyOutlined,
DownloadOutlined,
FileImageOutlined,
RotateLeftOutlined,
RotateRightOutlined,
SwapOutlined,
UndoOutlined,
ZoomInOutlined,
ZoomOutOutlined
} from '@ant-design/icons'
import { download } from '@renderer/utils/download'
import { Dropdown, Image as AntImage, ImageProps as AntImageProps, Space } from 'antd'
import { Base64 } from 'js-base64'
import mime from 'mime'
import React from 'react'
import { useTranslation } from 'react-i18next'
import styled from 'styled-components'
interface ImageViewerProps extends AntImageProps {
src: string
}
const ImageViewer: React.FC<ImageViewerProps> = ({ src, style, ...props }) => {
const { t } = useTranslation()
// 复制图片到剪贴板
const handleCopyImage = async (src: string) => {
try {
if (src.startsWith('data:')) {
// 处理 base64 格式的图片
const match = src.match(/^data:(image\/\w+);base64,(.+)$/)
if (!match) throw new Error('无效的 base64 图片格式')
const mimeType = match[1]
const byteArray = Base64.toUint8Array(match[2])
const blob = new Blob([byteArray], { type: mimeType })
await navigator.clipboard.write([new ClipboardItem({ [mimeType]: blob })])
} else if (src.startsWith('file://')) {
// 处理本地文件路径
const bytes = await window.api.fs.read(src)
const mimeType = mime.getType(src) || 'application/octet-stream'
const blob = new Blob([bytes], { type: mimeType })
await navigator.clipboard.write([
new ClipboardItem({
[mimeType]: blob
})
])
} else {
// 处理 URL 格式的图片
const response = await fetch(src)
const blob = await response.blob()
await navigator.clipboard.write([
new ClipboardItem({
[blob.type]: blob
})
])
}
window.message.success(t('message.copy.success'))
} catch (error) {
console.error('复制图片失败:', error)
window.message.error(t('message.copy.failed'))
}
}
const getContextMenuItems = (src: string) => {
return [
{
key: 'copy-url',
label: t('common.copy'),
icon: <CopyOutlined />,
onClick: () => {
navigator.clipboard.writeText(src)
window.message.success(t('message.copy.success'))
}
},
{
key: 'download',
label: t('common.download'),
icon: <DownloadOutlined />,
onClick: () => download(src)
},
{
key: 'copy-image',
label: t('code_block.preview.copy.image'),
icon: <FileImageOutlined />,
onClick: () => handleCopyImage(src)
}
]
}
return (
<Dropdown menu={{ items: getContextMenuItems(src) }} trigger={['contextMenu']}>
<AntImage
src={src}
style={style}
{...props}
preview={{
mask: typeof props.preview === 'object' ? props.preview.mask : false,
toolbarRender: (
_,
{
transform: { scale },
actions: { onFlipY, onFlipX, onRotateLeft, onRotateRight, onZoomOut, onZoomIn, onReset }
}
) => (
<ToolbarWrapper size={12} className="toolbar-wrapper">
<SwapOutlined rotate={90} onClick={onFlipY} />
<SwapOutlined onClick={onFlipX} />
<RotateLeftOutlined onClick={onRotateLeft} />
<RotateRightOutlined onClick={onRotateRight} />
<ZoomOutOutlined disabled={scale === 1} onClick={onZoomOut} />
<ZoomInOutlined disabled={scale === 50} onClick={onZoomIn} />
<UndoOutlined onClick={onReset} />
<CopyOutlined onClick={() => handleCopyImage(src)} />
<DownloadOutlined onClick={() => download(src)} />
</ToolbarWrapper>
)
}}
/>
</Dropdown>
)
}
const ToolbarWrapper = styled(Space)`
padding: 0px 24px;
color: #fff;
font-size: 20px;
background-color: rgba(0, 0, 0, 0.1);
border-radius: 100px;
.anticon {
padding: 12px;
cursor: pointer;
}
.anticon:hover {
opacity: 0.3;
}
`
export default ImageViewer

View File

@@ -395,10 +395,7 @@ const MinappPopupContainer: React.FC = () => {
height={'100%'}
maskClosable={false}
closeIcon={null}
style={{
marginLeft: 'var(--sidebar-width)',
backgroundColor: window.root.style.background
}}>
style={{ backgroundColor: window.root.style.background }}>
{!isReady && (
<EmptyView>
<Avatar
@@ -418,7 +415,7 @@ const TitleContainer = styled.div`
display: flex;
flex-direction: row;
align-items: center;
padding-left: ${isMac ? '20px' : '10px'};
padding-left: ${isMac ? '80px' : '10px'};
padding-right: 10px;
position: absolute;
top: 0;

View File

@@ -78,7 +78,7 @@ const WebviewContainer = memo(
)
const WebviewStyle: React.CSSProperties = {
width: 'calc(100vw - var(--sidebar-width))',
width: '100vw',
height: 'calc(100vh - var(--navbar-height))',
backgroundColor: 'var(--color-background)',
display: 'inline-flex'

View File

@@ -20,10 +20,16 @@ interface FileInfo {
name: string
}
const ObsidianProcessingMethod = {
APPEND: '1',
PREPEND: '2',
NEW_OR_OVERWRITE: '3'
} as const
interface PopupContainerProps {
title: string
obsidianTags: string | null
processingMethod: string | '3'
processingMethod: (typeof ObsidianProcessingMethod)[keyof typeof ObsidianProcessingMethod]
open: boolean
resolve: (success: boolean) => void
message?: Message
@@ -230,10 +236,10 @@ const PopupContainer: React.FC<PopupContainerProps> = ({
markdown = ''
}
let content = ''
if (state.processingMethod !== '3') {
if (state.processingMethod !== ObsidianProcessingMethod.NEW_OR_OVERWRITE) {
content = `\n---\n${markdown}`
} else {
content = `---\n\ntitle: ${state.title}\ncreated: ${state.createdAt}\nsource: ${state.source}\ntags: ${state.tags}\n---\n${markdown}`
content = `---\ntitle: ${state.title}\ncreated: ${state.createdAt}\nsource: ${state.source}\ntags: ${state.tags}\n---\n${markdown}`
}
if (content === '') {
window.message.error(i18n.t('chat.topics.export.obsidian_export_failed'))
@@ -280,9 +286,9 @@ const PopupContainer: React.FC<PopupContainerProps> = ({
const titleWithoutExt = fileName.endsWith('.md') ? fileName.substring(0, fileName.length - 3) : fileName
handleChange('title', titleWithoutExt)
setHasTitleBeenManuallyEdited(false)
handleChange('processingMethod', '1')
handleChange('processingMethod', ObsidianProcessingMethod.APPEND)
} else {
handleChange('processingMethod', '3')
handleChange('processingMethod', ObsidianProcessingMethod.NEW_OR_OVERWRITE)
if (!hasTitleBeenManuallyEdited) {
handleChange('title', title)
}
@@ -390,9 +396,15 @@ const PopupContainer: React.FC<PopupContainerProps> = ({
onChange={(value) => handleChange('processingMethod', value)}
placeholder={i18n.t('chat.topics.export.obsidian_operate_placeholder')}
allowClear>
<Option value="1">{i18n.t('chat.topics.export.obsidian_operate_append')}</Option>
<Option value="2">{i18n.t('chat.topics.export.obsidian_operate_prepend')}</Option>
<Option value="3">{i18n.t('chat.topics.export.obsidian_operate_new_or_overwrite')}</Option>
<Option value={ObsidianProcessingMethod.APPEND}>
{i18n.t('chat.topics.export.obsidian_operate_append')}
</Option>
<Option value={ObsidianProcessingMethod.PREPEND}>
{i18n.t('chat.topics.export.obsidian_operate_prepend')}
</Option>
<Option value={ObsidianProcessingMethod.NEW_OR_OVERWRITE}>
{i18n.t('chat.topics.export.obsidian_operate_new_or_overwrite')}
</Option>
</Select>
</Form.Item>
<Form.Item label={i18n.t('chat.topics.export.obsidian_reasoning')}>
@@ -403,4 +415,4 @@ const PopupContainer: React.FC<PopupContainerProps> = ({
)
}
export { PopupContainer }
export { ObsidianProcessingMethod, PopupContainer }

View File

@@ -1,11 +1,11 @@
import { PopupContainer } from '@renderer/components/ObsidianExportDialog'
import { ObsidianProcessingMethod, PopupContainer } from '@renderer/components/ObsidianExportDialog'
import { TopView } from '@renderer/components/TopView'
import type { Topic } from '@renderer/types'
import type { Message } from '@renderer/types/newMessage'
interface ObsidianExportOptions {
title: string
processingMethod: string | '3'
processingMethod: (typeof ObsidianProcessingMethod)[keyof typeof ObsidianProcessingMethod]
topic?: Topic
message?: Message
messages?: Message[]

View File

@@ -65,7 +65,7 @@ const TopViewContainer: React.FC<Props> = ({ children }) => {
const FullScreenContainer: React.FC<PropsWithChildren> = useCallback(({ children }) => {
return (
<Box flex={1} position="absolute" w="100%" h="100%">
<Box flex={1} position="absolute" w="100%" h="100%" className="topview-fullscreen-container">
<Box position="absolute" w="100%" h="100%" onClick={onPop} />
{children}
</Box>

View File

@@ -14,9 +14,9 @@ interface BackupFile {
interface WebdavConfig {
webdavHost: string
webdavUser: string
webdavPass: string
webdavPath: string
webdavUser?: string
webdavPass?: string
webdavPath?: string
}
interface WebdavBackupManagerProps {
@@ -47,7 +47,7 @@ export function WebdavBackupManager({ visible, onClose, webdavConfig, restoreMet
const { webdavHost, webdavUser, webdavPass, webdavPath } = webdavConfig
const fetchBackupFiles = useCallback(async () => {
if (!webdavHost || !webdavUser || !webdavPass || !webdavPath) {
if (!webdavHost) {
message.error(t('message.error.invalid.webdav'))
return
}
@@ -93,7 +93,7 @@ export function WebdavBackupManager({ visible, onClose, webdavConfig, restoreMet
return
}
if (!webdavHost || !webdavUser || !webdavPass || !webdavPath) {
if (!webdavHost) {
message.error(t('message.error.invalid.webdav'))
return
}
@@ -132,7 +132,7 @@ export function WebdavBackupManager({ visible, onClose, webdavConfig, restoreMet
}
const handleDeleteSingle = async (fileName: string) => {
if (!webdavHost || !webdavUser || !webdavPass || !webdavPath) {
if (!webdavHost) {
message.error(t('message.error.invalid.webdav'))
return
}
@@ -165,7 +165,7 @@ export function WebdavBackupManager({ visible, onClose, webdavConfig, restoreMet
}
const handleRestore = async (fileName: string) => {
if (!webdavHost || !webdavUser || !webdavPass || !webdavPath) {
if (!webdavHost) {
message.error(t('message.error.invalid.webdav'))
return
}

View File

@@ -123,7 +123,7 @@ export function useWebdavRestoreModal({
const [backupFiles, setBackupFiles] = useState<BackupFile[]>([])
const showRestoreModal = useCallback(async () => {
if (!webdavHost || !webdavUser || !webdavPass || !webdavPath) {
if (!webdavHost) {
window.message.error({ content: t('message.error.invalid.webdav'), key: 'webdav-error' })
return
}
@@ -146,7 +146,7 @@ export function useWebdavRestoreModal({
}, [webdavHost, webdavUser, webdavPass, webdavPath, t])
const handleRestore = useCallback(async () => {
if (!selectedFile || !webdavHost || !webdavUser || !webdavPass || !webdavPath) {
if (!selectedFile || !webdavHost) {
window.message.error({
content: !selectedFile ? t('message.error.no.file.selected') : t('message.error.invalid.webdav'),
key: 'restore-error'
@@ -170,7 +170,7 @@ export function useWebdavRestoreModal({
}
}
})
}, [selectedFile, webdavHost, webdavUser, webdavPass, webdavPath, t, restoreMethod])
}, [selectedFile, webdavHost, t, restoreMethod])
const handleCancel = () => {
setIsRestoreModalVisible(false)

View File

@@ -0,0 +1,90 @@
import { isMac } from '@renderer/config/constant'
import { EVENT_NAMES, EventEmitter } from '@renderer/services/EventService'
import { Tooltip } from 'antd'
import { t } from 'i18next'
import { MessageSquareDiff, Search } from 'lucide-react'
import { FC } from 'react'
import styled from 'styled-components'
import SearchPopup from '../Popups/SearchPopup'
interface Props {}
const HeaderNavbar: FC<Props> = () => {
return (
<Container>
<div>
{!isMac && (
<Tooltip title={t('chat.assistant.search.placeholder')} mouseEnterDelay={0.8}>
<NarrowIcon onClick={() => SearchPopup.show()}>
<Search size={18} />
</NarrowIcon>
</Tooltip>
)}
</div>
<Tooltip title={t('settings.shortcuts.new_topic')} mouseEnterDelay={0.8}>
<NavbarIcon onClick={() => EventEmitter.emit(EVENT_NAMES.ADD_NEW_TOPIC)} style={{ marginRight: 5 }}>
<MessageSquareDiff size={18} />
</NavbarIcon>
</Tooltip>
</Container>
)
}
const Container = styled.div`
display: flex;
width: var(--assistant-width);
flex-direction: row;
justify-content: space-between;
align-items: center;
padding: 0;
padding-left: var(--sidebar-width);
height: var(--navbar-height);
min-height: var(--navbar-height);
background-color: transparent;
-webkit-app-region: drag;
padding-left: ${isMac ? '75px' : '0'};
`
export const NavbarIcon = styled.div`
-webkit-app-region: none;
border-radius: 8px;
height: 30px;
padding: 0 7px;
display: flex;
flex-direction: row;
justify-content: center;
align-items: center;
transition: all 0.2s ease-in-out;
-webkit-app-region: no-drag;
cursor: pointer;
.iconfont {
font-size: 18px;
color: var(--color-icon);
&.icon-a-addchat {
font-size: 20px;
}
&.icon-a-darkmode {
font-size: 20px;
}
&.icon-appstore {
font-size: 20px;
}
}
.anticon {
color: var(--color-icon);
font-size: 16px;
}
&:hover {
background-color: var(--color-background-mute);
color: var(--color-icon-white);
}
`
const NarrowIcon = styled(NavbarIcon)`
@media (max-width: 1000px) {
display: none;
}
`
export default HeaderNavbar

View File

@@ -0,0 +1,379 @@
import EmojiAvatar from '@renderer/components/Avatar/EmojiAvatar'
import { UserAvatar } from '@renderer/config/env'
import { useTheme } from '@renderer/context/ThemeProvider'
import { useAssistants } from '@renderer/hooks/useAssistant'
import useAvatar from '@renderer/hooks/useAvatar'
import { useChat } from '@renderer/hooks/useChat'
import { useSettings } from '@renderer/hooks/useSettings'
import { EVENT_NAMES, EventEmitter } from '@renderer/services/EventService'
import NavigationService from '@renderer/services/NavigationService'
import { ThemeMode } from '@renderer/types'
import { isEmoji } from '@renderer/utils'
import { Avatar, Tooltip } from 'antd'
import {
Blocks,
Bot,
ChevronDown,
ChevronRight,
Compass,
FileSearch,
Folder,
Languages,
MessageSquare,
Moon,
Palette,
SquareTerminal,
Sun,
SunMoon
} from 'lucide-react'
import { FC, useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { useLocation, useNavigate } from 'react-router-dom'
import styled from 'styled-components'
import Tabs from '../../pages/home/Tabs'
import MainNavbar from './MainNavbar'
type Tab = 'assistants' | 'topic' | 'settings'
const MainSidebar: FC = () => {
const { assistants } = useAssistants()
const navigate = useNavigate()
const [tab, setTab] = useState<Tab>('assistants')
const avatar = useAvatar()
const { userName, defaultPaintingProvider } = useSettings()
const { t } = useTranslation()
const { theme, settedTheme, toggleTheme } = useTheme()
const [isAppMenuExpanded, setIsAppMenuExpanded] = useState(false)
const location = useLocation()
const { pathname } = location
const { activeAssistant, activeTopic, setActiveAssistant, setActiveTopic } = useChat()
const { showAssistants, showTopics, topicPosition } = useSettings()
useEffect(() => {
NavigationService.setNavigate(navigate)
}, [navigate])
useEffect(() => {
const unsubscribe = EventEmitter.on(EVENT_NAMES.SHOW_TOPIC_SIDEBAR, () => setTab('topic'))
return () => unsubscribe()
}, [])
useEffect(() => {
const unsubscribe = EventEmitter.on(EVENT_NAMES.SWITCH_ASSISTANT, (assistantId: string) => {
const newAssistant = assistants.find((a) => a.id === assistantId)
if (newAssistant) {
setActiveAssistant(newAssistant)
}
})
return () => {
unsubscribe()
}
}, [assistants, setActiveAssistant])
useEffect(() => {
const canMinimize = !showAssistants && !showTopics
window.api.window.setMinimumSize(canMinimize ? 520 : 1080, 600)
return () => {
window.api.window.resetMinimumSize()
}
}, [showAssistants, showTopics, topicPosition])
useEffect(() => {
setIsAppMenuExpanded(false)
}, [activeAssistant.id, activeTopic.id])
const onAvatarClick = () => {
navigate('/settings/provider')
}
const onChageTheme = (e: React.MouseEvent<HTMLDivElement>) => {
e.stopPropagation()
toggleTheme()
}
const appMenuItems = [
// { icon: <Sparkle size={18} className="icon" />, text: t('agents.title'), path: '/agents' },
{ icon: <Compass size={18} className="icon" />, text: t('discover.title'), path: '/discover' },
{ icon: <Languages size={18} className="icon" />, text: t('translate.title'), path: '/translate' },
{
icon: <Palette size={18} className="icon" />,
text: t('paintings.title'),
path: `/paintings/${defaultPaintingProvider}`
},
// { icon: <LayoutGrid size={18} className="icon" />, text: t('minapp.title'), path: '/apps' },
{ icon: <FileSearch size={18} className="icon" />, text: t('knowledge.title'), path: '/knowledge' },
{ icon: <SquareTerminal size={18} className="icon" />, text: t('common.mcp'), path: '/mcp-servers' },
{ icon: <Folder size={18} className="icon" />, text: t('files.title'), path: '/files' }
]
const isRoutes = (path: string): boolean => pathname.startsWith(path)
const onChageTab = (tab: Tab) => {
setTab(tab)
setIsAppMenuExpanded(false)
}
if (!showAssistants) {
return null
}
if (location.pathname !== '/') {
return null
}
return (
<Container id="main-sidebar">
<MainNavbar />
<MainMenu>
<MainMenuItem
active={tab === 'assistants' && location.pathname === '/'}
onClick={() => onChageTab('assistants')}>
<MainMenuItemLeft>
<MainMenuItemIcon>
<Bot size={18} />
</MainMenuItemIcon>
<MainMenuItemText>{t('assistants.title')}</MainMenuItemText>
</MainMenuItemLeft>
{tab === 'topic' && (
<MainMenuItemRight>
<MainMenuItemRightText>{activeAssistant.name}</MainMenuItemRightText>
</MainMenuItemRight>
)}
</MainMenuItem>
<MainMenuItem active={tab === 'topic' && location.pathname === '/'} onClick={() => onChageTab('topic')}>
<MainMenuItemLeft>
<MainMenuItemIcon>
<MessageSquare size={18} />
</MainMenuItemIcon>
<MainMenuItemText>{t('common.topics')}</MainMenuItemText>
</MainMenuItemLeft>
</MainMenuItem>
<MainMenuItem
style={{ opacity: isAppMenuExpanded ? 0.5 : 1 }}
onClick={() => setIsAppMenuExpanded(!isAppMenuExpanded)}>
<MainMenuItemLeft>
<MainMenuItemIcon>
<Blocks size={19} className="icon" />
</MainMenuItemIcon>
<MainMenuItemText>{t('common.apps')}</MainMenuItemText>
</MainMenuItemLeft>
<MainMenuItemRight>
{isAppMenuExpanded ? (
<ChevronDown size={18} color="var(--color-text-3)" />
) : (
<ChevronRight size={18} color="var(--color-text-3)" />
)}
</MainMenuItemRight>
</MainMenuItem>
{isAppMenuExpanded && (
<SubMenu>
{appMenuItems.map((item) => (
<MainMenuItem key={item.path} active={isRoutes(item.path)} onClick={() => navigate(item.path)}>
<MainMenuItemLeft>
<MainMenuItemIcon>{item.icon}</MainMenuItemIcon>
<MainMenuItemText>{item.text}</MainMenuItemText>
</MainMenuItemLeft>
</MainMenuItem>
))}
</SubMenu>
)}
</MainMenu>
<Tabs
tab={tab}
activeAssistant={activeAssistant}
activeTopic={activeTopic}
setActiveAssistant={setActiveAssistant}
setActiveTopic={setActiveTopic}
/>
<UserMenu onClick={onAvatarClick}>
<UserMenuLeft>
{isEmoji(avatar) ? (
<EmojiAvatar className="sidebar-avatar" size={31} fontSize={18}>
{avatar}
</EmojiAvatar>
) : (
<AvatarImg src={avatar || UserAvatar} draggable={false} className="nodrag" />
)}
<UserMenuText>{userName}</UserMenuText>
</UserMenuLeft>
<Tooltip
title={t('settings.theme.title') + ': ' + t(`settings.theme.${settedTheme}`)}
mouseEnterDelay={0.8}
placement="right">
<Icon theme={theme} onClick={onChageTheme}>
{settedTheme === ThemeMode.dark ? (
<Moon size={18} className="icon" />
) : settedTheme === ThemeMode.light ? (
<Sun size={18} className="icon" />
) : (
<SunMoon size={18} className="icon" />
)}
</Icon>
</Tooltip>
</UserMenu>
</Container>
)
}
const Container = styled.div`
display: flex;
flex-direction: column;
width: var(--assistant-width);
max-width: var(--assistant-width);
border-right: 0.5px solid var(--color-border);
`
const MainMenu = styled.div`
display: flex;
flex-direction: column;
padding: 10px;
padding-top: 0;
gap: 8px;
`
const MainMenuItem = styled.div<{ active?: boolean }>`
display: flex;
justify-content: space-between;
align-items: center;
gap: 5px;
background-color: ${({ active }) => (active ? 'var(--color-list-item)' : 'transparent')};
padding: 5px 10px;
border-radius: 5px;
border-radius: 8px;
&:hover {
background-color: ${({ active }) => (active ? 'var(--color-list-item)' : 'var(--color-list-item-hover)')};
}
`
const MainMenuItemLeft = styled.div`
display: flex;
align-items: center;
gap: 5px;
`
const MainMenuItemRight = styled.div`
display: flex;
align-items: center;
gap: 5px;
`
const MainMenuItemRightText = styled.div`
font-size: 12px;
color: var(--color-text-3);
`
const MainMenuItemIcon = styled.div`
display: flex;
align-items: center;
justify-content: center;
width: 20px;
height: 20px;
`
const MainMenuItemText = styled.div`
font-size: 14px;
font-weight: 500;
`
const UserMenu = styled.div`
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
margin: 10px;
margin-bottom: 10px;
padding: 4px 8px;
gap: 5px;
border-radius: 8px;
&:hover {
background-color: var(--color-list-item);
}
`
const UserMenuLeft = styled.div`
display: flex;
flex-direction: row;
align-items: center;
gap: 8px;
`
const AvatarImg = styled(Avatar)`
width: 28px;
height: 28px;
background-color: var(--color-background-soft);
border: none;
cursor: pointer;
`
const UserMenuText = styled.div`
font-size: 14px;
font-weight: 500;
`
const Icon = styled.div<{ theme: string }>`
width: 30px;
height: 30px;
display: flex;
justify-content: center;
align-items: center;
border-radius: 50%;
box-sizing: border-box;
-webkit-app-region: none;
border: 0.5px solid transparent;
&:hover {
background-color: ${({ theme }) => (theme === 'dark' ? 'var(--color-black)' : 'var(--color-white)')};
opacity: 0.8;
cursor: pointer;
.icon {
color: var(--color-icon-white);
}
}
&.active {
background-color: ${({ theme }) => (theme === 'dark' ? 'var(--color-black)' : 'var(--color-white)')};
border: 0.5px solid var(--color-border);
.icon {
color: var(--color-primary);
}
}
@keyframes borderBreath {
0% {
opacity: 0.1;
}
50% {
opacity: 1;
}
100% {
opacity: 0.1;
}
}
&.opened-minapp {
position: relative;
}
&.opened-minapp::after {
content: '';
position: absolute;
width: 100%;
height: 100%;
top: 0;
left: 0;
border-radius: inherit;
opacity: 0.3;
border: 0.5px solid var(--color-primary);
}
`
const SubMenu = styled.div`
display: flex;
flex-direction: column;
gap: 5px;
`
export default MainSidebar

View File

@@ -1,24 +1,16 @@
import { isLinux, isMac, isWindows } from '@renderer/config/constant'
import { useFullscreen } from '@renderer/hooks/useFullscreen'
import useNavBackgroundColor from '@renderer/hooks/useNavBackgroundColor'
import { Button } from 'antd'
import { ChevronDown, X } from 'lucide-react'
import type { FC, PropsWithChildren } from 'react'
import type { HTMLAttributes } from 'react'
import { useNavigate } from 'react-router-dom'
import styled from 'styled-components'
type Props = PropsWithChildren & HTMLAttributes<HTMLDivElement>
export const Navbar: FC<Props> = ({ children, ...props }) => {
const backgroundColor = useNavBackgroundColor()
return (
<NavbarContainer {...props} style={{ backgroundColor }}>
{children}
</NavbarContainer>
)
}
export const NavbarLeft: FC<Props> = ({ children, ...props }) => {
return <NavbarLeftContainer {...props}>{children}</NavbarLeftContainer>
return <NavbarContainer {...props}>{children}</NavbarContainer>
}
export const NavbarCenter: FC<Props> = ({ children, ...props }) => {
@@ -34,34 +26,56 @@ export const NavbarRight: FC<Props> = ({ children, ...props }) => {
)
}
export const NavbarMain: FC<Props> = ({ children, ...props }) => {
const isFullscreen = useFullscreen()
return (
<NavbarMainContainer {...props} $isFullscreen={isFullscreen}>
<CloseIconWindows />
{children}
<CloseIconMac />
</NavbarMainContainer>
)
}
const CloseIconMac = () => {
const navigate = useNavigate()
if (!isMac) {
return null
}
return <Button type="text" icon={<X size={18} />} onClick={() => navigate('/')} className="nodrag" />
}
const CloseIconWindows = () => {
const navigate = useNavigate()
if (isMac) {
return null
}
return (
<Button
size="small"
type="default"
shape="circle"
icon={<ChevronDown size={16} />}
onClick={() => navigate('/')}
className="nodrag"
style={{ marginRight: 5 }}
/>
)
}
const NavbarContainer = styled.div`
min-width: 100%;
display: flex;
flex-direction: row;
min-height: var(--navbar-height);
max-height: var(--navbar-height);
margin-left: ${isMac ? 'calc(var(--sidebar-width) * -1)' : 0};
padding-left: ${isMac ? 'var(--sidebar-width)' : 0};
-webkit-app-region: drag;
`
const NavbarLeftContainer = styled.div`
min-width: var(--assistants-width);
padding: 0 10px;
display: flex;
flex-direction: row;
align-items: center;
font-weight: bold;
color: var(--color-text-1);
`
const NavbarCenterContainer = styled.div`
flex: 1;
display: flex;
align-items: center;
padding: 0 ${isMac ? '20px' : 0};
font-weight: bold;
color: var(--color-text-1);
background-color: var(--color-background);
`
const NavbarRightContainer = styled.div<{ $isFullscreen: boolean }>`
@@ -72,3 +86,32 @@ const NavbarRightContainer = styled.div<{ $isFullscreen: boolean }>`
padding-right: ${({ $isFullscreen }) => ($isFullscreen ? '12px' : isWindows ? '140px' : isLinux ? '120px' : '12px')};
justify-content: flex-end;
`
const NavbarMainContainer = styled.div<{ $isFullscreen: boolean }>`
flex: 1;
display: flex;
flex-direction: row;
align-items: center;
height: var(--navbar-height);
max-height: var(--navbar-height);
min-height: var(--navbar-height);
justify-content: space-between;
padding-left: ${isMac ? '70px' : '10px'};
font-weight: bold;
color: var(--color-text-1);
padding-right: ${({ $isFullscreen }) => ($isFullscreen ? '12px' : isWindows ? '140px' : isLinux ? '120px' : '12px')};
-webkit-app-region: drag;
`
const NavbarCenterContainer = styled.div`
flex: 1;
display: flex;
align-items: center;
height: var(--navbar-height);
max-height: var(--navbar-height);
min-height: var(--navbar-height);
padding: 0 8px;
font-weight: bold;
justify-content: space-between;
color: var(--color-text-1);
`

View File

@@ -16,6 +16,7 @@ import type { MenuProps } from 'antd'
import { Avatar, Dropdown, Tooltip } from 'antd'
import {
CircleHelp,
Compass,
FileSearch,
Folder,
Languages,
@@ -155,7 +156,8 @@ const MainMenus: FC = () => {
translate: <Languages size={18} className="icon" />,
minapp: <LayoutGrid size={18} className="icon" />,
knowledge: <FileSearch size={18} className="icon" />,
files: <Folder size={17} className="icon" />
files: <Folder size={17} className="icon" />,
discover: <Compass size={18} className="icon" />
}
const pathMap = {
@@ -165,7 +167,8 @@ const MainMenus: FC = () => {
translate: '/translate',
minapp: '/apps',
knowledge: '/knowledge',
files: '/files'
files: '/files',
discover: '/discover'
}
return sidebarIcons.visible.map((icon) => {

View File

@@ -395,6 +395,37 @@ export function getModelLogo(modelId: string) {
}
export const SYSTEM_MODELS: Record<string, Model[]> = {
defaultModel: [
{
// 默认助手模型
id: 'deepseek-ai/DeepSeek-V3',
name: 'deepseek-ai/DeepSeek-V3',
provider: 'silicon',
group: 'deepseek-ai'
},
{
// 默认话题命名模型
id: 'Qwen/Qwen3-8B',
name: 'Qwen/Qwen3-8B',
provider: 'silicon',
group: 'Qwen'
},
{
// 默认翻译模型
id: 'deepseek-ai/DeepSeek-V3',
name: 'deepseek-ai/DeepSeek-V3',
provider: 'silicon',
group: 'deepseek-ai'
},
{
// 默认快捷助手模型
id: 'deepseek-ai/DeepSeek-V3',
name: 'deepseek-ai/DeepSeek-V3',
provider: 'silicon',
group: 'deepseek-ai'
}
],
aihubmix: [
{
id: 'gpt-4o',
@@ -600,17 +631,17 @@ export const SYSTEM_MODELS: Record<string, Model[]> = {
name: 'Qwen2.5-7B-Instruct',
group: 'Qwen'
},
{
id: 'meta-llama/Llama-3.3-70B-Instruct',
name: 'meta-llama/Llama-3.3-70B-Instruct',
provider: 'silicon',
group: 'meta-llama'
},
{
id: 'BAAI/bge-m3',
name: 'BAAI/bge-m3',
provider: 'silicon',
group: 'BAAI'
},
{
id: 'Qwen/Qwen3-8B',
name: 'Qwen/Qwen3-8B',
provider: 'silicon',
group: 'Qwen'
}
],
ppio: [
@@ -1709,24 +1740,6 @@ export const SYSTEM_MODELS: Record<string, Model[]> = {
name: 'ERNIE-Speed-128K',
group: '免费模型'
},
{
id: 'THUDM/glm-4-9b-chat',
provider: 'dmxapi',
name: 'THUDM/glm-4-9b-chat',
group: '免费模型'
},
{
id: 'glm-4-flash',
provider: 'dmxapi',
name: 'glm-4-flash',
group: '免费模型'
},
{
id: 'hunyuan-lite',
provider: 'dmxapi',
name: 'hunyuan-lite',
group: '免费模型'
},
{
id: 'gpt-4o',
provider: 'dmxapi',
@@ -2631,7 +2644,8 @@ export function groupQwenModels(models: Model[]): Record<string, Model[]> {
export const THINKING_TOKEN_MAP: Record<string, { min: number; max: number }> = {
// Gemini models
'gemini-.*$': { min: 0, max: 24576 },
'gemini-.*-flash.*$': { min: 0, max: 24576 },
'gemini-.*-pro.*$': { min: 128, max: 32768 },
// Qwen models
'qwen-plus-.*$': { min: 0, max: 38912 },

View File

@@ -47,7 +47,7 @@ As [role name], with [list skills], strictly adhering to [list constraints], usi
`
export const SUMMARIZE_PROMPT =
"You are an assistant skilled in conversation. You need to summarize the user's conversation into a title within 10 words. The language of the title should be consistent with the user's primary language. Do not use punctuation marks or other special symbols"
"You are an assistant skilled in conversation. You need to summarize the user's conversation into a title within 10 words. The language of the title should be consistent with the user's primary language. Do not use punctuation marks, markdown language markers, or other special symbols"
// https://github.com/ItzCrazyKns/Perplexica/blob/master/src/lib/prompts/webSearch.ts
export const SEARCH_SUMMARY_PROMPT = `

View File

@@ -169,7 +169,7 @@ export const PROVIDER_CONFIG = {
official: 'https://www.siliconflow.cn',
apiKey: 'https://cloud.siliconflow.cn/i/d1nTBKXU',
docs: 'https://docs.siliconflow.cn/',
models: 'https://docs.siliconflow.cn/docs/model-names'
models: 'https://cloud.siliconflow.cn/models'
}
},
'gitee-ai': {

View File

@@ -38,8 +38,22 @@ export const ThemeProvider: React.FC<ThemeProviderProps> = ({ children }) => {
setSettedTheme(nextTheme || ThemeMode.system)
}
const tailwindThemeChange = (theme) => {
const root = window.document.documentElement
root.classList.remove('light', 'dark')
root.classList.add(theme)
}
useEffect(() => {
window.api?.setTheme(settedTheme || actualTheme)
}, [settedTheme, actualTheme])
useEffect(() => {
document.body.setAttribute('theme-mode', settedTheme)
tailwindThemeChange(settedTheme)
}, [settedTheme])
useEffect(() => {
// Set initial theme and OS attributes on body
document.body.setAttribute('os', isMac ? 'mac' : 'windows')
document.body.setAttribute('theme-mode', actualTheme)

View File

@@ -281,7 +281,6 @@ export async function upgradeToV7(tx: Transaction): Promise<void> {
modelId: oldMessage.modelId,
model: oldMessage.model,
type: oldMessage.type === 'clear' ? 'clear' : undefined,
isPreset: oldMessage.isPreset,
useful: oldMessage.useful,
askId: oldMessage.askId,
mentions: oldMessage.mentions,

View File

@@ -1,3 +1,4 @@
import './assets/styles/tailwind.css'
import './assets/styles/index.scss'
import '@ant-design/v5-patch-for-react-19'

View File

@@ -0,0 +1,19 @@
import * as React from "react"
const MOBILE_BREAKPOINT = 768
export function useIsMobile() {
const [isMobile, setIsMobile] = React.useState<boolean | undefined>(undefined)
React.useEffect(() => {
const mql = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`)
const onChange = () => {
setIsMobile(window.innerWidth < MOBILE_BREAKPOINT)
}
mql.addEventListener("change", onChange)
setIsMobile(window.innerWidth < MOBILE_BREAKPOINT)
return () => mql.removeEventListener("change", onChange)
}, [])
return !!isMobile
}

View File

@@ -0,0 +1,47 @@
import { EVENT_NAMES, EventEmitter } from '@renderer/services/EventService'
import { useAppDispatch, useAppSelector } from '@renderer/store'
import { setActiveAssistant, setActiveTopic } from '@renderer/store/runtime'
import { loadTopicMessagesThunk } from '@renderer/store/thunk/messageThunk'
import { Assistant } from '@renderer/types'
import { Topic } from '@renderer/types'
import { useEffect } from 'react'
import { useAssistants } from './useAssistant'
import { useSettings } from './useSettings'
export const useChat = () => {
const { assistants } = useAssistants()
const activeAssistant = useAppSelector((state) => state.runtime.chat.activeAssistant) || assistants[0]
const activeTopic = useAppSelector((state) => state.runtime.chat.activeTopic) || activeAssistant?.topics[0]!
const { clickAssistantToShowTopic } = useSettings()
const dispatch = useAppDispatch()
useEffect(() => {
if (activeTopic) {
dispatch(loadTopicMessagesThunk(activeTopic.id))
EventEmitter.emit(EVENT_NAMES.CHANGE_TOPIC, activeTopic)
}
}, [activeTopic, dispatch])
useEffect(() => {
const firstTopic = activeAssistant.topics[0]
firstTopic && dispatch(setActiveTopic(firstTopic))
}, [activeAssistant, dispatch])
useEffect(() => {
if (clickAssistantToShowTopic) {
EventEmitter.emit(EVENT_NAMES.SHOW_TOPIC_SIDEBAR)
}
}, [clickAssistantToShowTopic, activeAssistant])
return {
activeAssistant,
activeTopic,
setActiveAssistant: (assistant: Assistant) => {
dispatch(setActiveAssistant(assistant))
},
setActiveTopic: (topic: Topic) => {
dispatch(setActiveTopic(topic))
}
}
}

View File

@@ -4,6 +4,7 @@ import {
SendMessageShortcut,
setAssistantIconType,
setAutoCheckUpdate as _setAutoCheckUpdate,
setEarlyAccess as _setEarlyAccess,
setLaunchOnBoot,
setLaunchToTray,
setPinTopicsToTop,
@@ -19,6 +20,7 @@ import {
setWindowStyle
} from '@renderer/store/settings'
import { SidebarIcon, ThemeMode, TranslateLanguageVarious } from '@renderer/types'
import { FeedUrl } from '@shared/config/constant'
export function useSettings() {
const settings = useAppSelector((state) => state.settings)
@@ -58,6 +60,11 @@ export function useSettings() {
window.api.setAutoUpdate(isAutoUpdate)
},
setEarlyAccess(isEarlyAccess: boolean) {
dispatch(_setEarlyAccess(isEarlyAccess))
window.api.setFeedUrl(isEarlyAccess ? FeedUrl.EARLY_ACCESS : FeedUrl.PRODUCTION)
},
setTheme(theme: ThemeMode) {
dispatch(setTheme(theme))
},

View File

@@ -1,3 +1,5 @@
import { useAppDispatch, useAppSelector } from '@renderer/store'
import { setTagsOrder, updateAssistants } from '@renderer/store/assistants'
import { flatMap, groupBy, uniq } from 'lodash'
import { useCallback, useMemo } from 'react'
import { useTranslation } from 'react-i18next'
@@ -11,17 +13,48 @@ import { useAssistants } from './useAssistant'
export const useTags = () => {
const { assistants } = useAssistants()
const { t } = useTranslation()
const dispatch = useAppDispatch()
const savedTagsOrder = useAppSelector((state) => state.assistants.tagsOrder || [])
// 计算所有标签
const allTags = useMemo(() => {
return uniq(flatMap(assistants, (assistant) => assistant.tags || []))
}, [assistants])
const tags = uniq(flatMap(assistants, (assistant) => assistant.tags || []))
if (savedTagsOrder.length > 0) {
return [
...savedTagsOrder.filter((tag) => tags.includes(tag)),
...tags.filter((tag) => !savedTagsOrder.includes(tag))
]
}
return tags
}, [assistants, savedTagsOrder])
const getAssistantsByTag = useCallback(
(tag: string) => assistants.filter((assistant) => assistant.tags?.includes(tag)),
[assistants]
)
const updateTagsOrder = useCallback(
(newOrder: string[]) => {
dispatch(setTagsOrder(newOrder))
updateAssistants(
assistants.map((assistant) => {
if (!assistant.tags || assistant.tags.length === 0) {
return assistant
}
const newTags = [...assistant.tags]
newTags.sort((a, b) => {
return newOrder.indexOf(a) - newOrder.indexOf(b)
})
return {
...assistant,
tags: newTags
}
})
)
},
[assistants, dispatch]
)
const getGroupedAssistants = useMemo(() => {
// 按标签分组,处理多标签的情况
const assistantsByTags = flatMap(assistants, (assistant) => {
@@ -42,12 +75,30 @@ export const useTags = () => {
grouped.unshift(untagged)
}
// 根据savedTagsOrder对标签组进行排序
if (savedTagsOrder.length > 0) {
const untagged = grouped.length > 0 && grouped[0].tag === t('assistants.tags.untagged') ? grouped.shift() : null
grouped.sort((a, b) => {
const indexA = savedTagsOrder.indexOf(a.tag)
const indexB = savedTagsOrder.indexOf(b.tag)
if (indexA === -1 && indexB === -1) return 0
if (indexA === -1) return 1
if (indexB === -1) return -1
return indexA - indexB
})
if (untagged) {
grouped.unshift(untagged)
}
}
return grouped
}, [assistants, t])
}, [assistants, t, savedTagsOrder])
return {
allTags,
getAssistantsByTag,
getGroupedAssistants
getGroupedAssistants,
updateTagsOrder
}
}

View File

@@ -1,47 +1,16 @@
import db from '@renderer/databases'
import i18n from '@renderer/i18n'
import { EVENT_NAMES, EventEmitter } from '@renderer/services/EventService'
import { deleteMessageFiles } from '@renderer/services/MessagesService'
import store from '@renderer/store'
import { updateTopic } from '@renderer/store/assistants'
import { loadTopicMessagesThunk } from '@renderer/store/thunk/messageThunk'
import { Assistant, Topic } from '@renderer/types'
import { findMainTextBlocks } from '@renderer/utils/messageUtils/find'
import { find, isEmpty } from 'lodash'
import { useEffect, useState } from 'react'
import { isEmpty } from 'lodash'
import { useAssistant } from './useAssistant'
import { getStoreSetting } from './useSettings'
const renamingTopics = new Set<string>()
let _activeTopic: Topic
let _setActiveTopic: (topic: Topic) => void
export function useActiveTopic(_assistant: Assistant, topic?: Topic) {
const { assistant } = useAssistant(_assistant.id)
const [activeTopic, setActiveTopic] = useState(topic || _activeTopic || assistant?.topics[0])
_activeTopic = activeTopic
_setActiveTopic = setActiveTopic
useEffect(() => {
if (activeTopic) {
store.dispatch(loadTopicMessagesThunk(activeTopic.id))
EventEmitter.emit(EVENT_NAMES.CHANGE_TOPIC, activeTopic)
}
}, [activeTopic])
useEffect(() => {
// activeTopic not in assistant.topics
if (assistant && !find(assistant.topics, { id: activeTopic?.id })) {
setActiveTopic(assistant.topics[0])
}
}, [activeTopic?.id, assistant])
return { activeTopic, setActiveTopic }
}
export function useTopic(assistant: Assistant, topicId?: string) {
return assistant?.topics.find((topic) => topic.id === topicId)
}
@@ -86,7 +55,6 @@ export const autoRenameTopic = async (assistant: Assistant, topicId: string) =>
.substring(0, 50)
if (topicName) {
const data = { ...topic, name: topicName } as Topic
_setActiveTopic(data)
store.dispatch(updateTopic({ assistantId: assistant.id, topic: data }))
}
return
@@ -97,7 +65,6 @@ export const autoRenameTopic = async (assistant: Assistant, topicId: string) =>
const summaryText = await fetchMessagesSummary({ messages: topic.messages, assistant })
if (summaryText) {
const data = { ...topic, name: summaryText }
_setActiveTopic(data)
store.dispatch(updateTopic({ assistantId: assistant.id, topic: data }))
} else {
window.message?.error(i18n.t('message.error.fetchTopicName'))

View File

@@ -8,7 +8,10 @@
"add.name.placeholder": "Enter name",
"add.prompt": "Prompt",
"add.prompt.placeholder": "Enter prompt",
"add.prompt.variables.tip": "Available variables: {{date}}, {{time}}, {{datetime}}, {{system}}, {{arch}}, {{language}}, {{model_name}}",
"add.prompt.variables.tip": {
"title": "Available variables",
"content": "{{date}}:\tDate\n{{time}}:\tTime\n{{datetime}}:\tDate and time\n{{system}}:\tOperating system\n{{arch}}:\tCPU architecture\n{{language}}:\tLanguage\n{{model_name}}:\tModel name"
},
"add.title": "Create Agent",
"import": {
"title": "Import from External",
@@ -30,16 +33,7 @@
"agent": "Export Agent"
},
"delete.popup.content": "Are you sure you want to delete this agent?",
"edit.message.add.title": "Add",
"edit.message.assistant.placeholder": "Enter assistant message",
"edit.message.assistant.title": "Assistant",
"edit.message.empty.content": "Conversation input content cannot be empty",
"edit.message.group.title": "Message Group",
"edit.message.title": "Preset messages",
"edit.message.user.placeholder": "Enter user message",
"edit.message.user.title": "User",
"edit.model.select.title": "Select Model",
"edit.settings.hide_preset_messages": "Hide Preset Message",
"edit.title": "Edit Agent",
"manage.title": "Manage Agents",
"my_agents": "My Agents",
@@ -76,7 +70,6 @@
"settings.mcp.noServersAvailable": "No MCP servers available. Add servers in settings",
"settings.mcp.description": "Default enabled MCP servers",
"settings.model": "Model Settings",
"settings.preset_messages": "Preset Messages",
"settings.prompt": "Prompt Settings",
"settings.reasoning_effort": "Reasoning effort",
"settings.reasoning_effort.off": "Off",
@@ -428,7 +421,9 @@
"pinyin.asc": "Sort by Pinyin (A-Z)",
"pinyin.desc": "Sort by Pinyin (Z-A)"
},
"no_results": "No results"
"no_results": "No results",
"apps": "Apps",
"mcp": "Tools"
},
"docs": {
"title": "Docs"
@@ -950,7 +945,10 @@
"magic_prompt_option_tip": "Intelligently enhances upscaling prompts"
},
"text_desc_required": "Please enter image description first",
"image_handle_required": "Please upload an image first.",
"req_error_text": "Operation failed. Please try again. Avoid using 'copyrighted' or 'sensitive' words in your prompt.",
"req_error_token": "Please check the validity of the token",
"req_error_no_balance": "Please check the validity of the token",
"auto_create_paint": "Auto-create image",
"auto_create_paint_tip": "After the image is generated, a new image will be created automatically.",
"select_model": "Select Model",
@@ -1345,6 +1343,8 @@
"general.emoji_picker": "Emoji Picker",
"general.image_upload": "Image Upload",
"general.auto_check_update.title": "Auto Update",
"general.early_access.title": "Early Access",
"general.early_access.tooltip": "Enable to use the latest version from GitHub, which may be slower. Please backup your data in advance.",
"general.reset.button": "Reset",
"general.reset.title": "Data Reset",
"general.restore.button": "Restore",
@@ -1884,7 +1884,8 @@
"summary": "Summarize",
"search": "Search",
"refine": "Refine",
"copy": "Copy"
"copy": "Copy",
"quote": "Quote"
},
"window": {
"pin": "Pin",

View File

@@ -8,7 +8,10 @@
"add.name.placeholder": "名前を入力",
"add.prompt": "プロンプト",
"add.prompt.placeholder": "プロンプトを入力",
"add.prompt.variables.tip": "利用可能な変数:{{date}}, {{time}}, {{datetime}}, {{system}}, {{arch}}, {{language}}, {{model_name}}",
"add.prompt.variables.tip": {
"title": "利用可能な変数",
"content": "{{date}}:\t日付\n{{time}}:\t時間\n{{datetime}}:\t日付と時間\n{{system}}:\tオペレーティングシステム\n{{arch}}:\tCPUアーキテクチャ\n{{language}}:\t言語\n{{model_name}}:\tモデル名"
},
"add.title": "エージェントを作成",
"import": {
"title": "外部からインポート",
@@ -30,16 +33,7 @@
"agent": "エージェントをエクスポート"
},
"delete.popup.content": "このエージェントを削除してもよろしいですか?",
"edit.message.add.title": "追加",
"edit.message.assistant.placeholder": "アシスタントのメッセージを入力",
"edit.message.assistant.title": "アシスタント",
"edit.message.empty.content": "会話の入力内容が空です",
"edit.message.group.title": "メッセージグループ",
"edit.message.title": "プリセットメッセージ",
"edit.message.user.placeholder": "ユーザーメッセージを入力",
"edit.message.user.title": "ユーザー",
"edit.model.select.title": "モデルを選択",
"edit.settings.hide_preset_messages": "プリセットメッセージを非表示",
"edit.title": "エージェントを編集",
"manage.title": "エージェントを管理",
"my_agents": "マイエージェント",
@@ -76,7 +70,6 @@
"settings.default_model": "デフォルトモデル",
"settings.knowledge_base": "ナレッジベース設定",
"settings.model": "モデル設定",
"settings.preset_messages": "プリセットメッセージ",
"settings.prompt": "プロンプト設定",
"settings.reasoning_effort": "思考連鎖の長さ",
"settings.reasoning_effort.off": "オフ",
@@ -428,7 +421,9 @@
"pinyin.asc": "ピンインで昇順ソート",
"pinyin.desc": "ピンインで降順ソート"
},
"no_results": "検索結果なし"
"no_results": "検索結果なし",
"apps": "アプリ",
"mcp": "ツール"
},
"docs": {
"title": "ドキュメント"
@@ -950,7 +945,10 @@
"rendering_speed": "レンダリング速度",
"translating": "翻訳中...",
"text_desc_required": "画像の説明を先に入力してください",
"image_handle_required": "最初に画像をアップロードしてください。",
"req_error_text": "実行に失敗しました。もう一度お試しください。プロンプトに「著作権用語」や「センシティブな用語」を含めないでください。",
"req_error_token": "トークンの有効性を確認してください",
"req_error_no_balance": "トークンの有効性を確認してください",
"auto_create_paint": "画像を自動作成",
"auto_create_paint_tip": "画像が生成された後、自動的に新しい画像が作成されます。",
"select_model": "モデルを選択",
@@ -1758,6 +1756,8 @@
"content_limit_tooltip": "検索結果の内容長を制限し、制限を超える内容は切り捨てられます。"
},
"general.auto_check_update.title": "自動更新",
"general.early_access.title": "早期アクセス",
"general.early_access.tooltip": "有効にすると、GitHub の最新バージョンを使用します。ダウンロード速度が遅く、不安定な場合があります。データを事前にバックアップしてください。",
"quickPhrase": {
"title": "クイックフレーズ",
"add": "フレーズを追加",
@@ -1884,7 +1884,8 @@
"summary": "要約",
"search": "検索",
"refine": "最適化",
"copy": "コピー"
"copy": "コピー",
"quote": "引用"
},
"window": {
"pin": "最前面に固定",

View File

@@ -8,19 +8,13 @@
"add.name.placeholder": "Введите имя",
"add.prompt": "Промпт",
"add.prompt.placeholder": "Введите промпт",
"add.prompt.variables.tip": "Доступные переменные: {{date}}, {{time}}, {{datetime}}, {{system}}, {{arch}}, {{language}}, {{model_name}}",
"add.prompt.variables.tip": {
"title": "Доступные переменные",
"content": "{{date}}:\tДата\n{{time}}:\tВремя\n{{datetime}}:\tДата и время\n{{system}}:\tОперационная система\n{{arch}}:\tАрхитектура процессора\n{{language}}:\tЯзык\n{{model_name}}:\tНазвание модели"
},
"add.title": "Создать агента",
"delete.popup.content": "Вы уверены, что хотите удалить этого агента?",
"edit.message.add.title": "Добавить",
"edit.message.assistant.placeholder": "Введите сообщение ассистента",
"edit.message.assistant.title": "Ассистент",
"edit.message.empty.content": "Содержание вводимого сообщения не может быть пустым",
"edit.message.group.title": "Группа сообщений",
"edit.message.title": "Предустановленные сообщения",
"edit.message.user.placeholder": "Введите сообщение пользователя",
"edit.message.user.title": "Пользователь",
"edit.model.select.title": "Выбрать модель",
"edit.settings.hide_preset_messages": "Скрыть предустановленные сообщения",
"edit.title": "Редактировать агента",
"manage.title": "Редактировать агентов",
"my_agents": "Мои агенты",
@@ -76,7 +70,6 @@
"settings.default_model": "Модель по умолчанию",
"settings.knowledge_base": "Настройки базы знаний",
"settings.model": "Настройки модели",
"settings.preset_messages": "Предустановленные сообщения",
"settings.prompt": "Настройки промптов",
"settings.reasoning_effort.off": "Выключить",
"settings.reasoning_effort.high": "Стараюсь думать",
@@ -428,7 +421,9 @@
"pinyin.asc": "Сортировать по пиньинь (А-Я)",
"pinyin.desc": "Сортировать по пиньинь (Я-А)"
},
"no_results": "Результатов не найдено"
"no_results": "Результатов не найдено",
"apps": "Приложения",
"mcp": "Инструменты"
},
"docs": {
"title": "Документация"
@@ -950,7 +945,10 @@
"magic_prompt_option_tip": "Улучшает увеличение изображений с помощью интеллектуального оптимизирования промптов"
},
"text_desc_required": "Пожалуйста, сначала введите описание изображения",
"image_handle_required": "Пожалуйста, сначала загрузите изображение.",
"req_error_text": "Операция не удалась, повторите попытку. Пожалуйста, избегайте защищенных авторским правом терминов и конфиденциальных слов в запросах.",
"req_error_token": "Пожалуйста, проверьте действительность токена",
"req_error_no_balance": "Пожалуйста, проверьте действительность токена",
"auto_create_paint": "Автоматическое создание изображения",
"auto_create_paint_tip": "После генерации изображения будет автоматически создано новое.",
"select_model": "Выбрать модель",
@@ -1757,7 +1755,9 @@
"content_limit": "Ограничение длины текста",
"content_limit_tooltip": "Ограничьте длину содержимого результатов поиска, контент, превышающий ограничение, будет обрезан."
},
"general.auto_check_update.title": "Включить автообновление",
"general.auto_check_update.title": "Автоматическое обновление",
"general.early_access.title": "Ранний доступ",
"general.early_access.tooltip": "Включить для использования последней версии из GitHub, что может быть медленнее и нестабильно. Пожалуйста, сделайте резервную копию данных заранее.",
"quickPhrase": {
"title": "Быстрые фразы",
"add": "Добавить фразу",
@@ -1884,7 +1884,8 @@
"summary": "Суммаризировать",
"search": "Поиск",
"refine": "Уточнить",
"copy": "Копировать"
"copy": "Копировать",
"quote": "Цитировать"
},
"window": {
"pin": "Закрепить",

View File

@@ -8,7 +8,10 @@
"add.name.placeholder": "输入名称",
"add.prompt": "提示词",
"add.prompt.placeholder": "输入提示词",
"add.prompt.variables.tip": "可用的变量:{{date}}, {{time}}, {{datetime}}, {{system}}, {{arch}}, {{language}}, {{model_name}}",
"add.prompt.variables.tip": {
"title": "可用的变量",
"content": "{{date}}:\t日期\n{{time}}:\t时间\n{{datetime}}:\t日期和时间\n{{system}}:\t操作系统\n{{arch}}:\tCPU架构\n{{language}}:\t语言\n{{model_name}}:\t模型名称"
},
"add.title": "创建智能体",
"import": {
"title": "从外部导入",
@@ -30,16 +33,7 @@
"agent": "导出智能体"
},
"delete.popup.content": "确定要删除此智能体吗?",
"edit.message.add.title": "添加",
"edit.message.assistant.placeholder": "输入助手消息",
"edit.message.assistant.title": "助手",
"edit.message.empty.content": "会话输入内容不能为空",
"edit.message.group.title": "消息组",
"edit.message.title": "预设消息",
"edit.message.user.placeholder": "输入用户消息",
"edit.message.user.title": "用户",
"edit.model.select.title": "选择模型",
"edit.settings.hide_preset_messages": "隐藏预设消息",
"edit.title": "编辑智能体",
"manage.title": "管理智能体",
"my_agents": "我的智能体",
@@ -83,7 +77,6 @@
"settings.tool_use_mode.function": "函数",
"settings.tool_use_mode.prompt": "提示词",
"settings.model": "模型设置",
"settings.preset_messages": "预设消息",
"settings.prompt": "提示词设置",
"settings.reasoning_effort": "思维链长度",
"settings.reasoning_effort.off": "关闭",
@@ -428,7 +421,9 @@
"pinyin.asc": "按拼音升序",
"pinyin.desc": "按拼音降序"
},
"no_results": "无结果"
"no_results": "无结果",
"apps": "应用",
"mcp": "工具"
},
"docs": {
"title": "帮助文档"
@@ -951,6 +946,9 @@
},
"text_desc_required": "请先输入图片描述",
"req_error_text": "运行失败,请重试。提示词避免“版权词”和”敏感词”哦。",
"req_error_token": "请检查令牌有效性",
"req_error_no_balance": "请检查令牌有效性",
"image_handle_required": "请先上传图片",
"auto_create_paint": "自动新建图片",
"auto_create_paint_tip": "在图片生成后,会自动新建图片",
"select_model": "选择模型",
@@ -1345,6 +1343,8 @@
"general.emoji_picker": "表情选择器",
"general.image_upload": "图片上传",
"general.auto_check_update.title": "自动更新",
"general.early_access.title": "抢先体验",
"general.early_access.tooltip": "开启后,将使用 GitHub 的最新版本,下载速度可能较慢,请务必提前备份数据",
"general.reset.button": "重置",
"general.reset.title": "重置数据",
"general.restore.button": "恢复",
@@ -1706,7 +1706,7 @@
"search_message_in_chat": "在当前对话中搜索消息",
"show_app": "显示/隐藏应用",
"show_settings": "打开设置",
"title": "快捷方式",
"title": "快捷",
"toggle_new_context": "清除上下文",
"toggle_show_assistants": "切换助手显示",
"toggle_show_topics": "切换话题显示",
@@ -1814,6 +1814,13 @@
"service_tier.flex": "灵活"
}
},
"discover": {
"title": "发现",
"install": "安装",
"uninstall": "卸载",
"update": "更新",
"update_all": "全部更新"
},
"translate": {
"any.language": "任意语言",
"target_language": "目标语言",
@@ -1884,7 +1891,8 @@
"summary": "总结",
"search": "搜索",
"refine": "优化",
"copy": "复制"
"copy": "复制",
"quote": "引用"
},
"window": {
"pin": "置顶",

View File

@@ -8,7 +8,10 @@
"add.name.placeholder": "輸入名稱",
"add.prompt": "提示詞",
"add.prompt.placeholder": "輸入提示詞",
"add.prompt.variables.tip": "可用的變數:{{date}}, {{time}}, {{datetime}}, {{system}}, {{arch}}, {{language}}, {{model_name}}",
"add.prompt.variables.tip": {
"title": "可用的變數",
"content": "{{date}}:\t日期\n{{time}}:\t時間\n{{datetime}}:\t日期和時間\n{{system}}:\t作業系統\n{{arch}}:\tCPU架構\n{{language}}:\t語言\n{{model_name}}:\t模型名稱"
},
"add.title": "建立智慧代理人",
"import": {
"title": "從外部導入",
@@ -30,16 +33,7 @@
"agent": "匯出智慧代理人"
},
"delete.popup.content": "確定要刪除此智慧代理人嗎?",
"edit.message.add.title": "新增",
"edit.message.assistant.placeholder": "輸入助手訊息",
"edit.message.assistant.title": "助手",
"edit.message.empty.content": "會話輸入內容不能為空",
"edit.message.group.title": "訊息分組",
"edit.message.title": "預設訊息",
"edit.message.user.placeholder": "輸入使用者訊息",
"edit.message.user.title": "使用者",
"edit.model.select.title": "選擇模型",
"edit.settings.hide_preset_messages": "隱藏預設訊息",
"edit.title": "編輯智慧代理人",
"manage.title": "管理智慧代理人",
"my_agents": "我的智慧代理人",
@@ -76,7 +70,6 @@
"settings.default_model": "預設模型",
"settings.knowledge_base": "知識庫設定",
"settings.model": "模型設定",
"settings.preset_messages": "預設訊息",
"settings.prompt": "提示詞設定",
"settings.reasoning_effort": "思維鏈長度",
"settings.reasoning_effort.off": "關閉",
@@ -428,7 +421,9 @@
"pinyin.asc": "按拼音升序",
"pinyin.desc": "按拼音降序"
},
"no_results": "沒有結果"
"no_results": "沒有結果",
"apps": "應用",
"mcp": "工具"
},
"docs": {
"title": "說明文件"
@@ -950,7 +945,10 @@
},
"rendering_speed": "渲染速度",
"text_desc_required": "請先輸入圖片描述",
"image_handle_required": "請先上傳圖片。",
"req_error_text": "运行失败,请重试。提示词避免“版权词”和”敏感词”哦。",
"req_error_token": "請檢查令牌的有效性",
"req_error_no_balance": "請檢查令牌的有效性",
"auto_create_paint": "自動新增圖片",
"auto_create_paint_tip": "圖片生成後,會自動新增圖片",
"select_model": "選擇模型",
@@ -1698,7 +1696,7 @@
"search_message_in_chat": "在當前對話中搜尋訊息",
"show_app": "顯示/隱藏應用程式",
"show_settings": "開啟設定",
"title": "快速方式",
"title": "快捷鍵",
"toggle_new_context": "清除上下文",
"toggle_show_assistants": "切換助手顯示",
"toggle_show_topics": "切換話題顯示",
@@ -1760,7 +1758,9 @@
"content_limit": "內容長度限制",
"content_limit_tooltip": "限制搜尋結果的內容長度,超過限制的內容將被截斷"
},
"general.auto_check_update.title": "啟用自動更新",
"general.auto_check_update.title": "自動更新",
"general.early_access.title": "搶先體驗",
"general.early_access.tooltip": "開啟後,將使用 GitHub 的最新版本,下載速度可能較慢,請務必提前備份數據",
"quickPhrase": {
"title": "快捷短語",
"add": "新增短語",
@@ -1884,7 +1884,8 @@
"summary": "總結",
"search": "搜尋",
"refine": "優化",
"copy": "複製"
"copy": "複製",
"quote": "引用"
},
"window": {
"pin": "置頂",

View File

@@ -8,18 +8,13 @@
"add.name.placeholder": "Εισαγάγετε όνομα",
"add.prompt": "Φράση προκαλέσεως",
"add.prompt.placeholder": "Εισαγάγετε φράση προκαλέσεως",
"add.prompt.variables.tip": {
"title": "Διαθέσιμες μεταβλητές",
"content": "{{date}}:\tΗμερομηνία\n{{time}}:\tΏρα\n{{datetime}}:\tΗμερομηνία και ώρα\n{{system}}:\tΛειτουργικό σύστημα\n{{arch}}:\tΑρχιτεκτονική CPU\n{{language}}:\tΓλώσσα\n{{model_name}}:\tΌνομα μοντέλου"
},
"add.title": "Δημιουργία νέου ειδικού",
"delete.popup.content": "Είστε σίγουροι ότι θέλετε να διαγράψετε αυτόν τον ειδικό;",
"edit.message.add.title": "Προσθήκη",
"edit.message.assistant.placeholder": "Εισαγάγετε μήνυμα βοηθού",
"edit.message.assistant.title": "Βοηθός",
"edit.message.empty.content": "Το περιεχόμενο του συνομιλητή δεν μπορεί να είναι κενό.",
"edit.message.group.title": "Ομάδα μηνυμάτων",
"edit.message.title": "Προεπιλογές μηνυμάτων",
"edit.message.user.placeholder": "Εισαγάγετε μήνυμα χρήστη",
"edit.message.user.title": "Χρήστης",
"edit.model.select.title": "Επιλογή μοντέλου",
"edit.settings.hide_preset_messages": "Απόκρυψη προεπιλογών μηνυμάτων",
"edit.title": "Επεξεργασία ειδικού",
"manage.title": "Διαχείριση ειδικών",
"my_agents": "Οι ειδικοί μου",
@@ -64,7 +59,6 @@
"settings.default_model": "Προεπιλεγμένο μοντέλο",
"settings.knowledge_base": "Ρυθμίσεις βάσης γνώσεων",
"settings.model": "Ρυθμίσεις μοντέλου",
"settings.preset_messages": "Προεπιλεγμένα μηνύματα",
"settings.prompt": "Ρυθμίσεις προκαλύμματος",
"settings.reasoning_effort": "Μήκος λογισμικού αλυσίδας",
"settings.reasoning_effort.high": "Μεγάλο",

View File

@@ -8,18 +8,13 @@
"add.name.placeholder": "Ingrese el nombre",
"add.prompt": "Palabra clave",
"add.prompt.placeholder": "Ingrese la palabra clave",
"add.prompt.variables.tip": {
"title": "Variables disponibles",
"content": "{{date}}:\tFecha\n{{time}}:\tHora\n{{datetime}}:\tFecha y hora\n{{system}}:\tSistema operativo\n{{arch}}:\tArquitectura de CPU\n{{language}}:\tIdioma\n{{model_name}}:\tNombre del modelo"
},
"add.title": "Crear agente inteligente",
"delete.popup.content": "¿Está seguro de que desea eliminar este agente inteligente?",
"edit.message.add.title": "Agregar",
"edit.message.assistant.placeholder": "Ingrese el mensaje del asistente",
"edit.message.assistant.title": "Asistente",
"edit.message.empty.content": "El contenido de la sesión de chat no puede estar vacío",
"edit.message.group.title": "Grupo de mensajes",
"edit.message.title": "Mensaje predeterminado",
"edit.message.user.placeholder": "Ingrese el mensaje del usuario",
"edit.message.user.title": "Usuario",
"edit.model.select.title": "Seleccionar modelo",
"edit.settings.hide_preset_messages": "Ocultar mensajes predeterminados",
"edit.title": "Editar agente inteligente",
"manage.title": "Administrar agentes inteligentes",
"my_agents": "Mis agentes inteligentes",
@@ -64,7 +59,6 @@
"settings.default_model": "Modelo Predeterminado",
"settings.knowledge_base": "Configuración de Base de Conocimientos",
"settings.model": "Configuración de Modelo",
"settings.preset_messages": "Mensajes Preestablecidos",
"settings.prompt": "Configuración de Palabras Clave",
"settings.reasoning_effort": "Longitud de Cadena de Razonamiento",
"settings.reasoning_effort.high": "Largo",

View File

@@ -8,18 +8,13 @@
"add.name.placeholder": "Entrer le nom",
"add.prompt": "Mot-clé",
"add.prompt.placeholder": "Entrer le mot-clé",
"add.prompt.variables.tip": {
"title": "Variables disponibles",
"content": "{{date}}:\tDate\n{{time}}:\tHeure\n{{datetime}}:\tDate et heure\n{{system}}:\tSystème d'exploitation\n{{arch}}:\tArchitecture du processeur\n{{language}}:\tLangue\n{{model_name}}:\tNom du modèle"
},
"add.title": "Créer un agent intelligent",
"delete.popup.content": "Êtes-vous sûr de vouloir supprimer cet agent intelligent ?",
"edit.message.add.title": "Ajouter",
"edit.message.assistant.placeholder": "Entrer le message de l'assistant",
"edit.message.assistant.title": "Assistant",
"edit.message.empty.content": "Le contenu de la session ne peut pas être vide",
"edit.message.group.title": "Groupe de messages",
"edit.message.title": "Messages prédéfinis",
"edit.message.user.placeholder": "Entrer le message de l'utilisateur",
"edit.message.user.title": "Utilisateur",
"edit.model.select.title": "Sélectionner un modèle",
"edit.settings.hide_preset_messages": "Masquer les messages prédéfinis",
"edit.title": "Modifier l'agent intelligent",
"manage.title": "Gérer les agents intelligents",
"my_agents": "Mes agents intelligents",
@@ -64,7 +59,6 @@
"settings.default_model": "Modèle par défaut",
"settings.knowledge_base": "Paramètres de la base de connaissances",
"settings.model": "Paramètres du modèle",
"settings.preset_messages": "Messages prédéfinis",
"settings.prompt": "Paramètres de l'invite",
"settings.reasoning_effort": "Longueur de la chaîne de raisonnement",
"settings.reasoning_effort.high": "Long",

View File

@@ -8,18 +8,13 @@
"add.name.placeholder": "Digite o Nome",
"add.prompt": "Prompt",
"add.prompt.placeholder": "Digite o Prompt",
"add.prompt.variables.tip": {
"title": "Variáveis disponíveis",
"content": "{{date}}:\tData\n{{time}}:\tHora\n{{datetime}}:\tData e hora\n{{system}}:\tSistema operativo\n{{arch}}:\tArquitetura da CPU\n{{language}}:\tIdioma\n{{model_name}}:\tNome do modelo"
},
"add.title": "Criar Agente Inteligente",
"delete.popup.content": "Tem certeza de que deseja excluir este agente inteligente?",
"edit.message.add.title": "Adicionar",
"edit.message.assistant.placeholder": "Digite a Mensagem do Assistente",
"edit.message.assistant.title": "Assistente",
"edit.message.empty.content": "O conteúdo da sessão não pode estar vazio",
"edit.message.group.title": "Grupo de Mensagens",
"edit.message.title": "Mensagens Padrão",
"edit.message.user.placeholder": "Digite a Mensagem do Usuário",
"edit.message.user.title": "Usuário",
"edit.model.select.title": "Selecionar Modelo",
"edit.settings.hide_preset_messages": "Ocultar Mensagens Padrão",
"edit.title": "Editar Agente Inteligente",
"manage.title": "Gerenciar Agentes Inteligentes",
"my_agents": "Meus Agentes Inteligentes",
@@ -64,7 +59,6 @@
"settings.default_model": "Modelo Padrão",
"settings.knowledge_base": "Configurações da Base de Conhecimento",
"settings.model": "Configurações do Modelo",
"settings.preset_messages": "Mensagens Pré-definidas",
"settings.prompt": "Configurações de Prompt",
"settings.reasoning_effort": "Comprimento da Cadeia de Raciocínio",
"settings.reasoning_effort.high": "Longo",

View File

@@ -1,5 +1,4 @@
import { ImportOutlined, PlusOutlined } from '@ant-design/icons'
import { Navbar, NavbarCenter } from '@renderer/components/app/Navbar'
import CustomTag from '@renderer/components/CustomTag'
import ListItem from '@renderer/components/ListItem'
import Scrollbar from '@renderer/components/Scrollbar'
@@ -152,27 +151,23 @@ const AgentsPage: FC = () => {
return (
<Container>
<Navbar>
<NavbarCenter style={{ borderRight: 'none', justifyContent: 'space-between' }}>
{t('agents.title')}
<Input
placeholder={t('common.search')}
className="nodrag"
style={{ width: '30%', height: 28, borderRadius: 15, paddingLeft: 12 }}
size="small"
variant="filled"
allowClear
onClear={handleSearchClear}
suffix={<Search size={14} color="var(--color-icon)" onClick={handleSearch} />}
value={searchInput}
maxLength={50}
onChange={(e) => setSearchInput(e.target.value)}
onPressEnter={handleSearch}
/>
<div style={{ width: 80 }} />
</NavbarCenter>
</Navbar>
<div className="p-4">
<Input
placeholder={t('common.search')}
className="nodrag"
style={{ width: '30%', height: 28, borderRadius: 15, paddingLeft: 12 }}
size="small"
variant="filled"
allowClear
onClear={handleSearchClear}
suffix={<Search size={14} color="var(--color-icon)" onClick={handleSearch} />}
value={searchInput}
maxLength={50}
onChange={(e) => setSearchInput(e.target.value)}
onPressEnter={handleSearch}
/>
<div style={{ width: 80 }} />
</div>
<Main id="content-container">
<AgentsGroupList>
{Object.entries(agentGroups).map(([group]) => (

View File

@@ -45,7 +45,7 @@ export function useSystemAgents() {
// 如果没有远程配置或获取失败,加载本地代理
if (resourcesPath && _agents.length === 0) {
const localAgentsData = await window.api.fs.read(resourcesPath + '/data/agents.json')
const localAgentsData = await window.api.fs.read(resourcesPath + '/data/agents.json', 'utf-8')
_agents = JSON.parse(localAgentsData) as Agent[]
}

View File

@@ -1,18 +1,21 @@
import { Navbar, NavbarCenter } from '@renderer/components/app/Navbar'
import { useMinapps } from '@renderer/hooks/useMinapps'
import { Input } from 'antd'
import { Search } from 'lucide-react'
import React, { FC, useState } from 'react'
import { Button, Input } from 'antd'
import { Search, SettingsIcon, X } from 'lucide-react'
import React, { FC, useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { useLocation } from 'react-router'
import styled from 'styled-components'
import App from './App'
import MiniAppSettings from './MiniappSettings/MiniAppSettings'
import NewAppButton from './NewAppButton'
const AppsPage: FC = () => {
const { t } = useTranslation()
const [search, setSearch] = useState('')
const { minapps } = useMinapps()
const [isSettingsOpen, setIsSettingsOpen] = useState(false)
const location = useLocation()
const filteredApps = search
? minapps.filter(
@@ -31,31 +34,53 @@ const AppsPage: FC = () => {
e.preventDefault()
}
useEffect(() => {
setIsSettingsOpen(false)
}, [location.key])
return (
<Container onContextMenu={handleContextMenu}>
<Navbar>
<NavbarCenter style={{ borderRight: 'none', justifyContent: 'space-between' }}>
{t('minapp.title')}
<Input
placeholder={t('common.search')}
className="nodrag"
style={{ width: '30%', height: 28, borderRadius: 15 }}
size="small"
variant="filled"
suffix={<Search size={18} />}
value={search}
onChange={(e) => setSearch(e.target.value)}
/>
<div style={{ width: 80 }} />
</NavbarCenter>
</Navbar>
{/* <Navbar> */}
{/* <NavbarMain> */}
{/* {t('minapp.title')} */}
<div className="p-2">
<Input
placeholder={t('common.search')}
className="nodrag"
style={{
width: '30%',
height: 28,
borderRadius: 15,
position: 'absolute',
left: '50vw',
transform: 'translateX(-50%)'
}}
size="small"
variant="filled"
suffix={<Search size={18} />}
value={search}
onChange={(e) => setSearch(e.target.value)}
disabled={isSettingsOpen}
/>
<Button
type="text"
className="nodrag"
icon={isSettingsOpen ? <X size={18} /> : <SettingsIcon size={18} color="var(--color-text-2)" />}
onClick={() => setIsSettingsOpen(!isSettingsOpen)}
/>
</div>
{/* </NavbarMain> */}
{/* </Navbar> */}
<ContentContainer id="content-container">
<AppsContainer style={{ height: containerHeight }}>
{filteredApps.map((app) => (
<App key={app.id} app={app} />
))}
<NewAppButton />
</AppsContainer>
{isSettingsOpen && <MiniAppSettings />}
{!isSettingsOpen && (
<AppsContainer style={{ height: containerHeight }}>
{filteredApps.map((app) => (
<App key={app.id} app={app} />
))}
<NewAppButton />
</AppsContainer>
)}
</ContentContainer>
</Container>
)

View File

@@ -1,8 +1,8 @@
import { UndoOutlined } from '@ant-design/icons' // 导入重置图标
import { DEFAULT_MIN_APPS } from '@renderer/config/minapps'
import { useTheme } from '@renderer/context/ThemeProvider'
import { useMinapps } from '@renderer/hooks/useMinapps'
import { useSettings } from '@renderer/hooks/useSettings'
import { SettingDescription, SettingDivider, SettingRowTitle, SettingTitle } from '@renderer/pages/settings'
import { useAppDispatch } from '@renderer/store'
import {
setMaxKeepAliveMinapps,
@@ -12,9 +12,9 @@ import {
import { Button, message, Slider, Switch, Tooltip } from 'antd'
import { FC, useCallback, useEffect, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { useNavigate } from 'react-router'
import styled from 'styled-components'
import { SettingContainer, SettingDescription, SettingDivider, SettingGroup, SettingRowTitle, SettingTitle } from '..'
import MiniAppIconsManager from './MiniAppIconsManager'
// 默认小程序缓存数量
@@ -22,10 +22,10 @@ const DEFAULT_MAX_KEEPALIVE = 3
const MiniAppSettings: FC = () => {
const { t } = useTranslation()
const { theme } = useTheme()
const dispatch = useAppDispatch()
const { maxKeepAliveMinapps, showOpenedMinappsInSidebar, minappsOpenLinkExternal } = useSettings()
const { minapps, disabled, updateMinapps, updateDisabledMinapps } = useMinapps()
const navigate = useNavigate()
const [visibleMiniApps, setVisibleMiniApps] = useState(minapps)
const [disabledMiniApps, setDisabledMiniApps] = useState(disabled || [])
@@ -72,83 +72,87 @@ const MiniAppSettings: FC = () => {
}, [])
return (
<SettingContainer theme={theme}>
<Container>
{contextHolder} {/* 添加消息上下文 */}
<SettingGroup theme={theme}>
<SettingTitle>{t('settings.miniapps.title')}</SettingTitle>
<SettingDivider />
<SettingTitle
style={{ display: 'flex', flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center' }}>
<span>{t('settings.miniapps.display_title')}</span>
<ResetButtonWrapper>
<Button onClick={handleResetMinApps}>{t('common.reset')}</Button>
</ResetButtonWrapper>
</SettingTitle>
<BorderedContainer>
<MiniAppIconsManager
visibleMiniApps={visibleMiniApps}
disabledMiniApps={disabledMiniApps}
setVisibleMiniApps={setVisibleMiniApps}
setDisabledMiniApps={setDisabledMiniApps}
/>
</BorderedContainer>
<SettingDivider />
<SettingRow style={{ height: 40, alignItems: 'center' }}>
<SettingLabelGroup>
<SettingRowTitle>{t('settings.miniapps.open_link_external.title')}</SettingRowTitle>
</SettingLabelGroup>
<Switch
checked={minappsOpenLinkExternal}
onChange={(checked) => dispatch(setMinappsOpenLinkExternal(checked))}
/>
</SettingRow>
<SettingDivider />
{/* 缓存小程序数量设置 */}
<SettingRow>
<SettingLabelGroup>
<SettingRowTitle>{t('settings.miniapps.cache_title')}</SettingRowTitle>
<SettingDescription>{t('settings.miniapps.cache_description')}</SettingDescription>
</SettingLabelGroup>
<CacheSettingControls>
<SliderWithResetContainer>
<Tooltip title={t('settings.miniapps.reset_tooltip')} placement="top">
<ResetButton onClick={handleResetCacheLimit}>
<UndoOutlined />
</ResetButton>
</Tooltip>
<Slider
min={1}
max={10}
value={maxKeepAliveMinapps}
onChange={handleCacheChange}
marks={{
1: '1',
5: '5',
10: 'Max'
}}
tooltip={{ formatter: (value) => `${value}` }}
/>
</SliderWithResetContainer>
</CacheSettingControls>
</SettingRow>
<SettingDivider />
<SettingRow>
<SettingLabelGroup>
<SettingRowTitle>{t('settings.miniapps.sidebar_title')}</SettingRowTitle>
<SettingDescription>{t('settings.miniapps.sidebar_description')}</SettingDescription>
</SettingLabelGroup>
<Switch
checked={showOpenedMinappsInSidebar}
onChange={(checked) => dispatch(setShowOpenedMinappsInSidebar(checked))}
/>
</SettingRow>
</SettingGroup>
</SettingContainer>
<SettingTitle
style={{ display: 'flex', flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center' }}>
<span>{t('settings.miniapps.display_title')}</span>
<ResetButtonWrapper>
<Button onClick={handleResetMinApps}>{t('common.reset')}</Button>
</ResetButtonWrapper>
</SettingTitle>
<BorderedContainer>
<MiniAppIconsManager
visibleMiniApps={visibleMiniApps}
disabledMiniApps={disabledMiniApps}
setVisibleMiniApps={setVisibleMiniApps}
setDisabledMiniApps={setDisabledMiniApps}
/>
</BorderedContainer>
<SettingDivider />
<SettingRow style={{ height: 40, alignItems: 'center' }}>
<SettingLabelGroup>
<SettingRowTitle>{t('settings.miniapps.open_link_external.title')}</SettingRowTitle>
</SettingLabelGroup>
<Switch
checked={minappsOpenLinkExternal}
onChange={(checked) => dispatch(setMinappsOpenLinkExternal(checked))}
/>
</SettingRow>
<SettingDivider />
{/* 缓存小程序数量设置 */}
<SettingRow>
<SettingLabelGroup>
<SettingRowTitle>{t('settings.miniapps.cache_title')}</SettingRowTitle>
<SettingDescription>{t('settings.miniapps.cache_description')}</SettingDescription>
</SettingLabelGroup>
<CacheSettingControls>
<SliderWithResetContainer>
<Tooltip title={t('settings.miniapps.reset_tooltip')} placement="top">
<ResetButton onClick={handleResetCacheLimit}>
<UndoOutlined />
</ResetButton>
</Tooltip>
<Slider
min={1}
max={10}
value={maxKeepAliveMinapps}
onChange={handleCacheChange}
marks={{
1: '1',
5: '5',
10: 'Max'
}}
tooltip={{ formatter: (value) => `${value}` }}
/>
</SliderWithResetContainer>
</CacheSettingControls>
</SettingRow>
<SettingDivider />
<SettingRow>
<SettingLabelGroup>
<SettingRowTitle>{t('settings.miniapps.sidebar_title')}</SettingRowTitle>
<SettingDescription>{t('settings.miniapps.sidebar_description')}</SettingDescription>
</SettingLabelGroup>
<Switch
checked={showOpenedMinappsInSidebar}
onChange={(checked) => dispatch(setShowOpenedMinappsInSidebar(checked))}
/>
</SettingRow>
<SettingDivider />
<SettingRow style={{ justifyContent: 'flex-end' }}>
<Button onClick={() => navigate('/apps')}>{t('common.close')}</Button>
</SettingRow>
</Container>
)
}
const Container = styled.div`
display: flex;
flex-direction: column;
flex: 1;
`
// 修改和新增样式
const SettingRow = styled.div`
display: flex;

View File

@@ -0,0 +1,47 @@
import { Category } from '@renderer/types/cherryStore'
import React, { Suspense } from 'react'
import { Navigate, Route, Routes, useLocation } from 'react-router-dom'
// 实际的 AgentsPage 组件 - 请确保路径正确
import { discoverRouters } from '../routers'
// import AssistantDetailsPage from '../../agents/AssistantDetailsPage'; // 示例详情页
// 其他分类的页面组件 (如果需要)
// const MiniAppPagePlaceholder = ({ categoryId, subcategoryId }: { categoryId?: string; subcategoryId?: string }) => (
// <div className="p-4">
// MiniApp Placeholder for Category: {categoryId || 'N/A'}, Subcategory: {subcategoryId || 'N/A'}
// </div>
// )
export interface DiscoverContentProps {
activeTabId: string // This should be one of the CherryStoreType values, e.g., "Assistant"
// selectedSubcategoryId: string
currentCategory: Category | undefined
}
const DiscoverContent: React.FC<DiscoverContentProps> = ({ activeTabId, currentCategory }) => {
const location = useLocation() // To see the current path for debugging or more complex logic
if (!currentCategory || !activeTabId) {
return <div className="p-4 text-center">Loading: Category or Tab ID missing...</div>
}
if (!activeTabId && !location.pathname.startsWith('/discover/')) {
return <Navigate to="/discover/assistant?subcategory=all" replace /> // Fallback redirect, adjust as needed
}
return (
<Suspense fallback={<div>Loading...</div>}>
<Routes>
{discoverRouters.map((_Route) => {
if (!_Route.component) return null
return <Route key={_Route.path} path={`/${_Route.path}`} element={<_Route.component />} />
})}
<Route path="*" element={<div>Discover Feature Not Found at {location.pathname}</div>} />
</Routes>
</Suspense>
)
}
export default DiscoverContent

View File

@@ -0,0 +1,64 @@
import { SubCategoryItem } from '@renderer/types/cherryStore'
import { Badge } from '@renderer/ui/badge'
import {
Sidebar,
SidebarContent,
SidebarMenu,
SidebarMenuButton,
SidebarMenuSubItem,
SidebarProvider
} from '@renderer/ui/sidebar'
import { InternalCategory } from '../hooks/useDiscoverCategories'
interface DiscoverSidebarProps {
activeCategory: InternalCategory | undefined
selectedSubcategory: string
onSelectSubcategory: (subcategoryId: string, row?: SubCategoryItem) => void
}
export default function DiscoverSidebar({
activeCategory,
selectedSubcategory,
onSelectSubcategory
}: DiscoverSidebarProps) {
if (!activeCategory) {
return (
<Sidebar className="absolute top-0 left-0 h-full border-r">
<SidebarContent>
<p className="p-4 text-sm text-gray-500">No active category selected.</p>
</SidebarContent>
</Sidebar>
)
}
return (
<SidebarProvider className="relative h-full min-h-full w-full">
<Sidebar className="absolute top-0 left-0 h-full border-r">
<SidebarContent>
<SidebarMenu>
{activeCategory.items &&
activeCategory.items.length > 0 &&
activeCategory.items.map((subItem) => (
<SidebarMenuSubItem key={subItem.id}>
<SidebarMenuButton
isActive={subItem.id === selectedSubcategory}
onClick={() => {
onSelectSubcategory(subItem.id, subItem)
}}
size="sm">
<span className="truncate">{subItem.name}</span>
{typeof subItem.count === 'number' && (
<Badge variant="secondary" className="ml-auto shrink-0">
{subItem.count}
</Badge>
)}
</SidebarMenuButton>
</SidebarMenuSubItem>
))}
</SidebarMenu>
</SidebarContent>
</Sidebar>
</SidebarProvider>
)
}

View File

@@ -0,0 +1,158 @@
import { Category } from '@renderer/types/cherryStore'
import { useEffect, useMemo, useState } from 'react'
import { useLocation, useNavigate } from 'react-router-dom'
import { discoverRouters } from '../routers'
// Extended Category type for internal use in hook, including path and sidebar flag
// Export this interface so other files can import it
export interface InternalCategory extends Category {
path: string
hasSidebar?: boolean // Optional: defaults to true if not specified, or handle explicitly
}
// Initial category data with path and hasSidebar
const initialCategories: InternalCategory[] = discoverRouters.map((router) => ({
id: router.id,
title: router.title,
path: router.path,
hasSidebar: !router.component,
// 目前没有需要二级分类的分类
items: []
}))
// Helper to find category by path
const findCategoryByPath = (path: string | undefined): InternalCategory | undefined =>
initialCategories.find((cat) => cat.path === path)
// Helper to find category by id (activeTab)
const findCategoryById = (id: string | undefined): InternalCategory | undefined =>
initialCategories.find((cat) => cat.id === id)
export function useDiscoverCategories() {
const [categories, setCategories] = useState<InternalCategory[]>(initialCategories)
const [activeTab, setActiveTab] = useState<string>('')
const [selectedSubcategory, setSelectedSubcategory] = useState<string>('all')
const navigate = useNavigate()
const location = useLocation()
// Effect to initialize activeTab from URL path segment or navigate to default
useEffect(() => {
const pathSegments = location.pathname.split('/').filter(Boolean) // e.g., ["discover", "assistant"]
// Expects URL like /discover/:categoryPathSegment/...
const currentCategoryPath = pathSegments.length >= 2 && pathSegments[0] === 'discover' ? pathSegments[1] : undefined
const categoryFromPath = findCategoryByPath(currentCategoryPath)
// Synchronize active tab with the category determined from the URL path.
// If a category is found from the path, update the active tab to match its ID.
if (categoryFromPath) {
if (activeTab !== categoryFromPath.id) {
setActiveTab(categoryFromPath.id)
}
} else if (location.pathname === '/discover' || location.pathname === '/discover/') {
// Handle the case where the URL is the base /discover path.
// Redirect to the first category's path to ensure a category is always selected.
if (categories.length > 0) {
const firstCategory = categories[0]
if (firstCategory?.path) {
navigate(`/discover/${firstCategory.path}?subcategory=all`, { replace: true })
}
}
} else if (!currentCategoryPath && categories.length > 0 && !activeTab) {
// Fallback for invalid or unmatched /discover/xxx URLs.
// If the URL contains a path segment that doesn't correspond to a known category,
// and no tab is active, redirect to the first valid category.
const firstCategory = categories[0]
if (firstCategory?.path) {
navigate(`/discover/${firstCategory.path}?subcategory=all`, { replace: true })
}
}
// If categoryFromPath is undefined, and it's not /discover, it means it's an invalid path like /discover/unknown
// In this case, we don't navigate from here; ideally App.tsx has a NotFound route, or DiscoverContent shows a message.
}, [location.pathname, categories, activeTab, navigate])
// Effect to initialize selectedSubcategory from URL query param or default to 'all'
useEffect(() => {
const searchParams = new URLSearchParams(location.search)
const subcategoryIdFromQuery = searchParams.get('subcategory')
const currentCatDetails = findCategoryById(activeTab) // Use the helper here
if (subcategoryIdFromQuery && currentCatDetails) {
// Check if the subcategory from query is valid for the current active category
if (currentCatDetails.items.some((item) => item.id === subcategoryIdFromQuery)) {
if (selectedSubcategory !== subcategoryIdFromQuery) {
setSelectedSubcategory(subcategoryIdFromQuery)
}
return // Valid subcategory from URL is set, no further action needed in this effect iteration
}
}
// If no valid subcategory in query, or if activeTab has changed and subcategory needs reset/defaulting
if (activeTab && currentCatDetails) {
const defaultSub = currentCatDetails.items.find((item) => item.id === 'all') || currentCatDetails.items[0]
if (defaultSub) {
// Ensure defaultSub exists
// Set selectedSubcategory state first
if (selectedSubcategory !== defaultSub.id) {
setSelectedSubcategory(defaultSub.id)
}
// Then, if URL doesn't match this default, update URL to reflect the default subcategory
// This ensures the URL is the source of truth / always consistent.
if (!subcategoryIdFromQuery || subcategoryIdFromQuery !== defaultSub.id) {
const newSearchParams = new URLSearchParams() // Start with clean params for this path
newSearchParams.set('subcategory', defaultSub.id)
// Ensure we use the current actual path from currentCatDetails if available for navigation
// This avoids issues if location.pathname is briefly out of sync during transitions.
const basePath = currentCatDetails.path
? `/discover/${currentCatDetails.path}`
: location.pathname.split('?')[0]
navigate(`${basePath}?${newSearchParams.toString()}`, { replace: true })
}
}
}
}, [activeTab, location.search, categories, navigate, selectedSubcategory]) // location.pathname removed as basePath logic handles path part
const currentCategory = useMemo(() => {
return findCategoryById(activeTab) // Use the helper here
}, [activeTab]) // categories removed from deps as findCategoryById uses stable initialCategories
const handleSelectTab = (tabId: string) => {
const categoryToSelect = findCategoryById(tabId)
if (categoryToSelect && categoryToSelect.path && activeTab !== tabId) {
navigate(`/discover/${categoryToSelect.path}?subcategory=all`)
}
}
const handleSelectSubcategory = (subcategoryId: string) => {
const currentCatDetails = findCategoryById(activeTab)
if (selectedSubcategory !== subcategoryId && currentCatDetails?.path) {
const newSearchParams = new URLSearchParams()
newSearchParams.set('subcategory', subcategoryId)
navigate(`/discover/${currentCatDetails.path}?${newSearchParams.toString()}`, { replace: false })
}
}
// Ensure each category has an "All" subcategory (runs once on mount)
useEffect(() => {
setCategories((prev) =>
prev.map((cat) => {
if (!cat.items.some((item) => item.id === 'all')) {
return { ...cat, items: [{ id: 'all', name: `All ${cat.title}` }, ...cat.items] }
}
return cat
})
)
}, [])
return {
categories,
activeTab,
selectedSubcategory,
currentCategory,
handleSelectTab,
handleSelectSubcategory,
setActiveTab
}
}

View File

@@ -0,0 +1,83 @@
import { NavbarCenter, NavbarMain } from '@renderer/components/app/Navbar'
// import { useRuntime } from '@renderer/hooks/useRuntime' // No longer needed if resourcesPath is not used
import { Tabs as VercelTabs } from '@renderer/ui/vercel-tabs'
import { useEffect } from 'react'
import { useTranslation } from 'react-i18next'
import { useParams } from 'react-router-dom'
// Import Context and the main Dialog Manager component
import DiscoverContent from './components/DiscoverContent' // Removed DiscoverContent import
import DiscoverSidebar from './components/DiscoverSidebar'
import { InternalCategory, useDiscoverCategories } from './hooks/useDiscoverCategories'
// Function to adapt categories for VercelTabs
const adaptCategoriesForVercelTabs = (categories: InternalCategory[]) => {
return categories.map((category) => ({
id: category.id, // VercelTabs expects `id`
label: category.title // VercelTabs expects `label`
}))
}
export default function DiscoverPage() {
const { t } = useTranslation()
const {
categories,
activeTab,
selectedSubcategory,
currentCategory,
handleSelectTab,
handleSelectSubcategory,
setActiveTab
} = useDiscoverCategories()
// Path like /discover/:categoryIdFromUrl. categoryIdFromUrl is lowercase from URL.
const { categoryIdFromUrl } = useParams<{ categoryIdFromUrl: string }>()
useEffect(() => {
const matchedCategory = categories.find((cat) => cat.id.toLowerCase() === categoryIdFromUrl?.toLowerCase())
if (matchedCategory && activeTab !== matchedCategory.id) {
setActiveTab(matchedCategory.id)
}
}, [categoryIdFromUrl, categories, activeTab, setActiveTab])
const vercelTabsData = adaptCategoriesForVercelTabs(categories)
return (
<div className="h-full w-full">
<div className="flex h-full w-full flex-col overflow-hidden">
<NavbarMain className="h-auto flex-shrink-0">
<NavbarCenter>{t('discover.title')}</NavbarCenter>
</NavbarMain>
{categories.length > 0 && (
<div className="px-4 py-2">
<VercelTabs tabs={vercelTabsData} onTabChange={handleSelectTab} />
</div>
)}
<div className="flex flex-grow flex-row overflow-auto">
{currentCategory?.hasSidebar && (
<div className="w-64 flex-shrink-0 border-r">
<DiscoverSidebar
activeCategory={currentCategory}
selectedSubcategory={selectedSubcategory}
onSelectSubcategory={handleSelectSubcategory}
/>
</div>
)}
{/* {!currentCategory && categories.length > 0 && (
<div className="w-64 flex-shrink-0 border-r p-4 text-muted-foreground">Select a category...</div>
)} */}
<main className="flex-grow overflow-hidden">
<DiscoverContent
activeTabId={activeTab}
// selectedSubcategoryId={selectedSubcategory}
currentCategory={currentCategory}
/>
</main>
</div>
</div>
</div>
)
}

View File

@@ -0,0 +1,44 @@
import i18n from '@renderer/i18n'
import { CherryStoreType } from '@renderer/types/cherryStore'
import { lazy } from 'react'
export const discoverRouters = [
{
id: CherryStoreType.ASSISTANT,
title: i18n.t('assistants.title'),
path: 'assistant',
component: lazy(() => import('../agents/AgentsPage'))
},
{
id: CherryStoreType.MINI_APP,
title: i18n.t('minapp.title'),
path: 'mini-app',
component: lazy(() => import('../apps/AppsPage'))
},
{
id: CherryStoreType.TRANSLATE,
title: i18n.t('translate.title'),
path: 'translate',
component: lazy(() => import('../translate/TranslatePage'))
},
{
id: CherryStoreType.FILES,
title: i18n.t('files.title'),
path: 'files',
component: lazy(() => import('../files/FilesPage'))
},
{
id: CherryStoreType.PAINTINGS,
title: i18n.t('paintings.title'),
path: 'paintings/*',
isPrefix: true,
component: lazy(() => import('../paintings/PaintingsRoutePage'))
},
{
id: CherryStoreType.MCP_SERVER,
title: i18n.t('common.mcp'),
path: 'mcp-servers/*',
isPrefix: true,
component: lazy(() => import('../mcp-servers'))
}
]

View File

@@ -0,0 +1,7 @@
import { Category } from '@renderer/types/cherryStore'
export interface DiscoverContextType {
selectedSubcategory: string
activeTabId: string
currentCategory?: Category // currentCategory might be undefined initially
}

View File

@@ -5,7 +5,6 @@ import {
SortAscendingOutlined,
SortDescendingOutlined
} from '@ant-design/icons'
import { Navbar, NavbarCenter } from '@renderer/components/app/Navbar'
import ListItem from '@renderer/components/ListItem'
import TextEditPopup from '@renderer/components/Popups/TextEditPopup'
import Logger from '@renderer/config/logger'
@@ -207,9 +206,9 @@ const FilesPage: FC = () => {
return (
<Container>
<Navbar>
{/* <NavbarMain>
<NavbarCenter style={{ borderRight: 'none' }}>{t('files.title')}</NavbarCenter>
</Navbar>
</NavbarMain> */}
<ContentContainer id="content-container">
<SideNav>
{menuItems.map((item) => (

View File

@@ -15,7 +15,6 @@ import styled from 'styled-components'
import Inputbar from './Inputbar/Inputbar'
import Messages from './Messages/Messages'
import Tabs from './Tabs'
interface Props {
assistant: Assistant
@@ -38,7 +37,7 @@ const Chat: FC<Props> = (props) => {
const showRightTopics = showTopics && topicPosition === 'right'
const minusAssistantsWidth = showAssistants ? '- var(--assistants-width)' : ''
const minusRightTopicsWidth = showRightTopics ? '- var(--assistants-width)' : ''
return `calc(100vw - var(--sidebar-width) ${minusAssistantsWidth} ${minusRightTopicsWidth})`
return `calc(100vw - ${minusAssistantsWidth} ${minusRightTopicsWidth})`
}, [showAssistants, showTopics, topicPosition])
useHotkeys('esc', () => {
@@ -128,15 +127,6 @@ const Chat: FC<Props> = (props) => {
{isMultiSelectMode && <MultiSelectActionPopup topic={props.activeTopic} />}
</QuickPanelProvider>
</Main>
{topicPosition === 'right' && showTopics && (
<Tabs
activeAssistant={assistant}
activeTopic={props.activeTopic}
setActiveAssistant={props.setActiveAssistant}
setActiveTopic={props.setActiveTopic}
position="right"
/>
)}
</Container>
)
}

View File

@@ -0,0 +1,167 @@
import { Navbar } from '@renderer/components/app/Navbar'
import { HStack } from '@renderer/components/Layout'
import MinAppsPopover from '@renderer/components/Popups/MinAppsPopover'
import SearchPopup from '@renderer/components/Popups/SearchPopup'
import { isLinux, isMac, isWindows } from '@renderer/config/constant'
import { useAssistant } from '@renderer/hooks/useAssistant'
import { useFullscreen } from '@renderer/hooks/useFullscreen'
import { modelGenerating } from '@renderer/hooks/useRuntime'
import { useSettings } from '@renderer/hooks/useSettings'
import { useShortcut } from '@renderer/hooks/useShortcuts'
import { useShowAssistants, useShowTopics } from '@renderer/hooks/useStore'
import { EVENT_NAMES, EventEmitter } from '@renderer/services/EventService'
import { useAppDispatch } from '@renderer/store'
import { setNarrowMode } from '@renderer/store/settings'
import { Assistant } from '@renderer/types'
import { Tooltip } from 'antd'
import { t } from 'i18next'
import { LayoutGrid, PanelLeft, PanelRight, Search } from 'lucide-react'
import { FC, useCallback } from 'react'
import styled from 'styled-components'
import SelectModelButton from './components/SelectModelButton'
import UpdateAppButton from './components/UpdateAppButton'
interface Props {
activeAssistant: Assistant
position: 'left' | 'right'
}
const ChatNavbar: FC<Props> = ({ activeAssistant }) => {
const { assistant } = useAssistant(activeAssistant.id)
const { showAssistants, toggleShowAssistants } = useShowAssistants()
const isFullscreen = useFullscreen()
const { topicPosition, sidebarIcons, narrowMode } = useSettings()
const { toggleShowTopics } = useShowTopics()
const dispatch = useAppDispatch()
// Function to toggle assistants with cooldown
const handleToggleShowAssistants = useCallback(() => {
if (showAssistants) {
// When hiding sidebar, set cooldown
toggleShowAssistants()
// setTimeout(() => {
// setSidebarHideCooldown(false)
// }, 10000) // 10 seconds cooldown
} else {
// When showing sidebar, no cooldown needed
toggleShowAssistants()
}
}, [showAssistants, toggleShowAssistants])
useShortcut('toggle_show_assistants', handleToggleShowAssistants)
useShortcut('toggle_show_topics', () => {
if (topicPosition === 'right') {
toggleShowTopics()
} else {
EventEmitter.emit(EVENT_NAMES.SHOW_TOPIC_SIDEBAR)
}
})
useShortcut('search_message', () => {
SearchPopup.show()
})
const handleNarrowModeToggle = async () => {
await modelGenerating()
dispatch(setNarrowMode(!narrowMode))
}
return (
<Navbar className="home-navbar">
<NavbarContainer $isFullscreen={isFullscreen} $showSidebar={showAssistants} className="home-navbar-right">
<HStack alignItems="center">
<NavbarIcon
onClick={() => toggleShowAssistants()}
style={{ marginRight: 8, marginLeft: isMac && !isFullscreen ? 4 : -12 }}>
{showAssistants ? <PanelLeft size={18} /> : <PanelRight size={18} />}
</NavbarIcon>
<SelectModelButton assistant={assistant} />
</HStack>
<HStack alignItems="center" gap={8}>
<UpdateAppButton />
{isMac && (
<Tooltip title={t('chat.assistant.search.placeholder')} mouseEnterDelay={0.8}>
<NarrowIcon onClick={() => SearchPopup.show()}>
<Search size={18} />
</NarrowIcon>
</Tooltip>
)}
<Tooltip title={t('navbar.expand')} mouseEnterDelay={0.8}>
<NarrowIcon onClick={handleNarrowModeToggle}>
<i className="iconfont icon-icon-adaptive-width"></i>
</NarrowIcon>
</Tooltip>
{sidebarIcons.visible.includes('minapp') && (
<MinAppsPopover>
<Tooltip title={t('minapp.title')} mouseEnterDelay={0.8}>
<NarrowIcon>
<LayoutGrid size={18} />
</NarrowIcon>
</Tooltip>
</MinAppsPopover>
)}
</HStack>
</NavbarContainer>
</Navbar>
)
}
const NavbarContainer = styled.div<{ $isFullscreen: boolean; $showSidebar: boolean }>`
flex: 1;
display: flex;
flex-direction: row;
align-items: center;
height: var(--navbar-height);
max-height: var(--navbar-height);
min-height: var(--navbar-height);
justify-content: space-between;
padding-left: ${({ $showSidebar }) => (isMac ? ($showSidebar ? '10px' : '75px') : '15px')};
font-weight: bold;
color: var(--color-text-1);
padding-right: ${({ $isFullscreen }) => ($isFullscreen ? '12px' : isWindows ? '140px' : isLinux ? '120px' : '12px')};
-webkit-app-region: drag;
`
export const NavbarIcon = styled.div`
-webkit-app-region: none;
border-radius: 8px;
height: 30px;
padding: 0 7px;
display: flex;
flex-direction: row;
justify-content: center;
align-items: center;
transition: all 0.2s ease-in-out;
cursor: pointer;
.iconfont {
font-size: 18px;
color: var(--color-icon);
&.icon-a-addchat {
font-size: 20px;
}
&.icon-a-darkmode {
font-size: 20px;
}
&.icon-appstore {
font-size: 20px;
}
}
.anticon {
color: var(--color-icon);
font-size: 16px;
}
&:hover {
background-color: var(--color-background-mute);
color: var(--color-icon-white);
}
`
const NarrowIcon = styled(NavbarIcon)`
@media (max-width: 1000px) {
display: none;
}
`
export default ChatNavbar

View File

@@ -1,18 +1,14 @@
import { useAssistants } from '@renderer/hooks/useAssistant'
import { useChat } from '@renderer/hooks/useChat'
import { useSettings } from '@renderer/hooks/useSettings'
import { useActiveTopic } from '@renderer/hooks/useTopic'
import { EVENT_NAMES, EventEmitter } from '@renderer/services/EventService'
import NavigationService from '@renderer/services/NavigationService'
import { Assistant } from '@renderer/types'
import { FC, useEffect, useState } from 'react'
import { FC, useEffect } from 'react'
import { useLocation, useNavigate } from 'react-router-dom'
import styled from 'styled-components'
import Chat from './Chat'
import Navbar from './Navbar'
import HomeTabs from './Tabs'
let _activeAssistant: Assistant
import ChatNavbar from './ChatNavbar'
const HomePage: FC = () => {
const { assistants } = useAssistants()
@@ -21,12 +17,9 @@ const HomePage: FC = () => {
const location = useLocation()
const state = location.state
const [activeAssistant, setActiveAssistant] = useState(state?.assistant || _activeAssistant || assistants[0])
const { activeTopic, setActiveTopic } = useActiveTopic(activeAssistant, state?.topic)
const { activeAssistant, activeTopic, setActiveAssistant, setActiveTopic } = useChat()
const { showAssistants, showTopics, topicPosition } = useSettings()
_activeAssistant = activeAssistant
useEffect(() => {
NavigationService.setNavigate(navigate)
}, [navigate])
@@ -61,23 +54,8 @@ const HomePage: FC = () => {
return (
<Container id="home-page">
<Navbar
activeAssistant={activeAssistant}
activeTopic={activeTopic}
setActiveTopic={setActiveTopic}
setActiveAssistant={setActiveAssistant}
position="left"
/>
<ChatNavbar activeAssistant={activeAssistant} position="left" />
<ContentContainer id="content-container">
{showAssistants && (
<HomeTabs
activeAssistant={activeAssistant}
activeTopic={activeTopic}
setActiveAssistant={setActiveAssistant}
setActiveTopic={setActiveTopic}
position="left"
/>
)}
<Chat
assistant={activeAssistant}
activeTopic={activeTopic}
@@ -93,7 +71,6 @@ const Container = styled.div`
display: flex;
flex: 1;
flex-direction: column;
max-width: calc(100vw - var(--sidebar-width));
`
const ContentContainer = styled.div`

View File

@@ -14,11 +14,11 @@ import { useAssistant } from '@renderer/hooks/useAssistant'
import { useKnowledgeBases } from '@renderer/hooks/useKnowledge'
import { useMCPServers } from '@renderer/hooks/useMCPServers'
import { useMessageOperations, useTopicLoading } from '@renderer/hooks/useMessageOperations'
import { modelGenerating, useRuntime } from '@renderer/hooks/useRuntime'
import { useRuntime } from '@renderer/hooks/useRuntime'
import { useMessageStyle, useSettings } from '@renderer/hooks/useSettings'
import { useShortcut, useShortcutDisplay } from '@renderer/hooks/useShortcuts'
import { useSidebarIconShow } from '@renderer/hooks/useSidebarIcon'
import { addAssistantMessagesToTopic, getDefaultTopic } from '@renderer/services/AssistantService'
import { getDefaultTopic } from '@renderer/services/AssistantService'
import { EVENT_NAMES, EventEmitter } from '@renderer/services/EventService'
import FileManager from '@renderer/services/FileManager'
import { checkRateLimit, getUserMessage } from '@renderer/services/MessagesService'
@@ -33,8 +33,10 @@ import { sendMessage as _sendMessage } from '@renderer/store/thunk/messageThunk'
import { Assistant, FileType, KnowledgeBase, KnowledgeItem, Model, Topic } from '@renderer/types'
import type { MessageInputBaseParams } from '@renderer/types/newMessage'
import { classNames, delay, formatFileSize, getFileExtension } from '@renderer/utils'
import { formatQuotedText } from '@renderer/utils/formats'
import { getFilesFromDropEvent } from '@renderer/utils/input'
import { documentExts, imageExts, textExts } from '@shared/config/constant'
import { IpcChannel } from '@shared/IpcChannel'
import { Button, Tooltip } from 'antd'
import TextArea, { TextAreaRef } from 'antd/es/input/TextArea'
import dayjs from 'dayjs'
@@ -50,6 +52,7 @@ import InputbarTools, { InputbarToolsRef } from './InputbarTools'
import KnowledgeBaseInput from './KnowledgeBaseInput'
import MentionModelsInput from './MentionModelsInput'
import SendMessageButton from './SendMessageButton'
import SettingButton from './SettingButton'
import TokenCount from './TokenCount'
interface Props {
@@ -403,12 +406,9 @@ const Inputbar: FC<Props> = ({ assistant: _assistant, setActiveTopic, topic }) =
}
const addNewTopic = useCallback(async () => {
await modelGenerating()
const topic = getDefaultTopic(assistant.id)
await db.topics.add({ id: topic.id, messages: [] })
await addAssistantMessagesToTopic({ assistant, topic })
// Clear previous state
// Reset to assistant default model
@@ -420,6 +420,19 @@ const Inputbar: FC<Props> = ({ assistant: _assistant, setActiveTopic, topic }) =
setTimeout(() => EventEmitter.emit(EVENT_NAMES.SHOW_TOPIC_SIDEBAR), 0)
}, [addTopic, assistant, setActiveTopic, setModel])
const onQuote = useCallback(
(text: string) => {
const quotedText = formatQuotedText(text)
setText((prevText) => {
const newText = prevText ? `${prevText}\n${quotedText}\n` : `${quotedText}\n`
setTimeout(() => resizeTextArea(), 0)
return newText
})
textareaRef.current?.focus()
},
[resizeTextArea]
)
const onPause = async () => {
await pauseMessages()
}
@@ -624,21 +637,25 @@ const Inputbar: FC<Props> = ({ assistant: _assistant, setActiveTopic, topic }) =
_setEstimateTokenCount(tokensCount)
setContextCount({ current: contextCount.current, max: contextCount.max }) // 现在contextCount是一个对象而不是单个数值
}),
EventEmitter.on(EVENT_NAMES.ADD_NEW_TOPIC, addNewTopic),
EventEmitter.on(EVENT_NAMES.QUOTE_TEXT, (quotedText: string) => {
setText((prevText) => {
const newText = prevText ? `${prevText}\n${quotedText}\n` : `${quotedText}\n`
setTimeout(() => resizeTextArea(), 0)
return newText
})
textareaRef.current?.focus()
})
EventEmitter.on(EVENT_NAMES.ADD_NEW_TOPIC, addNewTopic)
]
return () => unsubscribes.forEach((unsub) => unsub())
}, [addNewTopic, resizeTextArea])
// 监听引用事件
const quoteFromAnywhereRemover = window.electron?.ipcRenderer.on(
IpcChannel.App_QuoteToMain,
(_, selectedText: string) => onQuote(selectedText)
)
return () => {
unsubscribes.forEach((unsub) => unsub())
quoteFromAnywhereRemover?.()
}
}, [addNewTopic, onQuote])
useEffect(() => {
textareaRef.current?.focus()
if (!document.querySelector('.topview-fullscreen-container')) {
textareaRef.current?.focus()
}
}, [assistant, topic])
useEffect(() => {
@@ -723,50 +740,28 @@ const Inputbar: FC<Props> = ({ assistant: _assistant, setActiveTopic, topic }) =
}, [])
const onToggleExpended = () => {
if (textareaHeight) {
const textArea = textareaRef.current?.resizableTextArea?.textArea
if (textArea) {
textArea.style.height = 'auto'
setTextareaHeight(undefined)
setTimeout(() => {
textArea.style.height = `${textArea.scrollHeight}px`
}, 200)
return
}
}
const isExpended = !expended
setExpend(isExpended)
const currentlyExpanded = expended || !!textareaHeight
const shouldExpand = !currentlyExpanded
setExpend(shouldExpand)
const textArea = textareaRef.current?.resizableTextArea?.textArea
if (textArea) {
if (isExpended) {
textArea.style.height = '70vh'
} else {
resetHeight()
}
if (!textArea) return
if (shouldExpand) {
textArea.style.height = '70vh'
setTextareaHeight(window.innerHeight * 0.7)
} else {
textArea.style.height = 'auto'
setTextareaHeight(undefined)
requestAnimationFrame(() => {
if (textArea) {
const contentHeight = textArea.scrollHeight
textArea.style.height = contentHeight > 400 ? '400px' : `${contentHeight}px`
}
})
}
textareaRef.current?.focus()
}
const resetHeight = () => {
if (expended) {
setExpend(false)
}
setTextareaHeight(undefined)
requestAnimationFrame(() => {
const textArea = textareaRef.current?.resizableTextArea?.textArea
if (textArea) {
textArea.style.height = 'auto'
const contentHeight = textArea.scrollHeight
textArea.style.height = contentHeight > 400 ? '400px' : `${contentHeight}px`
}
})
}
const isExpended = expended || !!textareaHeight
const showThinkingButton = isSupportedThinkingTokenModel(model) || isSupportedReasoningEffortModel(model)
@@ -862,6 +857,7 @@ const Inputbar: FC<Props> = ({ assistant: _assistant, setActiveTopic, topic }) =
ToolbarButton={ToolbarButton}
onClick={onNewContext}
/>
<SettingButton assistant={assistant} ToolbarButton={ToolbarButton} />
<TranslateButton text={text} onTranslated={onTranslated} isLoading={isTranslating} />
{loading && (
<Tooltip placement="top" title={t('chat.input.pause')} arrow>
@@ -949,11 +945,11 @@ const Textarea = styled(TextArea)`
padding: 0;
border-radius: 0;
display: flex;
flex: 1;
resize: none !important;
overflow: auto;
width: 100%;
box-sizing: border-box;
transition: height 0.2s ease;
&.ant-input {
line-height: 1.4;
}
@@ -968,6 +964,9 @@ const Toolbar = styled.div`
margin-bottom: 4px;
height: 30px;
gap: 16px;
position: relative;
z-index: 2;
flex-shrink: 0;
`
const ToolbarMenu = styled.div`

View File

@@ -176,7 +176,7 @@ const MCPToolsButton: FC<Props> = ({ ref, setInputValue, resizeTextArea, Toolbar
newList.push({
label: t('settings.mcp.addServer') + '...',
icon: <Plus />,
action: () => navigate('/settings/mcp')
action: () => navigate('/mcp-servers')
})
newList.unshift({

View File

@@ -0,0 +1,43 @@
import { Assistant } from '@renderer/types'
import { Popover } from 'antd'
import { Settings } from 'lucide-react'
import { FC, useState } from 'react'
import SettingsTab from '../Tabs/SettingsTab'
interface Props {
assistant: Assistant
ToolbarButton: any
}
const SettingButton: FC<Props> = ({ assistant, ToolbarButton }) => {
const [open, setOpen] = useState(false)
const handleOpenChange = (newOpen: boolean) => {
setOpen(newOpen)
}
const handleClose = () => {
setOpen(false)
}
return (
<Popover
placement="topLeft"
content={<SettingsTab assistant={assistant} onClose={handleClose} />}
trigger="click"
open={open}
onOpenChange={handleOpenChange}
styles={{
body: {
padding: '4px 2px 4px 2px'
}
}}>
<ToolbarButton type="text">
<Settings size={18} />
</ToolbarButton>
</Popover>
)
}
export default SettingButton

View File

@@ -2,6 +2,7 @@ import 'katex/dist/katex.min.css'
import 'katex/dist/contrib/copy-tex'
import 'katex/dist/contrib/mhchem'
import ImageViewer from '@renderer/components/ImageViewer'
import MarkdownShadowDOMRenderer from '@renderer/components/MarkdownShadowDOMRenderer'
import { useSettings } from '@renderer/hooks/useSettings'
import { EVENT_NAMES, EventEmitter } from '@renderer/services/EventService'
@@ -12,7 +13,7 @@ import { findCitationInChildren, getCodeBlockId } from '@renderer/utils/markdown
import { isEmpty } from 'lodash'
import { type FC, memo, useCallback, useMemo } from 'react'
import { useTranslation } from 'react-i18next'
import ReactMarkdown, { type Components } from 'react-markdown'
import ReactMarkdown, { type Components, defaultUrlTransform } from 'react-markdown'
import rehypeKatex from 'rehype-katex'
// @ts-ignore rehype-mathjax is not typed
import rehypeMathjax from 'rehype-mathjax'
@@ -22,7 +23,6 @@ import remarkGfm from 'remark-gfm'
import remarkMath from 'remark-math'
import CodeBlock from './CodeBlock'
import ImagePreview from './ImagePreview'
import Link from './Link'
const ALLOWED_ELEMENTS =
@@ -83,11 +83,21 @@ const Markdown: FC<Props> = ({ block }) => {
code: (props: any) => (
<CodeBlock {...props} id={getCodeBlockId(props?.node?.position?.start)} onSave={onSaveCodeBlock} />
),
img: ImagePreview,
pre: (props: any) => <pre style={{ overflow: 'visible' }} {...props} />
img: (props: any) => <ImageViewer style={{ maxWidth: 500, maxHeight: 500 }} {...props} />,
pre: (props: any) => <pre style={{ overflow: 'visible' }} {...props} />,
p: (props) => {
const hasImage = props?.node?.children?.some((child: any) => child.tagName === 'img')
if (hasImage) return <div {...props} />
return <p {...props} />
}
} as Partial<Components>
}, [onSaveCodeBlock])
const urlTransform = useCallback((value: string) => {
if (value.startsWith('data:image/png') || value.startsWith('data:image/jpeg')) return value
return defaultUrlTransform(value)
}, [])
// if (role === 'user' && !renderInputMessageAsMarkdown) {
// return <p style={{ marginBottom: 5, whiteSpace: 'pre-wrap' }}>{messageContent}</p>
// }
@@ -103,6 +113,7 @@ const Markdown: FC<Props> = ({ block }) => {
className="markdown"
components={components}
disallowedElements={DISALLOWED_ELEMENTS}
urlTransform={urlTransform}
remarkRehypeOptions={{
footnoteLabel: t('common.footnotes'),
footnoteLabelTagName: 'h4',

View File

@@ -1,15 +1,37 @@
import SvgSpinners180Ring from '@renderer/components/Icons/SvgSpinners180Ring'
import ImageViewer from '@renderer/components/ImageViewer'
import type { ImageMessageBlock } from '@renderer/types/newMessage'
import React from 'react'
import MessageImage from '../MessageImage'
import styled from 'styled-components'
interface Props {
block: ImageMessageBlock
}
const ImageBlock: React.FC<Props> = ({ block }) => {
return block.status === 'success' ? <MessageImage block={block} /> : <SvgSpinners180Ring />
if (block.status !== 'success') return <SvgSpinners180Ring />
const images = block.metadata?.generateImageResponse?.images?.length
? block.metadata?.generateImageResponse?.images
: block?.file?.path
? [`file://${block?.file?.path}`]
: []
return (
<Container style={{ marginBottom: 8 }}>
{images.map((src, index) => (
<ImageViewer
src={src}
key={`image-${index}`}
style={{ maxWidth: 500, maxHeight: 500, padding: 5, borderRadius: 8 }}
/>
))}
</Container>
)
}
const Container = styled.div`
display: flex;
flex-direction: row;
gap: 10px;
margin-top: 8px;
`
export default React.memo(ImageBlock)

View File

@@ -29,7 +29,6 @@ interface Props {
assistant?: Assistant
index?: number
total?: number
hidePresetMessages?: boolean
hideMenuBar?: boolean
style?: React.CSSProperties
isGrouped?: boolean
@@ -42,7 +41,6 @@ const MessageItem: FC<Props> = ({
topic,
// assistant,
index,
hidePresetMessages,
hideMenuBar = false,
isGrouped,
isStreaming = false,
@@ -122,10 +120,6 @@ const MessageItem: FC<Props> = ({
return () => unsubscribes.forEach((unsub) => unsub())
}, [message.id, messageHighlightHandler])
if (hidePresetMessages && message.isPreset) {
return null
}
if (message.type === 'clear') {
return (
<NewContextMessage className="clear-context-divider" onClick={() => EventEmitter.emit(EVENT_NAMES.NEW_CONTEXT)}>
@@ -263,7 +257,7 @@ const MessageFooter = styled.div`
align-items: center;
padding: 2px 0;
margin-top: 2px;
border-top: 1px dotted var(--color-border);
border-top: 0.5px dotted var(--color-border);
gap: 20px;
`

View File

@@ -45,16 +45,6 @@ const MessageBlockEditor: FC<Props> = ({ message, onSave, onResend, onCancel })
const textareaRef = useRef<TextAreaRef>(null)
const attachmentButtonRef = useRef<AttachmentButtonRef>(null)
useEffect(() => {
setTimeout(() => {
resizeTextArea()
if (textareaRef.current) {
textareaRef.current.focus({ cursor: 'end' })
}
}, 0)
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [])
const resizeTextArea = useCallback(() => {
const textArea = textareaRef.current?.resizableTextArea?.textArea
if (textArea) {
@@ -63,6 +53,15 @@ const MessageBlockEditor: FC<Props> = ({ message, onSave, onResend, onCancel })
}
}, [])
useEffect(() => {
setTimeout(() => {
resizeTextArea()
if (textareaRef.current) {
textareaRef.current.focus({ cursor: 'end' })
}
}, 0)
}, [resizeTextArea])
const onPaste = useCallback(
async (event: ClipboardEvent) => {
return await PasteService.handlePaste(
@@ -84,13 +83,9 @@ const MessageBlockEditor: FC<Props> = ({ message, onSave, onResend, onCancel })
// 添加全局粘贴事件处理
useEffect(() => {
// 注册当前组件的粘贴处理函数
PasteService.registerHandler('messageEditor', onPaste)
// 在组件加载时将焦点设置为当前组件
PasteService.setLastFocusedComponent('messageEditor')
// 卸载时取消注册
return () => {
PasteService.unregisterHandler('messageEditor')
}
@@ -165,12 +160,6 @@ const MessageBlockEditor: FC<Props> = ({ message, onSave, onResend, onCancel })
}
}
const autoResizeTextArea = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
const textarea = e.target
textarea.style.height = 'auto'
textarea.style.height = `${textarea.scrollHeight}px`
}
return (
<EditorContainer className="message-editor" onDragOver={(e) => e.preventDefault()} onDrop={handleDrop}>
{editedBlocks
@@ -184,7 +173,7 @@ const MessageBlockEditor: FC<Props> = ({ message, onSave, onResend, onCancel })
value={block.content}
onChange={(e) => {
handleTextChange(block.id, e.target.value)
autoResizeTextArea(e)
resizeTextArea()
}}
autoFocus
contextMenu="true"

View File

@@ -19,11 +19,10 @@ import SelectableMessage from './MessageSelect'
interface Props {
messages: (Message & { index: number })[]
topic: Topic
hidePresetMessages?: boolean
registerMessageElement?: (id: string, element: HTMLElement | null) => void
}
const MessageGroup = ({ messages, topic, hidePresetMessages, registerMessageElement }: Props) => {
const MessageGroup = ({ messages, topic, registerMessageElement }: Props) => {
const { editMessage } = useMessageOperations(topic)
const { multiModelMessageStyle: multiModelMessageStyleSetting, gridColumns, gridPopoverTrigger } = useSettings()
const { isMultiSelectMode } = useChatContext(topic)
@@ -168,7 +167,6 @@ const MessageGroup = ({ messages, topic, hidePresetMessages, registerMessageElem
message,
topic,
index: message.index,
hidePresetMessages,
style: {
paddingTop: isGrouped && ['horizontal', 'grid'].includes(multiModelMessageStyle) ? 0 : 15
}
@@ -220,16 +218,7 @@ const MessageGroup = ({ messages, topic, hidePresetMessages, registerMessageElem
</SelectableMessage>
)
},
[
isGrid,
isGrouped,
topic,
hidePresetMessages,
multiModelMessageStyle,
isHorizontal,
selectedMessageId,
gridPopoverTrigger
]
[isGrid, isGrouped, topic, multiModelMessageStyle, isHorizontal, selectedMessageId, gridPopoverTrigger]
)
return (

View File

@@ -382,6 +382,10 @@ const MessageMenubar: FC<Props> = (props) => {
{!isUserMessage && (
<Dropdown
menu={{
style: {
maxHeight: 250,
overflowY: 'auto'
},
items: [
...TranslateLanguageOptions.map((item) => ({
label: item.emoji + ' ' + item.label,
@@ -437,7 +441,7 @@ const MessageMenubar: FC<Props> = (props) => {
onClick: (e) => e.domEvent.stopPropagation()
}}
trigger={['click']}
placement="topRight"
placement="top"
arrow>
<Tooltip title={t('chat.translate')} mouseEnterDelay={1.2}>
<ActionButton className="message-action-button" onClick={(e) => e.stopPropagation()}>

View File

@@ -289,7 +289,6 @@ const Messages: React.FC<MessagesProps> = ({ assistant, topic, setActiveTopic, o
key={key}
messages={groupMessages}
topic={topic}
hidePresetMessages={assistant.settings?.hideMessages}
registerMessageElement={registerMessageElement}
/>
))}

View File

@@ -1,231 +0,0 @@
import { Navbar, NavbarLeft, NavbarRight } from '@renderer/components/app/Navbar'
import { HStack } from '@renderer/components/Layout'
import FloatingSidebar from '@renderer/components/Popups/FloatingSidebar'
import MinAppsPopover from '@renderer/components/Popups/MinAppsPopover'
import SearchPopup from '@renderer/components/Popups/SearchPopup'
import { isMac } from '@renderer/config/constant'
import { useAssistant } from '@renderer/hooks/useAssistant'
import { useFullscreen } from '@renderer/hooks/useFullscreen'
import { modelGenerating } from '@renderer/hooks/useRuntime'
import { useSettings } from '@renderer/hooks/useSettings'
import { useShortcut } from '@renderer/hooks/useShortcuts'
import { useShowAssistants, useShowTopics } from '@renderer/hooks/useStore'
import { EVENT_NAMES, EventEmitter } from '@renderer/services/EventService'
import { useAppDispatch } from '@renderer/store'
import { setNarrowMode } from '@renderer/store/settings'
import { Assistant, Topic } from '@renderer/types'
import { Tooltip } from 'antd'
import { t } from 'i18next'
import { LayoutGrid, MessageSquareDiff, PanelLeftClose, PanelRightClose, Search } from 'lucide-react'
import { FC, useCallback, useState } from 'react'
import styled from 'styled-components'
import SelectModelButton from './components/SelectModelButton'
import UpdateAppButton from './components/UpdateAppButton'
interface Props {
activeAssistant: Assistant
activeTopic: Topic
setActiveTopic: (topic: Topic) => void
setActiveAssistant: (assistant: Assistant) => void
position: 'left' | 'right'
}
const HeaderNavbar: FC<Props> = ({ activeAssistant, setActiveAssistant, activeTopic, setActiveTopic }) => {
const { assistant } = useAssistant(activeAssistant.id)
const { showAssistants, toggleShowAssistants } = useShowAssistants()
const isFullscreen = useFullscreen()
const { topicPosition, sidebarIcons, narrowMode } = useSettings()
const { showTopics, toggleShowTopics } = useShowTopics()
const dispatch = useAppDispatch()
const [sidebarHideCooldown, setSidebarHideCooldown] = useState(false)
// Function to toggle assistants with cooldown
const handleToggleShowAssistants = useCallback(() => {
if (showAssistants) {
// When hiding sidebar, set cooldown
toggleShowAssistants()
setSidebarHideCooldown(true)
// setTimeout(() => {
// setSidebarHideCooldown(false)
// }, 10000) // 10 seconds cooldown
} else {
// When showing sidebar, no cooldown needed
toggleShowAssistants()
}
}, [showAssistants, toggleShowAssistants])
const handleToggleShowTopics = useCallback(() => {
if (showTopics) {
// When hiding sidebar, set cooldown
toggleShowTopics()
setSidebarHideCooldown(true)
// setTimeout(() => {
// setSidebarHideCooldown(false)
// }, 10000) // 10 seconds cooldown
} else {
// When showing sidebar, no cooldown needed
toggleShowTopics()
}
}, [showTopics, toggleShowTopics])
useShortcut('toggle_show_assistants', handleToggleShowAssistants)
useShortcut('toggle_show_topics', () => {
if (topicPosition === 'right') {
toggleShowTopics()
} else {
EventEmitter.emit(EVENT_NAMES.SHOW_TOPIC_SIDEBAR)
}
})
useShortcut('search_message', () => {
SearchPopup.show()
})
const handleNarrowModeToggle = async () => {
await modelGenerating()
dispatch(setNarrowMode(!narrowMode))
}
return (
<Navbar className="home-navbar">
{showAssistants && (
<NavbarLeft style={{ justifyContent: 'space-between', borderRight: 'none', padding: 0 }}>
<Tooltip title={t('navbar.hide_sidebar')} mouseEnterDelay={0.8}>
<NavbarIcon onClick={handleToggleShowAssistants} style={{ marginLeft: isMac && !isFullscreen ? 16 : 0 }}>
<PanelLeftClose size={18} />
</NavbarIcon>
</Tooltip>
<Tooltip title={t('settings.shortcuts.new_topic')} mouseEnterDelay={0.8}>
<NavbarIcon onClick={() => EventEmitter.emit(EVENT_NAMES.ADD_NEW_TOPIC)} style={{ marginRight: 5 }}>
<MessageSquareDiff size={18} />
</NavbarIcon>
</Tooltip>
</NavbarLeft>
)}
<NavbarRight style={{ justifyContent: 'space-between', flex: 1 }} className="home-navbar-right">
<HStack alignItems="center">
{!showAssistants && !sidebarHideCooldown && (
<FloatingSidebar
activeAssistant={assistant}
setActiveAssistant={setActiveAssistant}
activeTopic={activeTopic}
setActiveTopic={setActiveTopic}
position={'left'}>
<Tooltip title={t('navbar.show_sidebar')} mouseEnterDelay={2}>
<NavbarIcon
onClick={() => toggleShowAssistants()}
style={{ marginRight: 8, marginLeft: isMac && !isFullscreen ? 4 : -12 }}>
<PanelRightClose size={18} />
</NavbarIcon>
</Tooltip>
</FloatingSidebar>
)}
{!showAssistants && sidebarHideCooldown && (
<Tooltip title={t('navbar.show_sidebar')} mouseEnterDelay={0.8}>
<NavbarIcon
onClick={() => toggleShowAssistants()}
style={{ marginRight: 8, marginLeft: isMac && !isFullscreen ? 4 : -12 }}
onMouseOut={() => setSidebarHideCooldown(false)}>
<PanelRightClose size={18} />
</NavbarIcon>
</Tooltip>
)}
<SelectModelButton assistant={assistant} />
</HStack>
<HStack alignItems="center" gap={8}>
<UpdateAppButton />
<Tooltip title={t('chat.assistant.search.placeholder')} mouseEnterDelay={0.8}>
<NarrowIcon onClick={() => SearchPopup.show()}>
<Search size={18} />
</NarrowIcon>
</Tooltip>
<Tooltip title={t('navbar.expand')} mouseEnterDelay={0.8}>
<NarrowIcon onClick={handleNarrowModeToggle}>
<i className="iconfont icon-icon-adaptive-width"></i>
</NarrowIcon>
</Tooltip>
{sidebarIcons.visible.includes('minapp') && (
<MinAppsPopover>
<Tooltip title={t('minapp.title')} mouseEnterDelay={0.8}>
<NarrowIcon>
<LayoutGrid size={18} />
</NarrowIcon>
</Tooltip>
</MinAppsPopover>
)}
{topicPosition === 'right' && !showTopics && !sidebarHideCooldown && (
<FloatingSidebar
activeAssistant={assistant}
setActiveAssistant={setActiveAssistant}
activeTopic={activeTopic}
setActiveTopic={setActiveTopic}
position={'right'}>
<Tooltip title={t('navbar.show_sidebar')} mouseEnterDelay={2}>
<NavbarIcon onClick={() => toggleShowTopics()}>
<PanelLeftClose size={18} />
</NavbarIcon>
</Tooltip>
</FloatingSidebar>
)}
{topicPosition === 'right' && !showTopics && sidebarHideCooldown && (
<Tooltip title={t('navbar.show_sidebar')} mouseEnterDelay={2}>
<NavbarIcon onClick={() => toggleShowTopics()} onMouseOut={() => setSidebarHideCooldown(false)}>
<PanelLeftClose size={18} />
</NavbarIcon>
</Tooltip>
)}
{topicPosition === 'right' && showTopics && (
<Tooltip title={t('navbar.hide_sidebar')} mouseEnterDelay={2}>
<NavbarIcon onClick={() => handleToggleShowTopics()}>
<PanelRightClose size={18} />
</NavbarIcon>
</Tooltip>
)}
</HStack>
</NavbarRight>
</Navbar>
)
}
export const NavbarIcon = styled.div`
-webkit-app-region: none;
border-radius: 8px;
height: 30px;
padding: 0 7px;
display: flex;
flex-direction: row;
justify-content: center;
align-items: center;
transition: all 0.2s ease-in-out;
cursor: pointer;
.iconfont {
font-size: 18px;
color: var(--color-icon);
&.icon-a-addchat {
font-size: 20px;
}
&.icon-a-darkmode {
font-size: 20px;
}
&.icon-appstore {
font-size: 20px;
}
}
.anticon {
color: var(--color-icon);
font-size: 16px;
}
&:hover {
background-color: var(--color-background-mute);
color: var(--color-icon-white);
}
`
const NarrowIcon = styled(NavbarIcon)`
@media (max-width: 1000px) {
display: none;
}
`
export default HeaderNavbar

View File

@@ -1,4 +1,4 @@
import { PlusOutlined } from '@ant-design/icons'
import { DownOutlined, PlusOutlined, RightOutlined } from '@ant-design/icons'
import DragableList from '@renderer/components/DragableList'
import Scrollbar from '@renderer/components/Scrollbar'
import { useAgents } from '@renderer/hooks/useAgents'
@@ -6,7 +6,7 @@ import { useAssistants } from '@renderer/hooks/useAssistant'
import { useAssistantsTabSortType } from '@renderer/hooks/useStore'
import { useTags } from '@renderer/hooks/useTags'
import { Assistant, AssistantsSortType } from '@renderer/types'
import { Divider, Tooltip } from 'antd'
import { Tooltip } from 'antd'
import { FC, useCallback, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next'
import styled from 'styled-components'
@@ -27,6 +27,7 @@ const Assistants: FC<AssistantsTabProps> = ({
}) => {
const { assistants, removeAssistant, addAssistant, updateAssistants } = useAssistants()
const [dragging, setDragging] = useState(false)
const [collapsedTags, setCollapsedTags] = useState<Record<string, boolean>>({})
const { addAgent } = useAgents()
const { t } = useTranslation()
const { getGroupedAssistants } = useTags()
@@ -45,6 +46,13 @@ const Assistants: FC<AssistantsTabProps> = ({
[activeAssistant, assistants, removeAssistant, setActiveAssistant, onCreateDefaultAssistant]
)
const toggleTagCollapse = useCallback((tag: string) => {
setCollapsedTags((prev) => ({
...prev,
[tag]: !prev[tag]
}))
}, [])
const handleSortByChange = useCallback(
(sortType: AssistantsSortType) => {
setAssistantsTabSortType(sortType)
@@ -52,34 +60,69 @@ const Assistants: FC<AssistantsTabProps> = ({
[setAssistantsTabSortType]
)
const handleGroupReorder = useCallback(
(tag: string, newGroupList: Assistant[]) => {
let insertIndex = 0
const newGlobal = assistants.map((a) => {
const tags = a.tags?.length ? a.tags : [t('assistants.tags.untagged')]
if (tags.includes(tag)) {
const replaced = newGroupList[insertIndex]
insertIndex += 1
return replaced
}
return a
})
updateAssistants(newGlobal)
},
[assistants, t, updateAssistants]
)
if (assistantsTabSortType === 'tags') {
return (
<Container className="assistants-tab" ref={containerRef}>
<div style={{ marginBottom: '8px' }}>
<div style={{ display: 'flex', flexDirection: 'column', marginBottom: 4, gap: 10 }}>
{getGroupedAssistants.map((group) => (
<TagsContainer key={group.tag}>
{group.tag !== t('assistants.tags.untagged') && (
<GroupTitle>
<GroupTitle onClick={() => toggleTagCollapse(group.tag)}>
<Tooltip title={group.tag}>
<GroupTitleName>{group.tag}</GroupTitleName>
<GroupTitleName>
{collapsedTags[group.tag] ? (
<RightOutlined style={{ fontSize: '10px', marginRight: '5px' }} />
) : (
<DownOutlined style={{ fontSize: '10px', marginRight: '5px' }} />
)}
{group.tag}
</GroupTitleName>
</Tooltip>
<Divider style={{ margin: '12px 0' }}></Divider>
<GroupTitleDivider />
</GroupTitle>
)}
{group.assistants.map((assistant) => (
<AssistantItem
key={assistant.id}
assistant={assistant}
isActive={assistant.id === activeAssistant.id}
sortBy={assistantsTabSortType}
onSwitch={setActiveAssistant}
onDelete={onDelete}
addAgent={addAgent}
addAssistant={addAssistant}
onCreateDefaultAssistant={onCreateDefaultAssistant}
handleSortByChange={handleSortByChange}
/>
))}
{!collapsedTags[group.tag] && (
<div>
<DragableList
list={group.assistants}
onUpdate={(newList) => handleGroupReorder(group.tag, newList)}
style={{ paddingBottom: dragging ? '34px' : 0 }}
onDragStart={() => setDragging(true)}
onDragEnd={() => setDragging(false)}>
{(assistant) => (
<AssistantItem
key={assistant.id}
assistant={assistant}
isActive={assistant.id === activeAssistant.id}
sortBy={assistantsTabSortType}
onSwitch={setActiveAssistant}
onDelete={onDelete}
addAgent={addAgent}
addAssistant={addAssistant}
onCreateDefaultAssistant={onCreateDefaultAssistant}
handleSortByChange={handleSortByChange}
/>
)}
</DragableList>
</div>
)}
</TagsContainer>
))}
</div>
@@ -154,22 +197,20 @@ const AssistantAddItem = styled.div`
cursor: pointer;
&:hover {
background-color: var(--color-background-soft);
}
&.active {
background-color: var(--color-background-soft);
border: 0.5px solid var(--color-border);
background-color: var(--color-list-item-hover);
}
`
const GroupTitle = styled.div`
padding: 8px 0px;
position: relative;
color: var(--color-text-2);
font-size: 12px;
font-weight: 500;
margin-bottom: -8px;
cursor: pointer;
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
height: 24px;
`
const GroupTitleName = styled.div`
@@ -177,13 +218,18 @@ const GroupTitleName = styled.div`
text-overflow: ellipsis;
white-space: nowrap;
overflow: hidden;
background-color: var(--color-background);
box-sizing: border-box;
padding: 0 4px;
color: var(--color-text);
position: absolute;
transform: translateY(2px);
font-size: 13px;
line-height: 24px;
margin-right: 5px;
display: flex;
`
const GroupTitleDivider = styled.div`
flex: 1;
border-top: 1px solid var(--color-border);
`
const AssistantName = styled.div`

View File

@@ -69,6 +69,7 @@ import OpenAISettingsGroup from './components/OpenAISettingsGroup'
interface Props {
assistant: Assistant
onClose: () => void
}
const SettingsTab: FC<Props> = (props) => {
@@ -197,7 +198,10 @@ const SettingsTab: FC<Props> = (props) => {
type="text"
size="small"
icon={<Settings2 size={16} />}
onClick={() => AssistantSettingsPopup.show({ assistant, tab: 'model' })}
onClick={() => {
AssistantSettingsPopup.show({ assistant, tab: 'model' })
props.onClose()
}}
/>
</HStack>
}>
@@ -681,8 +685,10 @@ const SettingsTab: FC<Props> = (props) => {
}
const Container = styled(Scrollbar)`
min-width: 300px;
max-width: 40vw;
max-height: 70vh;
display: flex;
flex: 1;
flex-direction: column;
padding: 0 8px;
padding-right: 0;

View File

@@ -20,11 +20,11 @@ import AssistantSettingsPopup from '@renderer/pages/settings/AssistantSettings'
import { getDefaultModel, getDefaultTopic } from '@renderer/services/AssistantService'
import { EVENT_NAMES, EventEmitter } from '@renderer/services/EventService'
import { Assistant, AssistantsSortType } from '@renderer/types'
import { getLeadingEmoji, uuid } from '@renderer/utils'
import { classNames, getLeadingEmoji, uuid } from '@renderer/utils'
import { hasTopicPendingRequests } from '@renderer/utils/queue'
import { Dropdown, MenuProps } from 'antd'
import { Button, Dropdown, MenuProps } from 'antd'
import { omit } from 'lodash'
import { AlignJustify, Plus, Settings2, Tag, Tags } from 'lucide-react'
import { AlignJustify, EllipsisVertical, Plus, Settings2, Tag, Tags } from 'lucide-react'
import { FC, memo, startTransition, useCallback, useEffect, useMemo, useState } from 'react'
import { useTranslation } from 'react-i18next'
import styled from 'styled-components'
@@ -63,6 +63,7 @@ const AssistantItem: FC<AssistantItemProps> = ({
const { assistants, updateAssistants } = useAssistants()
const [isPending, setIsPending] = useState(false)
const [isMenuOpen, setIsMenuOpen] = useState(false)
useEffect(() => {
if (isActive) {
@@ -141,7 +142,7 @@ const AssistantItem: FC<AssistantItemProps> = ({
return (
<Dropdown menu={{ items: menuItems }} trigger={['contextMenu']}>
<Container onClick={handleSwitch} className={isActive ? 'active' : ''}>
<Container onClick={handleSwitch} className={classNames({ active: isActive, 'is-menu-open': isMenuOpen })}>
<AssistantNameRow className="name" title={fullAssistantName}>
{assistantIconType === 'model' ? (
<ModelAvatar
@@ -159,11 +160,15 @@ const AssistantItem: FC<AssistantItemProps> = ({
)}
<AssistantName className="text-nowrap">{assistantName}</AssistantName>
</AssistantNameRow>
{isActive && (
<MenuButton onClick={() => EventEmitter.emit(EVENT_NAMES.SWITCH_TOPIC_SIDEBAR)}>
<TopicCount className="topics-count">{assistant.topics.length}</TopicCount>
</MenuButton>
)}
<Dropdown menu={{ items: menuItems }} trigger={['click']} onOpenChange={setIsMenuOpen}>
<Button
className="item-menu-button"
type="text"
size="small"
icon={<EllipsisVertical size={16} color="var(--color-text-3)" />}
onClick={(e) => e.stopPropagation()}
/>
</Dropdown>
</Container>
</Dropdown>
)
@@ -185,10 +190,10 @@ const handleTagOperation = (
assistants: Assistant[],
updateAssistants: (assistants: Assistant[]) => void
) => {
if (assistant.tags?.includes(tag)) {
return
}
updateAssistants(assistants.map((a) => (a.id === assistant.id ? { ...a, tags: [tag] } : a)))
const removeTag = () => updateAssistants(assistants.map((a) => (a.id === assistant.id ? { ...a, tags: [] } : a)))
const addTag = () => updateAssistants(assistants.map((a) => (a.id === assistant.id ? { ...a, tags: [tag] } : a)))
const hasTag = assistant.tags?.includes(tag)
hasTag ? removeTag() : addTag()
}
// 提取创建菜单项的函数
@@ -382,6 +387,7 @@ const Container = styled.div`
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
padding: 0 8px;
height: 37px;
position: relative;
@@ -389,12 +395,23 @@ const Container = styled.div`
border: 0.5px solid transparent;
width: calc(var(--assistants-width) - 20px);
cursor: pointer;
&.is-menu-open {
.item-menu-button {
display: block;
}
}
&:hover {
background-color: var(--color-list-item-hover);
.item-menu-button {
display: block;
}
}
&.active {
background-color: var(--color-list-item);
}
.item-menu-button {
display: none;
}
`
const AssistantNameRow = styled.div`
@@ -410,31 +427,4 @@ const AssistantName = styled.div`
font-size: 13px;
`
const MenuButton = styled.div`
display: flex;
flex-direction: row;
justify-content: center;
align-items: center;
min-width: 22px;
height: 22px;
min-height: 22px;
border-radius: 11px;
position: absolute;
background-color: var(--color-background);
right: 9px;
top: 6px;
padding: 0 5px;
border: 0.5px solid var(--color-border);
`
const TopicCount = styled.div`
color: var(--color-text);
font-size: 10px;
border-radius: 10px;
display: flex;
flex-direction: row;
justify-content: center;
align-items: center;
`
export default memo(AssistantItem)

View File

@@ -1,3 +1,4 @@
import { DragDropContext, Draggable, Droppable } from '@hello-pangea/dnd'
import { Box } from '@renderer/components/Layout'
import { TopView } from '@renderer/components/TopView'
import { useAssistants } from '@renderer/hooks/useAssistant'
@@ -19,9 +20,10 @@ interface Props extends ShowParams {
const PopupContainer: React.FC<Props> = ({ title, resolve }) => {
const [open, setOpen] = useState(true)
const { allTags, getAssistantsByTag } = useTags()
const { allTags, getAssistantsByTag, updateTagsOrder } = useTags()
const { assistants, updateAssistants } = useAssistants()
const { t } = useTranslation()
const [tags, setTags] = useState(allTags)
const onOk = () => {
setOpen(false)
@@ -49,10 +51,24 @@ const PopupContainer: React.FC<Props> = ({ title, resolve }) => {
})
)
}
const newTags = tags.filter((tag) => tag !== removedTag)
setTags(newTags)
updateTagsOrder(newTags)
}
})
}
const handleDragEnd = (result) => {
if (!result.destination) return
const items = Array.from(tags)
const [reorderedItem] = items.splice(result.source.index, 1)
items.splice(result.destination.index, 0, reorderedItem)
setTags(items)
updateTagsOrder(items)
}
AssistantTagsPopup.hide = onCancel
return (
@@ -66,13 +82,37 @@ const PopupContainer: React.FC<Props> = ({ title, resolve }) => {
transitionName="animation-move-down"
centered>
<Container>
{allTags.map((tag) => (
<TagItem key={tag}>
<Box mr={8}>{tag}</Box>
<Button type="text" icon={<Trash size={16} />} danger onClick={() => onDelete(tag)} />
</TagItem>
))}
{allTags.length === 0 && <Empty description="" />}
{tags.length > 0 ? (
<DragDropContext onDragEnd={handleDragEnd}>
<Droppable droppableId="tags">
{(provided) => (
<div {...provided.droppableProps} ref={provided.innerRef}>
{tags.map((tag, index) => (
<Draggable key={tag} draggableId={tag} index={index}>
{(provided) => (
<TagItem ref={provided.innerRef} {...provided.draggableProps} {...provided.dragHandleProps}>
<Box mr={8}>{tag}</Box>
<Button
type="text"
icon={<Trash size={16} />}
danger
onClick={(e) => {
e.stopPropagation()
onDelete(tag)
}}
/>
</TagItem>
)}
</Draggable>
))}
{provided.placeholder}
</div>
)}
</Droppable>
</DragDropContext>
) : (
<Empty description="" />
)}
</Container>
</Modal>
)

View File

@@ -1,13 +1,8 @@
import AddAssistantPopup from '@renderer/components/Popups/AddAssistantPopup'
import { useAssistants, useDefaultAssistant } from '@renderer/hooks/useAssistant'
import { useSettings } from '@renderer/hooks/useSettings'
import { useShowTopics } from '@renderer/hooks/useStore'
import { EVENT_NAMES, EventEmitter } from '@renderer/services/EventService'
import { Assistant, Topic } from '@renderer/types'
import { uuid } from '@renderer/utils'
import { Segmented as AntSegmented, SegmentedProps } from 'antd'
import { FC, useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { FC } from 'react'
import styled from 'styled-components'
import Assistants from './AssistantsTab'
@@ -15,51 +10,19 @@ import Settings from './SettingsTab'
import Topics from './TopicsTab'
interface Props {
tab: Tab
activeAssistant: Assistant
activeTopic: Topic
setActiveAssistant: (assistant: Assistant) => void
setActiveTopic: (topic: Topic) => void
position: 'left' | 'right'
forceToSeeAllTab?: boolean
style?: React.CSSProperties
}
type Tab = 'assistants' | 'topic' | 'settings'
let _tab: any = ''
const HomeTabs: FC<Props> = ({
activeAssistant,
activeTopic,
setActiveAssistant,
setActiveTopic,
position,
forceToSeeAllTab,
style
}) => {
const HomeTabs: FC<Props> = ({ tab, activeAssistant, activeTopic, setActiveAssistant, setActiveTopic, style }) => {
const { addAssistant } = useAssistants()
const [tab, setTab] = useState<Tab>(position === 'left' ? _tab || 'assistants' : 'topic')
const { topicPosition } = useSettings()
const { defaultAssistant } = useDefaultAssistant()
const { showTopics, toggleShowTopics } = useShowTopics()
const { t } = useTranslation()
const borderStyle = '0.5px solid var(--color-border)'
const border =
position === 'left' ? { borderRight: borderStyle } : { borderLeft: borderStyle, borderTopLeftRadius: 0 }
if (position === 'left' && topicPosition === 'left') {
_tab = tab
}
const showTab = !(position === 'left' && topicPosition === 'right')
const assistantTab = {
label: t('assistants.abbr'),
value: 'assistants'
// icon: <BotIcon size={16} />
}
const onCreateAssistant = async () => {
const assistant = await AddAssistantPopup.show()
@@ -72,68 +35,8 @@ const HomeTabs: FC<Props> = ({
setActiveAssistant(assistant)
}
useEffect(() => {
const unsubscribes = [
EventEmitter.on(EVENT_NAMES.SHOW_ASSISTANTS, (): any => {
showTab && setTab('assistants')
}),
EventEmitter.on(EVENT_NAMES.SHOW_TOPIC_SIDEBAR, (): any => {
showTab && setTab('topic')
}),
EventEmitter.on(EVENT_NAMES.SHOW_CHAT_SETTINGS, (): any => {
showTab && setTab('settings')
}),
EventEmitter.on(EVENT_NAMES.SWITCH_TOPIC_SIDEBAR, () => {
showTab && setTab('topic')
if (position === 'left' && topicPosition === 'right') {
toggleShowTopics()
}
})
]
return () => unsubscribes.forEach((unsub) => unsub())
}, [position, showTab, tab, toggleShowTopics, topicPosition])
useEffect(() => {
if (position === 'right' && topicPosition === 'right' && tab === 'assistants') {
setTab('topic')
}
if (position === 'left' && topicPosition === 'right' && forceToSeeAllTab != true && tab !== 'assistants') {
setTab('assistants')
}
}, [position, tab, topicPosition, forceToSeeAllTab])
return (
<Container style={{ ...border, ...style }} className="home-tabs">
{(showTab || (forceToSeeAllTab == true && !showTopics)) && (
<>
<Segmented
value={tab}
style={{ borderRadius: 50 }}
shape="round"
options={
[
(position === 'left' && topicPosition === 'left') || (forceToSeeAllTab == true && position === 'left')
? assistantTab
: undefined,
{
label: t('common.topics'),
value: 'topic'
// icon: <MessageSquareQuote size={16} />
},
{
label: t('settings.title'),
value: 'settings'
// icon: <SettingsIcon size={16} />
}
].filter(Boolean) as SegmentedProps['options']
}
onChange={(value) => setTab(value as 'topic' | 'settings')}
block
/>
<Divider />
</>
)}
<Container style={{ ...style }} className="home-tabs">
<TabContent className="home-tabs-content">
{tab === 'assistants' && (
<Assistants
@@ -154,6 +57,7 @@ const HomeTabs: FC<Props> = ({
const Container = styled.div`
display: flex;
flex: 1;
flex-direction: column;
max-width: var(--assistants-width);
min-width: var(--assistants-width);
@@ -173,68 +77,4 @@ const TabContent = styled.div`
overflow-x: hidden;
`
const Divider = styled.div`
border-top: 0.5px solid var(--color-border);
margin-top: 10px;
margin-left: 10px;
margin-right: 10px;
`
const Segmented = styled(AntSegmented)`
font-family: var(--font-family);
&.ant-segmented {
background-color: transparent;
margin: 0 10px;
margin-top: 10px;
padding: 0;
}
.ant-segmented-item {
overflow: hidden;
transition: none !important;
height: 34px;
line-height: 34px;
background-color: transparent;
user-select: none;
border-radius: var(--list-item-border-radius);
box-shadow: none;
}
.ant-segmented-item-selected,
.ant-segmented-item-selected:active {
transition: none !important;
background-color: var(--color-list-item);
}
.ant-segmented-item-label {
align-items: center;
display: flex;
flex-direction: row;
justify-content: center;
font-size: 13px;
height: 100%;
}
.ant-segmented-item-label[aria-selected='true'] {
color: var(--color-text);
}
.icon-business-smart-assistant {
margin-right: -2px;
}
.ant-segmented-thumb {
transition: none !important;
background-color: var(--color-list-item);
border-radius: var(--list-item-border-radius);
box-shadow: none;
&:hover {
background-color: transparent;
}
}
.ant-segmented-item-label,
.ant-segmented-item-icon {
display: flex;
align-items: center;
}
/* These styles ensure the same appearance as before */
border-radius: 0;
box-shadow: none;
`
export default HomeTabs

View File

@@ -1,4 +1,5 @@
import { CopyOutlined, DeleteOutlined, EditOutlined, RedoOutlined } from '@ant-design/icons'
import { NavbarIcon } from '@renderer/components/app/MainNavbar'
import CustomTag from '@renderer/components/CustomTag'
import Ellipsis from '@renderer/components/Ellipsis'
import { HStack } from '@renderer/components/Layout'
@@ -22,7 +23,6 @@ import styled from 'styled-components'
import CustomCollapse from '../../components/CustomCollapse'
import FileItem from '../files/FileItem'
import { NavbarIcon } from '../home/Navbar'
import KnowledgeSearchPopup from './components/KnowledgeSearchPopup'
import KnowledgeSettingsPopup from './components/KnowledgeSettingsPopup'
import StatusIcon from './components/StatusIcon'

View File

@@ -1,5 +1,5 @@
import { DeleteOutlined, EditOutlined, SettingOutlined } from '@ant-design/icons'
import { Navbar, NavbarCenter } from '@renderer/components/app/Navbar'
import { NavbarCenter, NavbarMain } from '@renderer/components/app/Navbar'
import DragableList from '@renderer/components/DragableList'
import ListItem from '@renderer/components/ListItem'
import PromptPopup from '@renderer/components/Popups/PromptPopup'
@@ -92,9 +92,9 @@ const KnowledgePage: FC = () => {
return (
<Container>
<Navbar>
<NavbarMain>
<NavbarCenter style={{ borderRight: 'none' }}>{t('knowledge.title')}</NavbarCenter>
</Navbar>
</NavbarMain>
<ContentContainer id="content-container">
<SideNav>
<ScrollContainer>

View File

@@ -1,5 +1,6 @@
import { CheckCircleOutlined, QuestionCircleOutlined, WarningOutlined } from '@ant-design/icons'
import { Center, VStack } from '@renderer/components/Layout'
import { SettingDescription, SettingRow, SettingSubtitle } from '@renderer/pages/settings'
import { useAppDispatch, useAppSelector } from '@renderer/store'
import { setIsBunInstalled, setIsUvInstalled } from '@renderer/store/mcp'
import { Alert, Button } from 'antd'
@@ -8,8 +9,6 @@ import { useTranslation } from 'react-i18next'
import { useNavigate } from 'react-router'
import styled from 'styled-components'
import { SettingDescription, SettingRow, SettingSubtitle } from '..'
interface Props {
mini?: boolean
}
@@ -82,7 +81,7 @@ const InstallNpxUv: FC<Props> = ({ mini = false }) => {
icon={installed ? <CheckCircleOutlined /> : <WarningOutlined />}
className="nodrag"
color={installed ? 'green' : 'danger'}
onClick={() => navigate('/settings/mcp/mcp-install')}
onClick={() => navigate('mcp-install')}
/>
)
}

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