Compare commits

...

85 Commits

Author SHA1 Message Date
suyao
e4434eb7c8 feat: add settings window functionality and shortcuts
- Introduced a new IPC channel for showing the settings window.
- Registered IPC handler for the settings window in the main process.
- Updated shortcut service to include a shortcut for opening the settings window.
- Modified the settings popup to use the new IPC method for displaying the settings.
- Adjusted the main sidebar to navigate to the settings window using the new IPC call.
- Enhanced the navbar to accommodate the new settings window functionality.
2025-07-11 16:05:57 +08:00
suyao
762732af9d Merge branch 'main' into feat/sidebar-ui 2025-07-11 13:55:51 +08:00
suyao
5d9b47198b Merge branch 'main' into feat/sidebar-ui 2025-07-03 05:21:20 +08:00
suyao
db1c03f9fa refactor(App): replace TabsContainer with AppLayout for improved layout structure; integrate ChatProvider for chat context management; update routing logic in NavigationHandler to utilize SettingsPopup; adjust main height variable in styles for consistency; enhance FilesPage layout by repositioning SideNav; implement new SettingsPopup component for settings management. 2025-07-02 11:50:41 +08:00
suyao
bedea8aaaa refactor(ContextMenu): simplify context menu implementation by removing unused props and handlers; streamline rendering logic in Message component 2025-07-02 03:48:21 +08:00
suyao
27d959caed refactor(SettingsTab): update import for Windows constant and improve shortcut label logic 2025-07-02 03:07:16 +08:00
suyao
0609b93a14 Merge branch 'main' into feat/sidebar-ui 2025-07-02 03:04:43 +08:00
one
c7a0b05841 refactor(Select): provide consistent search experience for antd Select (#7363)
- Add CustomSelect for enhanced searching
- Replace some Select components with CustomSelect
- Fix translation language searching problem
2025-06-19 19:23:25 +08:00
suyao
5ca0ce682b Merge branch 'main' into feat/sidebar-ui 2025-06-19 14:58:48 +08:00
suyao
7c3752a8e6 feat(TopicsHistory): integrate assistant information into topic display
- Added functionality to map and display assistant details alongside topics in the TopicsHistory component.
- Introduced a new AssistantTag styled component for better visual representation of assistants.
- Updated the layout of TopicItem to accommodate assistant information.
2025-06-19 14:20:20 +08:00
Teo
7ae10be387 refactor(ImportAgentPopup): replace Space with Flex for footer layout 2025-06-18 19:02:26 +08:00
Teo
7767dfaac7 feat(styles): add colorSplit property for Divider component in AntdProvider 2025-06-18 18:52:19 +08:00
Teo
c89ff17b36 feat(styles): add EditableNumber component and update styles for improved UI 2025-06-18 18:41:15 +08:00
Teo
0810a63fd8 feat(styles): enhance dropdown and slider components for improved UI 2025-06-18 14:15:50 +08:00
Teo
ff261fb52b refactor(styles): clean up and optimize dropdown and popover styles
- Removed redundant styles from mention-models-dropdown in ant.scss for better maintainability.
- Simplified Selector component by removing ConfigProvider wrapper around Dropdown.
- Updated AntdProvider to set consistent border radius for Dropdown and Popover components.
- Adjusted padding in SettingsTab for improved layout.
2025-06-17 15:35:29 +08:00
suyao
8f8deb9275 fix: enhance MessageSettingsPopup styles for better usability
- Added max height and overflow styles to the body of the MessageSettingsPopup to improve content visibility and scrolling behavior.
2025-06-17 13:46:18 +08:00
suyao
0a7e591f0e refactor: update icon colors to use primary color scheme
- Changed icon colors from var(--color-link) to var(--color-primary) in multiple components for consistency.
- Updated styles in ContentSearch, ReasoningIcon, WebSearchIcon, GenerateImageButton, ThinkingButton, and WebSearchButton to enhance visual coherence.
2025-06-17 12:55:32 +08:00
kangfenmao
c1e8f1063a Merge branch 'main' into feat/sidebar-ui 2025-06-16 17:53:21 +08:00
kangfenmao
4317f4b672 feat: use tabs
wip

wip

wip

wip
2025-06-16 17:52:18 +08:00
one
202504fd17 refactor(Navbar&Sidebar): rearrange navbar icons, add search functionality for assistants and topics (#7170)
* refactor(Navbar&Sidebar): rearrange navbar icons, add search functionality for assistants and topics

- Updated ChatNavbar and MainNavbar to streamline the display of assistant icons based on their visibility state.
- Introduced search input in MainSidebar for filtering assistants and topics.
- Enhanced AssistantsTab and TopicsTab to support search functionality, allowing users to filter displayed items based on input.
- Added a new utility function to improve search keyword matching logic.
- Improved overall layout and styling for better user experience.

* refactor: update icons

* refactor(Search): allow clear

* refactor: enhance search bar

* refactor: improve search bar style

* feat: new panel left icon
2025-06-16 12:35:24 +08:00
Wang Jiyuan
05727c637f refactor: Encapsulate image display related components (#7211) 2025-06-16 00:05:25 +08:00
Teo
c7843ca288 fix: 修复合并main分之后样式问题 2025-06-15 13:00:08 +08:00
kangfenmao
facf29e02b Merge branch 'main' into feat/sidebar-ui
# Conflicts:
#	src/renderer/src/assets/styles/index.scss
#	src/renderer/src/hooks/useTopic.ts
#	src/renderer/src/pages/home/Messages/Blocks/ImageBlock.tsx
#	src/renderer/src/pages/home/Messages/Blocks/index.tsx
#	src/renderer/src/pages/mcp-servers/providers/lanyun.ts
#	src/renderer/src/pages/settings/ModelSettings/TopicNamingModalPopup.tsx
2025-06-15 11:39:21 +08:00
kangfenmao
66d280136c style: enhance MainSidebar and PageContainer styles for improved UI
- Added 3D transform styles to PageContainer for better visual effects.
- Updated MainSidebar to use optional chaining for safer property access.
- Removed background color from MainSidebar to support transparency.
2025-06-15 11:36:33 +08:00
Teo
91892ea619 feat(FilesPage): 添加批量删除功能 2025-06-14 21:52:25 +08:00
Teo
7d70425c75 revert: remove backgroundMaterial for win 2025-06-14 18:44:35 +08:00
Teo
20b3db0c01 Revert "refactor(FileItem, FileList, FilesPage, i18n): enhance file management UI and localization"
This reverts commit edeb9f84f9.
2025-06-14 15:48:47 +08:00
Teo
f1804bc3a0 Revert "refactor(FileItem, FileList, FilesPage): enhance file selection UI and improve checkbox handling"
This reverts commit 58f3edb352.
2025-06-14 15:46:55 +08:00
Teo
2ec0c29087 style: change hover color 2025-06-14 15:03:58 +08:00
Teo
20a572aa46 feat(styles, useAppInit, DisplaySettings): 更新导航栏背景色和列表项样式,优化透明窗口设置
- 在 color.scss 中添加 Windows 版本的导航栏背景色。
- 修改 useAppInit 钩子以支持 Windows 和 Mac 的背景色设置。
- 更新 MainSidebar 的悬停样式以使用新的背景色。
- 在 DisplaySettings 中简化透明窗口设置的显示逻辑。
2025-06-14 14:49:30 +08:00
kangfenmao
142b624001 feat(ThemeProvider, color.scss, useAppInit): integrate transparent window settings and update styles 2025-06-14 10:36:45 +08:00
kangfenmao
3775562956 feat(DisplaySettings, MainSidebar, useSettings): implement transparent window feature and update styles
- Added functionality to manage transparent window settings in DisplaySettings and MainSidebar components.
- Updated useSettings hook to include transparent window state management.
- Modified MainSidebar and its styles to support transparency based on user settings.
- Removed obsolete opaque window translations from localization files across multiple languages for consistency.
2025-06-14 08:32:40 +08:00
Teo
3544c40d9a feat(TranslatePage, ant.scss): add ChevronDown icon to select components and update dropdown styles
- Introduced ChevronDown icon to various Select components in TranslatePage for improved visual consistency.
- Updated ant.scss to add a border style to the select dropdown for enhanced UI clarity.
2025-06-14 00:41:02 +08:00
Teo
36fa3af9e9 feat(CustomCollapse, ThinkingBlock, KnowledgeSearchPopup): add expand icons and adjust styles for improved UI
- Introduced ChevronRight expand icons in CustomCollapse and ThinkingBlock components for better visual feedback.
- Adjusted height and styling in KnowledgeSearchPopup for enhanced usability and consistency.
- Updated various styled components to improve layout and user interaction.
2025-06-14 00:28:33 +08:00
Teo
eb832cc25a refactor(AddKnowledgePopup, KnowledgeSettingsPopup, ModelSettings): enhance select components with consistent icons
- Updated Select components in AddKnowledgePopup and KnowledgeSettingsPopup to include a ChevronDown icon for improved visual consistency.
- Refactored ModelSettings to add ChevronDown icon to Select components, enhancing the overall UI experience.
- Simplified Slider component usage by removing unnecessary styling for a cleaner layout.
2025-06-13 23:27:46 +08:00
Teo
0378f9ceb1 refactor(styles, AntdProvider, DisplaySettings, DefaultAssistantSettings): enhance UI elements and improve styling consistency
- Updated ant.scss to refine modal styles for better visual distinction.
- Adjusted AntdProvider theme settings for Segmented and Switch components to improve color consistency.
- Modified DisplaySettings to change button sizes and styles for a more cohesive appearance.
- Tweaked DefaultAssistantSettings button dimensions for improved layout and usability.
2025-06-13 23:04:17 +08:00
suyao
99b23e1d5d fix(migrate): correct filter condition for assistants' template status
- Updated the filter condition in migrateConfig to check for undefined isTemplate, ensuring proper assignment of template status for assistants.
2025-06-13 22:24:50 +08:00
kangfenmao
8d23c810fe refactor(App, MainSidebar, OpenedMinapps): streamline component code and enhance layout
- Simplified the handleClick function in App component by removing unnecessary line breaks.
- Adjusted MainMenu component in MainSidebar to remove inline margin styling for cleaner layout.
- Updated OpenedMinapps to apply margin styling directly to TabsContainer for improved spacing.
- Removed the PinnedApps component to declutter the sidebar and enhance performance.
2025-06-13 20:44:08 +08:00
kangfenmao
cdfa2ac13a refactor(ChatProvider, useChat): implement context for chat state management and enhance event handling
- Introduced ChatProvider to manage active assistant and topic state using React context.
- Updated useChat hook to utilize context for accessing chat state and actions.
- Refactored message navigation to emit events for setting active assistant and topic.
- Improved topic selection logic to ensure active topic is valid when topics change.
- Integrated ChatProvider into HomePage and HistoryPage for consistent chat state management.
2025-06-13 19:31:21 +08:00
suyao
58f3edb352 refactor(FileItem, FileList, FilesPage): enhance file selection UI and improve checkbox handling
- Added isSelected prop to FileItem for visual indication of selection.
- Updated FileList to manage selected file IDs and pass selection state to FileItem.
- Enhanced FilesPage checkbox handling with improved visibility on hover and selection state.
- Introduced CheckboxContainer for consistent styling of checkboxes across components.
2025-06-13 19:20:16 +08:00
suyao
edeb9f84f9 refactor(FileItem, FileList, FilesPage, i18n): enhance file management UI and localization
- Updated FileItem component to display file count and additional metadata.
- Refactored FileList to support checkbox selection and improved layout.
- Enhanced FilesPage with batch delete functionality and improved file selection handling.
- Updated localization strings across multiple languages for consistency in delete confirmation messages.
2025-06-13 19:11:10 +08:00
Teo
2757fcf6b9 refactor(Selector, HomePage, MessageGroupSettings, NutstoreSettings, WebDavSettings): replace Select components with Selector for improved consistency
- Updated Selector component to handle value checks more robustly.
- Removed unused ContentContainer styles in HomePage for cleaner code.
- Replaced Select components with Selector in MessageGroupSettings, NutstoreSettings, and WebDavSettings for a unified UI experience.
- Enhanced option handling in Selector for better integration with existing settings.
2025-06-13 14:03:30 +08:00
kangfenmao
5339f4a9a3 refactor(MainSidebar, AssistantItem): enhance sidebar functionality and improve event handling
- Updated event handling in MainSidebar to conditionally show topics based on assistant selection.
- Integrated new settings option for showing topics when clicking on assistants.
- Removed unused code related to tab navigation for a cleaner implementation.
- Improved dependency management in useEffect hooks for better performance.
2025-06-13 13:49:15 +08:00
kangfenmao
e786feb165 refactor(i18n, settings): update localization and UI elements for improved consistency
- Removed redundant settings entries in multiple language files.
- Updated various settings titles for clarity, including "Code Settings" to "Code Block Settings".
- Added new localization keys for assistant and topic settings.
- Enhanced the layout of the ChatNavbar and MainSidebar components for better user experience.
- Adjusted the MessageMenubar to reflect updated settings terminology.
2025-06-13 13:11:06 +08:00
suyao
5794c36d0d Merge branch 'feat/sidebar-ui' of https://github.com/CherryHQ/cherry-studio into feat/sidebar-ui 2025-06-13 12:24:37 +08:00
Teo
7d8b88c56e refactor(styles, HistoryPage, MessageGroup): improve UI consistency and layout
- Removed padding from dropdown menu items for a cleaner look.
- Updated suffix rendering in HistoryPage to conditionally show icon based on search length.
- Added padding to MessageGroup for better spacing in grid layout.
2025-06-13 11:46:20 +08:00
Teo
d0daeddf14 Merge branch 'feat/sidebar-ui' of github.com:CherryHQ/cherry-studio into feat/sidebar-ui 2025-06-13 10:58:35 +08:00
Teo
2fe2ae797b refactor(styles): update CitationTooltip and CitationsList components for improved UI consistency and layout 2025-06-13 10:58:20 +08:00
kangfenmao
9bb66227b4 refactor(DragableList, AssistantsTab, AssistantItem): improve layout and styling consistency
- Adjusted margin and padding in DragableList for better spacing.
- Simplified structure in AssistantsTab by removing unnecessary divs and enhancing item layout.
- Added style prop to AssistantItem for flexible styling options.
- Updated GroupTitle and TagsContainer for improved visual consistency.
2025-06-13 10:42:09 +08:00
kangfenmao
5f4736e8c1 refactor(AssistantsTab): streamline drag-and-drop structure and improve layout consistency
- Removed unnecessary div wrappers to simplify the component structure.
- Enhanced the layout of the assistant items for better spacing and alignment.
- Added padding to the GroupTitle for improved visual consistency.
2025-06-13 10:24:52 +08:00
kangfenmao
7a44910847 Merge branch 'main' into feat/sidebar-ui
# Conflicts:
#	package.json
#	src/renderer/src/hooks/useTopic.ts
#	src/renderer/src/pages/home/Messages/Blocks/ImageBlock.tsx
#	src/renderer/src/pages/home/Messages/MessageTokens.tsx
#	src/renderer/src/store/index.ts
#	src/renderer/src/store/migrate.ts
#	src/renderer/src/store/runtime.ts
2025-06-13 10:05:43 +08:00
suyao
509632030b refactor(assistant): replace useAgents with useAssistants across components, streamline assistant management and enhance template handling 2025-06-13 05:14:53 +08:00
suyao
09fe2aa67b refactor(styles): update dropdown menu item styles to include submenu titles for consistent padding 2025-06-13 00:15:48 +08:00
neko engineer
793c641e1c feat: Optimize assistant drag-and-drop effect (#7115)
* feat: 优化助手拖拽效果
增加组内,组件,跨组拖拽效果

* feat: 提交注释

---------

Co-authored-by: linshuhao <nmnm1996>
2025-06-13 00:08:36 +08:00
Teo
38eb206a8a refactor(WindowService): 更新窗口服务配置,优化背景材质和透明度设置 2025-06-12 22:36:40 +08:00
Teo
3d9236a09a fix: fix ui 2025-06-12 21:47:24 +08:00
suyao
e6fd9b5678 Merge branch 'feat/sidebar-ui' of https://github.com/CherryHQ/cherry-studio into feat/sidebar-ui 2025-06-12 21:03:31 +08:00
suyao
d41f175a05 refactor(assistant): enhance assistant creation by automatically adding default topics; streamline default assistant handling 2025-06-12 21:03:17 +08:00
Teo
6d6a554fd3 refactor(styles): reorganize popover and citations list for improved UI consistency and user experience 2025-06-12 19:05:06 +08:00
suyao
cb1fcf7d2d Merge branch 'feat/sidebar-ui' of https://github.com/CherryHQ/cherry-studio into feat/sidebar-ui 2025-06-12 18:29:07 +08:00
suyao
6ed30fd78a Merge 'refactor/assistant-debounce' into 'feat/sidebar-ui' 2025-06-12 18:29:00 +08:00
Teo
69acb2fccd refactor(styles): adjust modal confirm body padding for improved layout consistency 2025-06-12 18:24:27 +08:00
Teo
d95a4e56f5 refactor(styles): update dropdown and modal styles for improved layout and consistency across components 2025-06-12 18:15:00 +08:00
Teo
b15dac9ef4 refactor(styles): standardize padding and width across various components for improved layout consistency 2025-06-12 17:37:20 +08:00
Teo
73fced37b4 refactor(styles): adjust border-radius and padding in various components for improved UI consistency; update layout in settings pages 2025-06-12 17:04:41 +08:00
Teo
1124090d87 refactor(styles): enhance popover styles with border and content overflow handling; update AntdProvider for background mask color 2025-06-12 15:56:35 +08:00
Teo
a92fa1b1ba refactor(MainSidebar): enhance dropdown menu with theme toggle and documentation link; update styles for dropdown menu 2025-06-12 15:46:10 +08:00
Teo
13fdfc58b6 refactor(ChatNavbar): add new topic button with tooltip and adjust sidebar styles for improved visibility 2025-06-12 15:25:03 +08:00
Teo
048a9135ac refactor(styles): update markdown table styles for improved layout and border handling 2025-06-12 14:59:26 +08:00
Teo
b5636646c9 refactor(Selector): enhance Selector component with option grouping, disabled state handling, and improved label retrieval logic 2025-06-12 12:03:04 +08:00
kangfenmao
d9def89ced refactor(HomePage): add id attribute to HStack and remove redundant id from Container for improved structure 2025-06-12 11:28:13 +08:00
kangfenmao
b16d0069bf refactor(Settings): remove windowStyle from settings and related components; transparent window 2025-06-12 10:39:56 +08:00
kangfenmao
30823691f9 refactor(SettingsPage): implement lazy loading for ProvidersList with Suspense and Spin component; remove unnecessary loading state from ProvidersList 2025-06-12 10:13:42 +08:00
kangfenmao
8be98ccbb3 refactor(Routes): enhance page transition animations and improve Navbar styling; conditionally render user name in MainSidebar 2025-06-12 10:08:46 +08:00
kangfenmao
922e85754a feat(Navigation): add IPC channels for navigation handling and implement cache service for URL management 2025-06-12 09:39:51 +08:00
kangfenmao
f041f9a231 refactor(RouteContainer): remove unused state and effect for route readiness; simplify animation handling 2025-06-12 09:39:51 +08:00
kangfenmao
0d9f1882b9 feat: add route animation
feat: add route animation
2025-06-12 09:39:51 +08:00
Teo
26d823e0a5 refactor(HomePage): update HomePage component to accept style prop and simplify layout structure 2025-06-12 09:39:51 +08:00
Teo
daa89df479 refactor(Inputbar): improve textarea height adjustment and update padding for better layout consistency 2025-06-12 09:39:51 +08:00
Teo
527740bf42 refactor(MessageSettingsPopup): replace StyledSelect with Selector component for improved consistency and maintainability 2025-06-12 09:39:51 +08:00
Teo
881e0b4713 refactor(Selector): update LabelIcon styles for improved hover effects and background consistency 2025-06-12 09:39:51 +08:00
Teo
88e251cee7 refactor(Settings): update input components to use Space.Compact for better layout and consistency across various settings pages 2025-06-12 09:39:51 +08:00
Teo
76387643f7 refactor(DisplaySettings): update button styles for zoom controls and color picker to enhance UI consistency 2025-06-12 09:39:51 +08:00
Teo
b41c89972b refactor(Selector): enhance Selector component with placeholder support and improve type safety; update GeneralSettings and WebSearchSettings to utilize Selector 2025-06-12 09:39:51 +08:00
kangfenmao
11a8154458 feat: new app sidebar
fix: adjust navbar and title bar dimensions, update icon handling

feat: implement ChatNavbar component and enhance MainNavbar with search functionality

fix: invert transparency setting for WindowService based on OS

refactor: clean up MainSidebar and useChat hooks, remove unused state handling and improve topic selection logic

refactor: simplify HomeTabs component by removing unused imports and commented code, update AssistantAddItem hover styles

fix: set WindowService transparency to false for consistent behavior across platforms

feat: add event listener to MainSidebar for topic tab navigation

feat: enhance summarization prompt and add topic sidebar visibility toggle

feat(Inputbar): add SettingButton component for settings access

style(color.scss): update border color to improve UI consistency

style: update chat background colors and margins for improved UI consistency

refactor(MainSidebar, i18n): update MCP title in sidebar and localization files, remove unused MCP entries

feat: remove prompt component

refactor(SettingsTab, OpenAISettingsGroup): restructure settings components for improved readability and maintainability, update layout and styling for better user experience

feat(ChatNavbar): add maximize and minimize icons for narrow mode toggle

style(markdown.scss, CodeBlockView): update border-radius for improved UI consistency and adjust CodeBlock styling

style(SettingsTab, OpenAISettingsGroup): adjust padding and minimum height for improved layout, comment out unused components for cleaner code

fix(i18n, MessageEditor, Settings): update localization keys for code settings and adjust border radius in MessageEditor, add gap to SettingRow for improved layout

feat(ChatNavbar, SVGIcon): add ExpandWidth icon and integrate it into ChatNavbar for narrow mode toggle

refactor(ImageBlock, MessageBlockRenderer): enhance image block styling and layout for better responsiveness

refactor(MessageAttachments): enhance file icon rendering and filename display with truncation for better UX

feat(MainSidebar, AssistantItem): integrate AssistantItem into MainSidebar for topic view, enhance event handling and styling adjustments

refactor(MessageAnchorLine): replace DownOutlined icon with CircleChevronDown for improved styling

feat(NarrowModeIcon, ChatNavbar): add NarrowModeIcon component and integrate it into ChatNavbar for narrow mode toggle

fix(MainSidebar, Inputbar, McpServersList): update event handling, adjust textarea rows, and enhance DragableList styling for improved layout and functionality

feat(MainSidebar): enhance submenu animation with framer-motion for improved user experience

style(CodeEditor, markdown.scss, SettingsTab): update border-radius to inherit and remove unused SettingDivider for improved consistency

style(MainNavbar, Message): adjust padding in MainNavbar and refine alignment logic in MessageItem for improved layout consistency

style(Inputbar, SelectModelButton): adjust margins and padding for improved layout consistency and add icon to SelectModelButton

wip

feat(MainSidebar): restructure sidebar components and add MainNavbar for enhanced navigation and user experience

style(MainSidebar, AssistantsTab): adjust margins and padding for improved layout consistency, integrate Scrollbar component for better scrolling experience

fix(MessageAnchorLine): prevent rendering of clear message type

refactor(SearchMessage, TopicMessages, MessagesService): update locateToMessage function to accept additional parameters for setting active assistant and topic, enhancing navigation logic

style(ColorStyles, Messages): update chat background colors for improved visibility and remove redundant background styles in message bubbles

revert: hide token show

refactor: settings tab

refactor(ChatNavbar): remove unused topic handling and simplify shortcut functions

fix(useChat): prevent setting active topic if it already exists in active assistant's topics

refactor(NavigationHandler, ChatNavbar, HomePage, MainSidebar): streamline navigation logic and remove unused code

chore: update react-router and react-router-dom to version 7.6.2, refactor routing logic in App component to use a RouteContainer for better location management

refactor(i18n): remove unused topic position settings and assistant display options from English, Japanese, Russian, Chinese, and Greek translations

refactor(MainSidebar, PinnedApps): remove unused imports and streamline component structure; update styling for better layout

refactor(TopicsTab): remove unused setTopicPosition function and streamline topic time display; update font size and family for TopicTime component

refactor(MainSidebar): integrate UserPopup for user settings access; streamline theme toggle logic and enhance styling for active menu items

refactor: remove topic position

fix(MainSidebar): update settings navigation path from '/settings/general' to '/settings/provider'

wip

refactor(SettingsTab): replace StyledSelect with Selector component and update styles for better UI consistency

chore(release): update fetch depth in GitHub Actions workflow for full history retrieval
2025-06-12 09:39:50 +08:00
167 changed files with 5171 additions and 2520 deletions

View File

@@ -176,6 +176,7 @@
"eslint-plugin-unused-imports": "^4.1.4", "eslint-plugin-unused-imports": "^4.1.4",
"fast-diff": "^1.3.0", "fast-diff": "^1.3.0",
"fast-xml-parser": "^5.2.0", "fast-xml-parser": "^5.2.0",
"framer-motion": "^12.17.3",
"franc-min": "^6.2.0", "franc-min": "^6.2.0",
"fs-extra": "^11.2.0", "fs-extra": "^11.2.0",
"google-auth-library": "^9.15.1", "google-auth-library": "^9.15.1",
@@ -206,8 +207,8 @@
"react-infinite-scroll-component": "^6.1.0", "react-infinite-scroll-component": "^6.1.0",
"react-markdown": "^10.1.0", "react-markdown": "^10.1.0",
"react-redux": "^9.1.2", "react-redux": "^9.1.2",
"react-router": "6", "react-router": "^7.6.2",
"react-router-dom": "6", "react-router-dom": "^7.6.2",
"react-spinners": "^0.14.1", "react-spinners": "^0.14.1",
"react-window": "^1.8.11", "react-window": "^1.8.11",
"redux": "^5.0.1", "redux": "^5.0.1",

View File

@@ -242,5 +242,12 @@ export enum IpcChannel {
Selection_ActionWindowMinimize = 'selection:action-window-minimize', Selection_ActionWindowMinimize = 'selection:action-window-minimize',
Selection_ActionWindowPin = 'selection:action-window-pin', Selection_ActionWindowPin = 'selection:action-window-pin',
Selection_ProcessAction = 'selection:process-action', Selection_ProcessAction = 'selection:process-action',
Selection_UpdateActionData = 'selection:update-action-data' Selection_UpdateActionData = 'selection:update-action-data',
// Navigation
Navigation_Url = 'navigation:url',
Navigation_Close = 'navigation:close',
// Settings Window
SettingsWindow_Show = 'settings-window:show'
} }

View File

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

View File

@@ -15,6 +15,7 @@ import { Notification } from 'src/renderer/src/types/notification'
import appService from './services/AppService' import appService from './services/AppService'
import AppUpdater from './services/AppUpdater' import AppUpdater from './services/AppUpdater'
import BackupManager from './services/BackupManager' import BackupManager from './services/BackupManager'
import { CacheService } from './services/CacheService'
import { configManager } from './services/ConfigManager' import { configManager } from './services/ConfigManager'
import CopilotService from './services/CopilotService' import CopilotService from './services/CopilotService'
import { ExportService } from './services/ExportService' import { ExportService } from './services/ExportService'
@@ -30,6 +31,7 @@ import { pythonService } from './services/PythonService'
import { FileServiceManager } from './services/remotefile/FileServiceManager' import { FileServiceManager } from './services/remotefile/FileServiceManager'
import { searchService } from './services/SearchService' import { searchService } from './services/SearchService'
import { SelectionService } from './services/SelectionService' import { SelectionService } from './services/SelectionService'
import { SettingsWindowService } from './services/SettingsWindowService'
import { registerShortcuts, unregisterAllShortcuts } from './services/ShortcutService' import { registerShortcuts, unregisterAllShortcuts } from './services/ShortcutService'
import storeSyncService from './services/StoreSyncService' import storeSyncService from './services/StoreSyncService'
import { themeService } from './services/ThemeService' import { themeService } from './services/ThemeService'
@@ -577,4 +579,12 @@ export function registerIpc(mainWindow: BrowserWindow, app: Electron.App) {
ipcMain.handle(IpcChannel.App_SetDisableHardwareAcceleration, (_, isDisable: boolean) => { ipcMain.handle(IpcChannel.App_SetDisableHardwareAcceleration, (_, isDisable: boolean) => {
configManager.setDisableHardwareAcceleration(isDisable) configManager.setDisableHardwareAcceleration(isDisable)
}) })
// Navigation
ipcMain.handle(IpcChannel.Navigation_Url, (_, url: string) => {
CacheService.set('navigation-url', url)
})
// Settings Window
SettingsWindowService.registerIpcHandler()
} }

View File

@@ -1,7 +1,7 @@
interface CacheItem<T> { interface CacheItem<T> {
data: T data: T
timestamp: number timestamp: number
duration: number duration?: number
} }
export class CacheService { export class CacheService {
@@ -11,9 +11,9 @@ export class CacheService {
* Set cache * Set cache
* @param key Cache key * @param key Cache key
* @param data Cache data * @param data Cache data
* @param duration Cache duration (in milliseconds) * @param duration Cache duration (in milliseconds), if not set the cache will never expire
*/ */
static set<T>(key: string, data: T, duration: number): void { static set<T>(key: string, data: T, duration?: number): void {
this.cache.set(key, { this.cache.set(key, {
data, data,
timestamp: Date.now(), timestamp: Date.now(),
@@ -30,6 +30,11 @@ export class CacheService {
const item = this.cache.get(key) const item = this.cache.get(key)
if (!item) return null if (!item) return null
// If duration is undefined, cache never expires
if (item.duration === undefined) {
return item.data
}
const now = Date.now() const now = Date.now()
if (now - item.timestamp > item.duration) { if (now - item.timestamp > item.duration) {
this.remove(key) this.remove(key)
@@ -63,6 +68,11 @@ export class CacheService {
const item = this.cache.get(key) const item = this.cache.get(key)
if (!item) return false if (!item) return false
// If duration is undefined, cache never expires
if (item.duration === undefined) {
return true
}
const now = Date.now() const now = Date.now()
if (now - item.timestamp > item.duration) { if (now - item.timestamp > item.duration) {
this.remove(key) this.remove(key)

View File

@@ -0,0 +1,149 @@
import { is } from '@electron-toolkit/utils'
import { isLinux, isMac } from '@main/constant'
import { IpcChannel } from '@shared/IpcChannel'
import { BrowserWindow, nativeTheme } from 'electron'
import { join } from 'path'
import icon from '../../../build/icon.png?asset'
import { titleBarOverlayDark, titleBarOverlayLight } from '../config'
export class SettingsWindowService {
private static instance: SettingsWindowService | null = null
private settingsWindow: BrowserWindow | null = null
public static getInstance(): SettingsWindowService {
if (!SettingsWindowService.instance) {
SettingsWindowService.instance = new SettingsWindowService()
}
return SettingsWindowService.instance
}
public createSettingsWindow(defaultTab?: string): BrowserWindow {
if (this.settingsWindow && !this.settingsWindow.isDestroyed()) {
this.settingsWindow.show()
this.settingsWindow.focus()
return this.settingsWindow
}
this.settingsWindow = new BrowserWindow({
width: 1000,
height: 700,
minWidth: 900,
minHeight: 600,
show: false,
autoHideMenuBar: true,
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: 12, y: 12 },
...(isLinux ? { icon } : {}),
webPreferences: {
preload: join(__dirname, '../preload/index.js'),
sandbox: false,
webSecurity: false,
webviewTag: true,
allowRunningInsecureContent: true,
backgroundThrottling: false
}
})
this.setupSettingsWindow()
this.loadSettingsWindowContent(defaultTab)
return this.settingsWindow
}
private setupSettingsWindow() {
if (!this.settingsWindow) return
this.settingsWindow.on('ready-to-show', () => {
this.settingsWindow?.show()
})
this.settingsWindow.on('closed', () => {
this.settingsWindow = null
})
this.settingsWindow.on('close', () => {
// Clean up when window is closed
})
// Handle theme changes
nativeTheme.on('updated', () => {
if (this.settingsWindow && !this.settingsWindow.isDestroyed()) {
this.settingsWindow.setTitleBarOverlay(
nativeTheme.shouldUseDarkColors ? titleBarOverlayDark : titleBarOverlayLight
)
}
})
}
private loadSettingsWindowContent(defaultTab?: string) {
if (!this.settingsWindow) return
const queryParam = defaultTab ? `?tab=${defaultTab}` : ''
if (is.dev && process.env['ELECTRON_RENDERER_URL']) {
this.settingsWindow.loadURL(process.env['ELECTRON_RENDERER_URL'] + '/settingsWindow.html' + queryParam)
} else {
this.settingsWindow.loadFile(join(__dirname, '../renderer/settingsWindow.html'))
if (defaultTab) {
this.settingsWindow.webContents.once('did-finish-load', () => {
this.settingsWindow?.webContents.send(IpcChannel.SettingsWindow_Show, { defaultTab })
})
}
}
}
public showSettingsWindow(defaultTab?: string) {
if (this.settingsWindow && !this.settingsWindow.isDestroyed()) {
if (this.settingsWindow.isMinimized()) {
this.settingsWindow.restore()
}
if (!isLinux) {
this.settingsWindow.setVisibleOnAllWorkspaces(true)
}
this.settingsWindow.show()
this.settingsWindow.focus()
if (!isLinux) {
this.settingsWindow.setVisibleOnAllWorkspaces(false)
}
} else {
this.createSettingsWindow(defaultTab)
}
}
public hideSettingsWindow() {
if (this.settingsWindow && !this.settingsWindow.isDestroyed()) {
this.settingsWindow.hide()
}
}
public closeSettingsWindow() {
if (this.settingsWindow && !this.settingsWindow.isDestroyed()) {
this.settingsWindow.close()
}
}
public getSettingsWindow(): BrowserWindow | null {
return this.settingsWindow
}
public static registerIpcHandler() {
const { ipcMain } = require('electron')
const service = SettingsWindowService.getInstance()
ipcMain.handle(IpcChannel.SettingsWindow_Show, (_, options?: { defaultTab?: string }) => {
service.showSettingsWindow(options?.defaultTab)
})
}
}
export const settingsWindowService = SettingsWindowService.getInstance()

View File

@@ -5,10 +5,12 @@ import Logger from 'electron-log'
import { configManager } from './ConfigManager' import { configManager } from './ConfigManager'
import selectionService from './SelectionService' import selectionService from './SelectionService'
import { settingsWindowService } from './SettingsWindowService'
import { windowService } from './WindowService' import { windowService } from './WindowService'
let showAppAccelerator: string | null = null let showAppAccelerator: string | null = null
let showMiniWindowAccelerator: string | null = null let showMiniWindowAccelerator: string | null = null
let showSettingsAccelerator: string | null = null
let selectionAssistantToggleAccelerator: string | null = null let selectionAssistantToggleAccelerator: string | null = null
let selectionAssistantSelectTextAccelerator: string | null = null let selectionAssistantSelectTextAccelerator: string | null = null
@@ -26,6 +28,10 @@ function getShortcutHandler(shortcut: Shortcut) {
return (window: BrowserWindow) => handleZoomFactor([window], -0.1) return (window: BrowserWindow) => handleZoomFactor([window], -0.1)
case 'zoom_reset': case 'zoom_reset':
return (window: BrowserWindow) => handleZoomFactor([window], 0, true) return (window: BrowserWindow) => handleZoomFactor([window], 0, true)
case 'show_settings':
return () => {
settingsWindowService.showSettingsWindow()
}
case 'show_app': case 'show_app':
return () => { return () => {
windowService.toggleMainWindow() windowService.toggleMainWindow()
@@ -146,9 +152,13 @@ export function registerShortcuts(window: BrowserWindow) {
// only register universal shortcuts when needed // only register universal shortcuts when needed
if ( if (
onlyUniversalShortcuts && onlyUniversalShortcuts &&
!['show_app', 'mini_window', 'selection_assistant_toggle', 'selection_assistant_select_text'].includes( ![
shortcut.key 'show_app',
) 'mini_window',
'show_settings',
'selection_assistant_toggle',
'selection_assistant_select_text'
].includes(shortcut.key)
) { ) {
return return
} }
@@ -171,6 +181,10 @@ export function registerShortcuts(window: BrowserWindow) {
showMiniWindowAccelerator = formatShortcutKey(shortcut.shortcut) showMiniWindowAccelerator = formatShortcutKey(shortcut.shortcut)
break break
case 'show_settings':
showSettingsAccelerator = formatShortcutKey(shortcut.shortcut)
break
case 'selection_assistant_toggle': case 'selection_assistant_toggle':
selectionAssistantToggleAccelerator = formatShortcutKey(shortcut.shortcut) selectionAssistantToggleAccelerator = formatShortcutKey(shortcut.shortcut)
break break
@@ -222,6 +236,12 @@ export function registerShortcuts(window: BrowserWindow) {
handler && globalShortcut.register(accelerator, () => handler(window)) handler && globalShortcut.register(accelerator, () => handler(window))
} }
if (showSettingsAccelerator) {
const handler = getShortcutHandler({ key: 'show_settings' } as Shortcut)
const accelerator = convertShortcutFormat(showSettingsAccelerator)
handler && globalShortcut.register(accelerator, () => handler(window))
}
if (selectionAssistantToggleAccelerator) { if (selectionAssistantToggleAccelerator) {
const handler = getShortcutHandler({ key: 'selection_assistant_toggle' } as Shortcut) const handler = getShortcutHandler({ key: 'selection_assistant_toggle' } as Shortcut)
const accelerator = convertShortcutFormat(selectionAssistantToggleAccelerator) const accelerator = convertShortcutFormat(selectionAssistantToggleAccelerator)
@@ -258,6 +278,7 @@ export function unregisterAllShortcuts() {
try { try {
showAppAccelerator = null showAppAccelerator = null
showMiniWindowAccelerator = null showMiniWindowAccelerator = null
showSettingsAccelerator = null
selectionAssistantToggleAccelerator = null selectionAssistantToggleAccelerator = null
selectionAssistantSelectTextAccelerator = null selectionAssistantSelectTextAccelerator = null
windowOnHandlers.forEach((handlers, window) => { windowOnHandlers.forEach((handlers, window) => {

View File

@@ -12,6 +12,7 @@ import { join } from 'path'
import icon from '../../../build/icon.png?asset' import icon from '../../../build/icon.png?asset'
import { titleBarOverlayDark, titleBarOverlayLight } from '../config' import { titleBarOverlayDark, titleBarOverlayLight } from '../config'
import { CacheService } from './CacheService'
import { configManager } from './ConfigManager' import { configManager } from './ConfigManager'
import { contextMenu } from './ContextMenu' import { contextMenu } from './ContextMenu'
import { initSessionUserAgent } from './WebviewService' import { initSessionUserAgent } from './WebviewService'
@@ -63,7 +64,7 @@ export class WindowService {
titleBarOverlay: nativeTheme.shouldUseDarkColors ? titleBarOverlayDark : titleBarOverlayLight, titleBarOverlay: nativeTheme.shouldUseDarkColors ? titleBarOverlayDark : titleBarOverlayLight,
backgroundColor: isMac ? undefined : nativeTheme.shouldUseDarkColors ? '#181818' : '#FFFFFF', backgroundColor: isMac ? undefined : nativeTheme.shouldUseDarkColors ? '#181818' : '#FFFFFF',
darkTheme: nativeTheme.shouldUseDarkColors, darkTheme: nativeTheme.shouldUseDarkColors,
trafficLightPosition: { x: 8, y: 12 }, trafficLightPosition: { x: 12, y: 12 },
...(isLinux ? { icon } : {}), ...(isLinux ? { icon } : {}),
webPreferences: { webPreferences: {
preload: join(__dirname, '../preload/index.js'), preload: join(__dirname, '../preload/index.js'),
@@ -313,6 +314,11 @@ export class WindowService {
return app.quit() return app.quit()
} }
if (CacheService.get('navigation-url') !== '/') {
event.preventDefault()
return mainWindow.webContents.send(IpcChannel.Navigation_Close)
}
// 托盘及关闭行为设置 // 托盘及关闭行为设置
const isShowTray = configManager.getTray() const isShowTray = configManager.getTray()
const isTrayOnClose = configManager.getTrayOnClose() const isTrayOnClose = configManager.getTrayOnClose()
@@ -435,8 +441,9 @@ export class WindowService {
show: false, show: false,
autoHideMenuBar: true, autoHideMenuBar: true,
transparent: isMac, transparent: isMac,
vibrancy: 'under-window', vibrancy: isMac ? 'under-window' : undefined,
visualEffectState: 'followWindow', visualEffectState: isMac ? 'followWindow' : undefined,
backgroundMaterial: isWin ? 'acrylic' : undefined,
center: true, center: true,
frame: false, frame: false,
alwaysOnTop: true, alwaysOnTop: true,

View File

@@ -318,8 +318,25 @@ const api = {
pinActionWindow: (isPinned: boolean) => ipcRenderer.invoke(IpcChannel.Selection_ActionWindowPin, isPinned) pinActionWindow: (isPinned: boolean) => ipcRenderer.invoke(IpcChannel.Selection_ActionWindowPin, isPinned)
}, },
quoteToMainWindow: (text: string) => ipcRenderer.invoke(IpcChannel.App_QuoteToMain, text), quoteToMainWindow: (text: string) => ipcRenderer.invoke(IpcChannel.App_QuoteToMain, text),
navigation: {
url: (url: string) => ipcRenderer.invoke(IpcChannel.Navigation_Url, url)
},
setDisableHardwareAcceleration: (isDisable: boolean) => setDisableHardwareAcceleration: (isDisable: boolean) =>
ipcRenderer.invoke(IpcChannel.App_SetDisableHardwareAcceleration, isDisable) ipcRenderer.invoke(IpcChannel.App_SetDisableHardwareAcceleration, isDisable),
// Settings Window
showSettingsWindow: (options?: { defaultTab?: string }) =>
ipcRenderer.invoke(IpcChannel.SettingsWindow_Show, options),
on: (channel: string, func: any) => {
const listener = (_event: Electron.IpcRendererEvent, ...args: any[]) => {
func(...args)
}
ipcRenderer.on(channel, listener)
return () => {
ipcRenderer.off(channel, listener)
}
}
} }
// Use `contextBridge` APIs to expose Electron APIs to // Use `contextBridge` APIs to expose Electron APIs to

View File

@@ -0,0 +1,23 @@
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="initial-scale=1, width=device-width" />
<meta
http-equiv="Content-Security-Policy"
content="default-src 'self'; connect-src blob: *; script-src 'self' 'unsafe-eval' *; worker-src 'self' blob:; style-src 'self' 'unsafe-inline' *; font-src 'self' data: *; img-src 'self' data: file: * blob:; frame-src * file:" />
<title>Cherry Studio</title>
<style>
html,
body {
margin: 0;
}
</style>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/windows/settings/entryPoint.tsx"></script>
</body>
</html>

View File

@@ -2,10 +2,10 @@ import '@renderer/databases'
import store, { persistor } from '@renderer/store' import store, { persistor } from '@renderer/store'
import { Provider } from 'react-redux' import { Provider } from 'react-redux'
import { HashRouter, Route, Routes } from 'react-router-dom' import { HashRouter } from 'react-router-dom'
import { PersistGate } from 'redux-persist/integration/react' import { PersistGate } from 'redux-persist/integration/react'
import Sidebar from './components/app/Sidebar' import AppLayout from './components/Layout/AppLayout'
import TopViewContainer from './components/TopView' import TopViewContainer from './components/TopView'
import AntdProvider from './context/AntdProvider' import AntdProvider from './context/AntdProvider'
import { CodeStyleProvider } from './context/CodeStyleProvider' import { CodeStyleProvider } from './context/CodeStyleProvider'
@@ -13,14 +13,8 @@ import { NotificationProvider } from './context/NotificationProvider'
import StyleSheetManager from './context/StyleSheetManager' import StyleSheetManager from './context/StyleSheetManager'
import { ThemeProvider } from './context/ThemeProvider' import { ThemeProvider } from './context/ThemeProvider'
import NavigationHandler from './handler/NavigationHandler' import NavigationHandler from './handler/NavigationHandler'
import AgentsPage from './pages/agents/AgentsPage' import { ChatProvider } from './hooks/useChat'
import AppsPage from './pages/apps/AppsPage' import Routes from './Routes'
import FilesPage from './pages/files/FilesPage'
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 { function App(): React.ReactElement {
return ( return (
@@ -31,22 +25,16 @@ function App(): React.ReactElement {
<NotificationProvider> <NotificationProvider>
<CodeStyleProvider> <CodeStyleProvider>
<PersistGate loading={null} persistor={persistor}> <PersistGate loading={null} persistor={persistor}>
<TopViewContainer> <HashRouter>
<HashRouter> <TopViewContainer>
<NavigationHandler /> <NavigationHandler />
<Sidebar /> <ChatProvider>
<Routes> <AppLayout>
<Route path="/" element={<HomePage />} /> <Routes />
<Route path="/agents" element={<AgentsPage />} /> </AppLayout>
<Route path="/paintings/*" element={<PaintingsRoutePage />} /> </ChatProvider>
<Route path="/translate" element={<TranslatePage />} /> </TopViewContainer>
<Route path="/files" element={<FilesPage />} /> </HashRouter>
<Route path="/knowledge" element={<KnowledgePage />} />
<Route path="/apps" element={<AppsPage />} />
<Route path="/settings/*" element={<SettingsPage />} />
</Routes>
</HashRouter>
</TopViewContainer>
</PersistGate> </PersistGate>
</CodeStyleProvider> </CodeStyleProvider>
</NotificationProvider> </NotificationProvider>

View File

@@ -0,0 +1,29 @@
import { Route, Routes } from 'react-router-dom'
import AgentsPage from './pages/agents/AgentsPage'
import AppsPage from './pages/apps/AppsPage'
import FilesPage from './pages/files/FilesPage'
import HomePage from './pages/home/HomePage'
import KnowledgePage from './pages/knowledge/KnowledgePage'
import McpServersPage from './pages/mcp-servers'
import PaintingsRoutePage from './pages/paintings/PaintingsRoutePage'
import TranslatePage from './pages/translate/TranslatePage'
const RouteContainer = () => {
return (
<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="/mcp-servers/*" element={<McpServersPage />} />
{/* <Route path="/settings/*" element={<SettingsPage />} />
<Route path="/launchpad" element={<LaunchpadPage />} /> */}
</Routes>
)
}
export default RouteContainer

View File

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

View File

@@ -29,7 +29,7 @@
--color-text-secondary: rgba(235, 235, 245, 0.7); --color-text-secondary: rgba(235, 235, 245, 0.7);
--color-icon: #ffffff99; --color-icon: #ffffff99;
--color-icon-white: #ffffff; --color-icon-white: #ffffff;
--color-border: #ffffff19; --color-border: #383838;
--color-border-soft: #ffffff10; --color-border-soft: #ffffff10;
--color-border-mute: #ffffff05; --color-border-mute: #ffffff05;
--color-error: #f44336; --color-error: #f44336;
@@ -54,25 +54,14 @@
--color-background-highlight-accent: rgba(255, 150, 50, 0.9); --color-background-highlight-accent: rgba(255, 150, 50, 0.9);
--navbar-background-mac: rgba(20, 20, 20, 0.55); --navbar-background-mac: rgba(20, 20, 20, 0.55);
--navbar-background-win: rgba(20, 20, 20, 0.75);
--navbar-background: #1f1f1f; --navbar-background: #1f1f1f;
--navbar-height: 40px;
--sidebar-width: 50px;
--status-bar-height: 40px;
--input-bar-height: 100px;
--assistants-width: 275px;
--topic-list-width: 275px;
--settings-width: 250px;
--scrollbar-width: 5px;
--chat-background: transparent; --chat-background: transparent;
--chat-background-user: rgba(255, 255, 255, 0.08); --chat-background-user: rgba(255, 255, 255, 0.08);
--chat-background-assistant: transparent; --chat-background-assistant: transparent;
--chat-text-user: var(--color-black); --chat-text-user: var(--color-black);
--list-item-border-radius: 20px;
--color-status-success: #52c41a; --color-status-success: #52c41a;
--color-status-error: #ff4d4f; --color-status-error: #ff4d4f;
--color-status-warning: #faad14; --color-status-warning: #faad14;
@@ -124,8 +113,8 @@
--color-reference-text: #000000; --color-reference-text: #000000;
--color-reference-background: #f1f7ff; --color-reference-background: #f1f7ff;
--color-list-item: #eee; --color-list-item: rgba(0, 0, 0, 0.05);
--color-list-item-hover: #f5f5f5; --color-list-item-hover: rgba(0, 0, 0, 0.03);
--modal-background: var(--color-white); --modal-background: var(--color-white);
@@ -134,6 +123,7 @@
--color-background-highlight-accent: rgba(255, 150, 50, 0.5); --color-background-highlight-accent: rgba(255, 150, 50, 0.5);
--navbar-background-mac: rgba(255, 255, 255, 0.55); --navbar-background-mac: rgba(255, 255, 255, 0.55);
--navbar-background-win: rgba(255, 255, 255, 0.75);
--navbar-background: rgba(244, 244, 244); --navbar-background: rgba(244, 244, 244);
--chat-background: transparent; --chat-background: transparent;

View File

@@ -1,6 +1,14 @@
#content-container { #content-container {
background-color: var(--color-background); background-color: var(--color-background);
border-top: 0.5px solid var(--color-border); border-top: 0.5px solid var(--color-border);
border-top-left-radius: 10px; }
border-left: 0.5px solid var(--color-border);
.group-container {
.context-menu-container {
width: 100%;
}
}
.context-menu-container {
max-width: 100%;
} }

View File

@@ -1,3 +1,4 @@
@use './variables.scss';
@use './color.scss'; @use './color.scss';
@use './font.scss'; @use './font.scss';
@use './markdown.scss'; @use './markdown.scss';

View File

@@ -0,0 +1,18 @@
:root {
--navbar-height: 42px;
--sidebar-width: 50px;
--status-bar-height: 40px;
--input-bar-height: 100px;
--assistants-width: 275px;
--topic-list-width: 275px;
--settings-width: 250px;
--scrollbar-width: 5px;
--list-item-border-radius: 15px;
--border-width: 0.5px;
--main-height: 100vh;
--border-width: 0.5px;
}

View File

@@ -292,6 +292,10 @@ const SplitViewWrapper = styled.div`
width: 100%; width: 100%;
} }
&:not(:has(+ .html-artifacts)) {
border-radius: 0 0 8px 8px;
}
&:not(:has(+ [class*='Container'])) { &:not(:has(+ [class*='Container'])) {
border-radius: 0 0 8px 8px; border-radius: 0 0 8px 8px;
overflow: hidden; overflow: hidden;

View File

@@ -10,8 +10,7 @@ import {
Text as UnWrapIcon, Text as UnWrapIcon,
WrapText as WrapIcon WrapText as WrapIcon
} from 'lucide-react' } from 'lucide-react'
import { useCallback, useEffect, useMemo, useRef, useState } from 'react' import { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { memo } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { useBlurHandler, useLanguageExtensions, useSaveKeymap } from './hooks' import { useBlurHandler, useLanguageExtensions, useSaveKeymap } from './hooks'

View File

@@ -348,20 +348,23 @@ export const ContentSearch = React.forwardRef<ContentSearchRef, Props>(
<ToolBar> <ToolBar>
<Tooltip title={t('button.includes_user_questions')} mouseEnterDelay={0.8} placement="bottom"> <Tooltip title={t('button.includes_user_questions')} mouseEnterDelay={0.8} placement="bottom">
<ToolbarButton type="text" onClick={userOutlinedButtonOnClick}> <ToolbarButton type="text" onClick={userOutlinedButtonOnClick}>
<User size={18} style={{ color: includeUser ? 'var(--color-link)' : 'var(--color-icon)' }} /> <User size={18} style={{ color: includeUser ? 'var(--color-primary)' : 'var(--color-icon)' }} />
</ToolbarButton> </ToolbarButton>
</Tooltip> </Tooltip>
<Tooltip title={t('button.case_sensitive')} mouseEnterDelay={0.8} placement="bottom"> <Tooltip title={t('button.case_sensitive')} mouseEnterDelay={0.8} placement="bottom">
<ToolbarButton type="text" onClick={caseSensitiveButtonOnClick}> <ToolbarButton type="text" onClick={caseSensitiveButtonOnClick}>
<CaseSensitive <CaseSensitive
size={18} size={18}
style={{ color: isCaseSensitive ? 'var(--color-link)' : 'var(--color-icon)' }} style={{ color: isCaseSensitive ? 'var(--color-primary)' : 'var(--color-icon)' }}
/> />
</ToolbarButton> </ToolbarButton>
</Tooltip> </Tooltip>
<Tooltip title={t('button.whole_word')} mouseEnterDelay={0.8} placement="bottom"> <Tooltip title={t('button.whole_word')} mouseEnterDelay={0.8} placement="bottom">
<ToolbarButton type="text" onClick={wholeWordButtonOnClick}> <ToolbarButton type="text" onClick={wholeWordButtonOnClick}>
<WholeWord size={18} style={{ color: isWholeWord ? 'var(--color-link)' : 'var(--color-icon)' }} /> <WholeWord
size={18}
style={{ color: isWholeWord ? 'var(--color-primary)' : 'var(--color-icon)' }}
/>
</ToolbarButton> </ToolbarButton>
</Tooltip> </Tooltip>
</ToolBar> </ToolBar>
@@ -406,7 +409,6 @@ const Container = styled.div`
` `
const SearchBarContainer = styled.div` const SearchBarContainer = styled.div`
border: 1px solid var(--color-primary);
border-radius: 10px; border-radius: 10px;
transition: all 0.2s ease; transition: all 0.2s ease;
position: fixed; position: fixed;
@@ -420,6 +422,7 @@ const SearchBarContainer = styled.div`
justify-content: center; justify-content: center;
background-color: var(--color-background); background-color: var(--color-background);
flex: 1 1 auto; /* Take up input's previous space */ flex: 1 1 auto; /* Take up input's previous space */
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
` `
const Placeholder = styled.div` const Placeholder = styled.div`

View File

@@ -0,0 +1,17 @@
import { includeKeywords } from '@renderer/utils/search'
import { Select, SelectProps } from 'antd'
/**
* 自定义 Select使用增强的搜索 filter
*/
const CustomSelect = ({ ref, ...props }: SelectProps & { ref?: React.RefObject<any | null> }) => {
return <Select ref={ref} filterOption={enhancedFilterOption} {...props} />
}
CustomSelect.displayName = 'CustomSelect'
function enhancedFilterOption(input: string, option: any) {
return includeKeywords(option.label, input)
}
export default CustomSelect

View File

@@ -51,23 +51,27 @@ const DraggableList: FC<Props<any>> = ({
<VirtualList data={list} itemKey="id"> <VirtualList data={list} itemKey="id">
{(item, index) => { {(item, index) => {
const id = item.id || item const id = item.id || item
return ( if (!item.disabled) {
<Draggable key={`draggable_${id}_${index}`} draggableId={id} index={index}> return (
{(provided) => ( <Draggable key={`draggable_${id}_${index}`} draggableId={id} index={index}>
<div {(provided) => (
ref={provided.innerRef} <div
{...provided.draggableProps} ref={provided.innerRef}
{...provided.dragHandleProps} {...provided.draggableProps}
style={{ {...provided.dragHandleProps}
...listStyle, style={{
...provided.draggableProps.style, marginBottom: 8,
marginBottom: 8 ...listStyle,
}}> ...provided.draggableProps.style
{children(item, index)} }}>
</div> {children(item, index)}
)} </div>
</Draggable> )}
) </Draggable>
)
} else {
return <div> {children(item, index)}</div>
}
}} }}
</VirtualList> </VirtualList>
{provided.placeholder} {provided.placeholder}

View File

@@ -0,0 +1,35 @@
import { FC } from 'react'
import styled from 'styled-components'
interface Props {
isNarrowMode: boolean
}
const NarrowModeIcon: FC<Props> = ({ isNarrowMode }) => {
return (
<Container $isNarrowMode={isNarrowMode}>
<Line />
<Line />
</Container>
)
}
const Container = styled.div<{ $isNarrowMode: boolean }>`
width: 16px;
height: 16px;
border: 1.5px solid var(--color-text-2);
border-radius: 3px;
display: flex;
align-items: center;
justify-content: ${({ $isNarrowMode }) => ($isNarrowMode ? 'space-evenly' : 'space-between')};
padding: 2px;
`
const Line = styled.div`
width: 2px;
height: 10px;
background-color: var(--color-text-2);
border-radius: 5px;
`
export default NarrowModeIcon

View File

@@ -0,0 +1,42 @@
import { SVGProps } from 'react'
interface PanelIconProps extends Omit<SVGProps<SVGSVGElement>, 'width' | 'height'> {
size?: number | string
expanded?: boolean
}
export const PanelLeftIcon = ({ size = 18, expanded = false, ...props }: PanelIconProps) => (
<svg
xmlns="http://www.w3.org/2000/svg"
width={size}
height={size}
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
className="lucide lucide-panel-left-icon lucide-panel-left"
{...props}>
<rect width="18" height="18" x="3" y="3" rx="2" />
{expanded ? <path d="M10 7v10" strokeWidth={4} /> : <path d="M9 6v12" strokeWidth={2} />}
</svg>
)
export const PanelRightIcon = ({ size = 18, expanded = false, ...props }: PanelIconProps) => (
<svg
xmlns="http://www.w3.org/2000/svg"
width={size}
height={size}
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
className="lucide lucide-panel-right-icon lucide-panel-right"
{...props}>
<rect width="18" height="18" x="3" y="3" rx="2" />
{expanded ? <path d="M14 7v10" strokeWidth={4} /> : <path d="M15 6v12" strokeWidth={2} />}
</svg>
)

View File

@@ -22,7 +22,7 @@ const Container = styled.div`
` `
const Icon = styled.i` const Icon = styled.i`
color: var(--color-link); color: var(--color-primary);
font-size: 16px; font-size: 16px;
margin-right: 6px; margin-right: 6px;
` `

View File

@@ -77,3 +77,18 @@ export function MdiLightbulbOn90(props: SVGProps<SVGSVGElement>) {
</svg> </svg>
) )
} }
export function ExpandWidth(props: SVGProps<SVGSVGElement>) {
return (
<svg width="1em" height="1em" viewBox="0 0 200 200" fill="none" xmlns="http://www.w3.org/2000/svg" {...props}>
<g>
<path
id="path"
d="M0 25L0 175C0 179.41 3.58 183 8 183L12 183C16.41 183 20 179.41 20 175L20 25C20 20.58 16.41 17 12 17L8 17C3.58 17 0 20.58 0 25ZM60.41 58.43L29.42 94.81C26.87 97.8 26.87 102.19 29.42 105.18L60.38 141.53C65.2 147.19 74.47 143.78 74.47 136.34L74.47 121.83C74.47 117.41 78.06 113.83 82.47 113.83L117.5 113.83C121.91 113.83 125.5 117.41 125.5 121.83L125.5 136.35C125.5 143.78 134.76 147.2 139.58 141.54L170.57 105.18C173.12 102.19 173.12 97.8 170.57 94.81L139.59 58.43C134.76 52.77 125.5 56.18 125.5 63.62L125.5 78.16C125.5 82.58 121.91 86.16 117.5 86.16L82.5 86.16C78.08 86.16 74.5 82.58 74.5 78.16L74.5 63.62C74.5 56.18 65.23 52.77 60.41 58.43ZM188 17L192 17C196.41 17 200 20.58 200 25L200 175C200 179.41 196.41 183 192 183L188 183C183.58 183 180 179.41 180 175L180 25C180 20.58 183.58 17 188 17Z"
fill="currentColor"
fillRule="nonzero"
/>
</g>
</svg>
)
}

View File

@@ -23,7 +23,7 @@ const Container = styled.div`
` `
const Icon = styled(GlobalOutlined)` const Icon = styled(GlobalOutlined)`
color: var(--color-link); color: var(--color-primary);
font-size: 15px; font-size: 15px;
margin-right: 6px; margin-right: 6px;
` `

View File

@@ -0,0 +1,27 @@
import { HStack } from '@renderer/components/Layout'
import MainSidebar from '@renderer/pages/home/MainSidebar/MainSidebar'
import { FC } from 'react'
import styled from 'styled-components'
interface AppLayoutProps {
children: React.ReactNode
}
const AppLayout: FC<AppLayoutProps> = ({ children }) => {
return (
<HStack style={{ display: 'flex', flex: 1 }} id="app-layout">
<MainSidebar />
<ContentArea>{children}</ContentArea>
</HStack>
)
}
const ContentArea = styled.div`
min-width: 0;
display: flex;
flex: 1;
flex-direction: column;
height: 100vh;
`
export default AppLayout

View File

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

View File

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

View File

@@ -1,5 +1,4 @@
import { TopView } from '@renderer/components/TopView' import { TopView } from '@renderer/components/TopView'
import { useAgents } from '@renderer/hooks/useAgents'
import { useAssistants, useDefaultAssistant } from '@renderer/hooks/useAssistant' import { useAssistants, useDefaultAssistant } from '@renderer/hooks/useAssistant'
import { useSystemAgents } from '@renderer/pages/agents' import { useSystemAgents } from '@renderer/pages/agents'
import { createAssistantFromAgent } from '@renderer/services/AssistantService' import { createAssistantFromAgent } from '@renderer/services/AssistantService'
@@ -24,7 +23,7 @@ interface Props {
const PopupContainer: React.FC<Props> = ({ resolve }) => { const PopupContainer: React.FC<Props> = ({ resolve }) => {
const [open, setOpen] = useState(true) const [open, setOpen] = useState(true)
const { t } = useTranslation() const { t } = useTranslation()
const { agents: userAgents } = useAgents() const { assistants: userAgents } = useAssistants()
const [searchText, setSearchText] = useState('') const [searchText, setSearchText] = useState('')
const { defaultAssistant } = useDefaultAssistant() const { defaultAssistant } = useDefaultAssistant()
const { assistants, addAssistant } = useAssistants() const { assistants, addAssistant } = useAssistants()

View File

@@ -14,14 +14,7 @@ interface Props {
position: 'left' | 'right' position: 'left' | 'right'
} }
const FloatingSidebar: FC<Props> = ({ const FloatingSidebar: FC<Props> = ({ children, activeAssistant, setActiveAssistant, activeTopic, setActiveTopic }) => {
children,
activeAssistant,
setActiveAssistant,
activeTopic,
setActiveTopic,
position = 'left'
}) => {
const [open, setOpen] = useState(false) const [open, setOpen] = useState(false)
useHotkeys('esc', () => { useHotkeys('esc', () => {
@@ -45,12 +38,11 @@ const FloatingSidebar: FC<Props> = ({
const content = ( const content = (
<PopoverContent maxHeight={maxHeight}> <PopoverContent maxHeight={maxHeight}>
<HomeTabs <HomeTabs
tab="assistants"
activeAssistant={activeAssistant} activeAssistant={activeAssistant}
activeTopic={activeTopic} activeTopic={activeTopic}
setActiveAssistant={setActiveAssistant} setActiveAssistant={setActiveAssistant}
setActiveTopic={setActiveTopic} setActiveTopic={setActiveTopic}
position={position}
forceToSeeAllTab={true}
style={{ style={{
background: 'transparent', background: 'transparent',
border: 'none', border: 'none',

View File

@@ -0,0 +1,375 @@
import { useCodeStyle } from '@renderer/context/CodeStyleProvider'
import { useTheme } from '@renderer/context/ThemeProvider'
import { useSettings } from '@renderer/hooks/useSettings'
import { SettingDivider, SettingRow, SettingRowTitle } from '@renderer/pages/settings'
import { useAppDispatch } from '@renderer/store'
import {
setCodeCollapsible,
setCodeEditor,
setCodeExecution,
setCodePreview,
setCodeShowLineNumbers,
setCodeWrappable,
setFontSize,
setMathEngine,
setMessageFont,
setMessageNavigation,
setMessageStyle,
setMultiModelMessageStyle,
setShowMessageDivider,
setThoughtAutoCollapse
} from '@renderer/store/settings'
import { CodeStyleVarious, MathEngine, ThemeMode } from '@renderer/types'
import { Col, InputNumber, Modal, Row, Slider, Switch, Tooltip } from 'antd'
import { CircleHelp } from 'lucide-react'
import { useCallback, useMemo, useState } from 'react'
import { useTranslation } from 'react-i18next'
import styled from 'styled-components'
import Selector from '../Selector'
import { TopView } from '../TopView'
interface ShowParams {
title: string
}
interface Props extends ShowParams {
resolve: (data: any) => void
}
const PopupContainer: React.FC<Props> = ({ title, resolve }) => {
const [open, setOpen] = useState(true)
const { messageStyle, fontSize } = useSettings()
const { theme } = useTheme()
const { themeNames } = useCodeStyle()
const [fontSizeValue, setFontSizeValue] = useState(fontSize)
const { t } = useTranslation()
const dispatch = useAppDispatch()
const {
showMessageDivider,
messageFont,
codeShowLineNumbers,
codeCollapsible,
codeWrappable,
codeEditor,
codePreview,
codeExecution,
mathEngine,
multiModelMessageStyle,
thoughtAutoCollapse,
messageNavigation
} = useSettings()
const codeStyle = useMemo(() => {
return codeEditor.enabled
? theme === ThemeMode.light
? codeEditor.themeLight
: codeEditor.themeDark
: theme === ThemeMode.light
? codePreview.themeLight
: codePreview.themeDark
}, [
codeEditor.enabled,
codeEditor.themeLight,
codeEditor.themeDark,
theme,
codePreview.themeLight,
codePreview.themeDark
])
const onCodeStyleChange = useCallback(
(value: CodeStyleVarious) => {
const field = theme === ThemeMode.light ? 'themeLight' : 'themeDark'
const action = codeEditor.enabled ? setCodeEditor : setCodePreview
dispatch(action({ [field]: value }))
},
[dispatch, theme, codeEditor.enabled]
)
const onOk = () => {
resolve(true)
setOpen(false)
}
const onCancel = () => {
resolve(false)
setOpen(false)
}
const onClose = () => {
TopView.hide(TopViewKey)
}
MessageSettingsPopup.hide = onCancel
return (
<Modal
title={title}
open={open}
onOk={onOk}
onCancel={onCancel}
afterClose={onClose}
transitionName="animation-move-down"
footer={null}
centered
styles={{ body: { maxHeight: '70vh', overflowY: 'auto' } }}>
<SettingGroup>
<SettingRow>
<SettingRowTitleSmall>{t('settings.messages.divider')}</SettingRowTitleSmall>
<Switch
size="small"
checked={showMessageDivider}
onChange={(checked) => dispatch(setShowMessageDivider(checked))}
/>
</SettingRow>
<SettingDivider />
<SettingRow>
<SettingRowTitleSmall>{t('settings.messages.use_serif_font')}</SettingRowTitleSmall>
<Switch
size="small"
checked={messageFont === 'serif'}
onChange={(checked) => dispatch(setMessageFont(checked ? 'serif' : 'system'))}
/>
</SettingRow>
<SettingDivider />
<SettingRow>
<SettingRowTitleSmall>
{t('chat.settings.thought_auto_collapse')}
<Tooltip title={t('chat.settings.thought_auto_collapse.tip')}>
<CircleHelp size={14} style={{ marginLeft: 4 }} color="var(--color-text-2)" />
</Tooltip>
</SettingRowTitleSmall>
<Switch
size="small"
checked={thoughtAutoCollapse}
onChange={(checked) => dispatch(setThoughtAutoCollapse(checked))}
/>
</SettingRow>
<SettingDivider />
<SettingRow>
<SettingRowTitleSmall>{t('message.message.style')}</SettingRowTitleSmall>
<Selector
value={messageStyle}
onChange={(value) => dispatch(setMessageStyle(value as 'plain' | 'bubble'))}
options={[
{ label: t('message.message.style.plain'), value: 'plain' },
{ label: t('message.message.style.bubble'), value: 'bubble' }
]}
/>
</SettingRow>
<SettingDivider />
<SettingRow>
<SettingRowTitleSmall>{t('message.message.multi_model_style')}</SettingRowTitleSmall>
<Selector
value={multiModelMessageStyle}
onChange={(value) =>
dispatch(setMultiModelMessageStyle(value as 'fold' | 'vertical' | 'horizontal' | 'grid'))
}
options={[
{ label: t('message.message.multi_model_style.fold'), value: 'fold' },
{ label: t('message.message.multi_model_style.vertical'), value: 'vertical' },
{ label: t('message.message.multi_model_style.horizontal'), value: 'horizontal' },
{ label: t('message.message.multi_model_style.grid'), value: 'grid' }
]}
/>
</SettingRow>
<SettingDivider />
<SettingRow>
<SettingRowTitleSmall>{t('settings.messages.navigation')}</SettingRowTitleSmall>
<Selector
value={messageNavigation}
onChange={(value) => dispatch(setMessageNavigation(value as 'none' | 'buttons' | 'anchor'))}
options={[
{ label: t('settings.messages.navigation.none'), value: 'none' },
{ label: t('settings.messages.navigation.buttons'), value: 'buttons' },
{ label: t('settings.messages.navigation.anchor'), value: 'anchor' }
]}
/>
</SettingRow>
<SettingDivider />
<SettingRow>
<SettingRowTitleSmall>{t('settings.messages.math_engine')}</SettingRowTitleSmall>
<Selector
value={mathEngine}
onChange={(value) => dispatch(setMathEngine(value as MathEngine))}
options={[
{ label: 'KaTeX', value: 'KaTeX' },
{ label: 'MathJax', value: 'MathJax' },
{ label: t('settings.messages.math_engine.none'), value: 'none' }
]}
/>
</SettingRow>
<SettingDivider />
<SettingRow>
<SettingRowTitleSmall>{t('settings.font_size.title')}</SettingRowTitleSmall>
</SettingRow>
<Row align="middle" gutter={10}>
<Col span={24}>
<Slider
value={fontSizeValue}
onChange={(value) => setFontSizeValue(value)}
onChangeComplete={(value) => dispatch(setFontSize(value))}
min={12}
max={22}
step={1}
marks={{
12: <span style={{ fontSize: '12px' }}>A</span>,
14: <span style={{ fontSize: '14px' }}>{t('common.default')}</span>,
22: <span style={{ fontSize: '18px' }}>A</span>
}}
/>
</Col>
</Row>
</SettingGroup>
<SettingGroup>
<SettingRow>
<SettingRowTitleSmall>{t('message.message.code_style')}</SettingRowTitleSmall>
<Selector
value={codeStyle}
onChange={(value) => onCodeStyleChange(value as CodeStyleVarious)}
options={themeNames.map((theme) => ({ label: theme, value: theme }))}
/>
</SettingRow>
<SettingDivider />
<SettingRow>
<SettingRowTitleSmall>
{t('chat.settings.code_execution.title')}
<Tooltip title={t('chat.settings.code_execution.tip')}>
<CircleHelp size={14} style={{ marginLeft: 4 }} color="var(--color-text-2)" />
</Tooltip>
</SettingRowTitleSmall>
<Switch
size="small"
checked={codeExecution.enabled}
onChange={(checked) => dispatch(setCodeExecution({ enabled: checked }))}
/>
</SettingRow>
{codeExecution.enabled && (
<>
<SettingDivider />
<SettingRow style={{ paddingLeft: 8 }}>
<SettingRowTitleSmall>
{t('chat.settings.code_execution.timeout_minutes')}
<Tooltip title={t('chat.settings.code_execution.timeout_minutes.tip')}>
<CircleHelp size={14} style={{ marginLeft: 4 }} color="var(--color-text-2)" />
</Tooltip>
</SettingRowTitleSmall>
<InputNumber
size="small"
min={1}
max={60}
step={1}
value={codeExecution.timeoutMinutes}
onChange={(value) => dispatch(setCodeExecution({ timeoutMinutes: value ?? 1 }))}
style={{ width: 80 }}
/>
</SettingRow>
</>
)}
<SettingDivider />
<SettingRow>
<SettingRowTitleSmall>{t('chat.settings.code_editor.title')}</SettingRowTitleSmall>
<Switch
size="small"
checked={codeEditor.enabled}
onChange={(checked) => dispatch(setCodeEditor({ enabled: checked }))}
/>
</SettingRow>
{codeEditor.enabled && (
<>
<SettingDivider />
<SettingRow style={{ paddingLeft: 8 }}>
<SettingRowTitleSmall>{t('chat.settings.code_editor.highlight_active_line')}</SettingRowTitleSmall>
<Switch
size="small"
checked={codeEditor.highlightActiveLine}
onChange={(checked) => dispatch(setCodeEditor({ highlightActiveLine: checked }))}
/>
</SettingRow>
<SettingDivider />
<SettingRow style={{ paddingLeft: 8 }}>
<SettingRowTitleSmall>{t('chat.settings.code_editor.fold_gutter')}</SettingRowTitleSmall>
<Switch
size="small"
checked={codeEditor.foldGutter}
onChange={(checked) => dispatch(setCodeEditor({ foldGutter: checked }))}
/>
</SettingRow>
<SettingDivider />
<SettingRow style={{ paddingLeft: 8 }}>
<SettingRowTitleSmall>{t('chat.settings.code_editor.autocompletion')}</SettingRowTitleSmall>
<Switch
size="small"
checked={codeEditor.autocompletion}
onChange={(checked) => dispatch(setCodeEditor({ autocompletion: checked }))}
/>
</SettingRow>
<SettingDivider />
<SettingRow style={{ paddingLeft: 8 }}>
<SettingRowTitleSmall>{t('chat.settings.code_editor.keymap')}</SettingRowTitleSmall>
<Switch
size="small"
checked={codeEditor.keymap}
onChange={(checked) => dispatch(setCodeEditor({ keymap: checked }))}
/>
</SettingRow>
</>
)}
<SettingDivider />
<SettingRow>
<SettingRowTitleSmall>{t('chat.settings.show_line_numbers')}</SettingRowTitleSmall>
<Switch
size="small"
checked={codeShowLineNumbers}
onChange={(checked) => dispatch(setCodeShowLineNumbers(checked))}
/>
</SettingRow>
<SettingDivider />
<SettingRow>
<SettingRowTitleSmall>{t('chat.settings.code_collapsible')}</SettingRowTitleSmall>
<Switch
size="small"
checked={codeCollapsible}
onChange={(checked) => dispatch(setCodeCollapsible(checked))}
/>
</SettingRow>
<SettingDivider />
<SettingRow>
<SettingRowTitleSmall>{t('chat.settings.code_wrappable')}</SettingRowTitleSmall>
<Switch size="small" checked={codeWrappable} onChange={(checked) => dispatch(setCodeWrappable(checked))} />
</SettingRow>
</SettingGroup>
</Modal>
)
}
const SettingRowTitleSmall = styled(SettingRowTitle)`
font-size: 13px;
`
const SettingGroup = styled.div<{ theme?: ThemeMode }>`
padding: 0;
width: 100%;
margin-top: 0;
border-radius: 8px;
margin-bottom: 10px;
margin-top: 10px;
`
const TopViewKey = 'MessageSettingsPopup'
export default class MessageSettingsPopup {
static topviewId = 0
static hide() {
TopView.hide(TopViewKey)
}
static show(props: ShowParams) {
return new Promise<any>((resolve) => {
TopView.show(<PopupContainer {...props} resolve={resolve} />, TopViewKey)
})
}
}

View File

@@ -0,0 +1,24 @@
export interface SettingsPopupShowParams {
defaultTab?:
| 'provider'
| 'model'
| 'tool'
| 'general'
| 'display'
| 'shortcut'
| 'quickAssistant'
| 'selectionAssistant'
| 'data'
| 'about'
| 'quickPhrase'
}
export default class SettingsPopup {
static hide() {
// Settings window is now independent, user can close it manually
}
static show(props: SettingsPopupShowParams = {}) {
return window.api.showSettingsWindow({ defaultTab: props.defaultTab })
}
}

View File

@@ -15,15 +15,17 @@ const PopupContainer: React.FC<Props> = ({ title, resolve }) => {
const [open, setOpen] = useState(true) const [open, setOpen] = useState(true)
const onOk = () => { const onOk = () => {
resolve(true)
setOpen(false) setOpen(false)
} }
const onCancel = () => { const onCancel = () => {
resolve(false)
setOpen(false) setOpen(false)
} }
const onClose = () => { const onClose = () => {
resolve({}) TopView.hide(TopViewKey)
} }
TemplatePopup.hide = onCancel TemplatePopup.hide = onCancel
@@ -51,16 +53,7 @@ export default class TemplatePopup {
} }
static show(props: ShowParams) { static show(props: ShowParams) {
return new Promise<any>((resolve) => { return new Promise<any>((resolve) => {
TopView.show( TopView.show(<PopupContainer {...props} resolve={resolve} />, TopViewKey)
<PopupContainer
{...props}
resolve={(v) => {
resolve(v)
TopView.hide(TopViewKey)
}}
/>,
TopViewKey
)
}) })
} }
} }

View File

@@ -0,0 +1,237 @@
import { PlusOutlined } from '@ant-design/icons'
import { isMac } from '@renderer/config/constant'
import { useFullscreen } from '@renderer/hooks/useFullscreen'
import { useAppDispatch, useAppSelector } from '@renderer/store'
import type { Tab } from '@renderer/store/tabs'
import { addTab, removeTab, setActiveTab } from '@renderer/store/tabs'
import {
FileSearch,
Folder,
Home,
Languages,
LayoutGrid,
Palette,
Settings,
Sparkle,
SquareTerminal,
X
} from 'lucide-react'
import { useEffect } from 'react'
import { useTranslation } from 'react-i18next'
import { useLocation, useNavigate } from 'react-router-dom'
import styled from 'styled-components'
interface TabsContainerProps {
children: React.ReactNode
}
const getTabIcon = (tabId: string): React.ReactNode | undefined => {
switch (tabId) {
case 'home':
return <Home size={14} />
case 'agents':
return <Sparkle size={14} />
case 'translate':
return <Languages size={14} />
case 'paintings':
return <Palette size={14} />
case 'apps':
return <LayoutGrid size={14} />
case 'knowledge':
return <FileSearch size={14} />
case 'mcp':
return <SquareTerminal size={14} />
case 'files':
return <Folder size={14} />
case 'settings':
return <Settings size={14} />
default:
return null
}
}
const TabsContainer: React.FC<TabsContainerProps> = ({ children }) => {
const { t } = useTranslation()
const location = useLocation()
const navigate = useNavigate()
const dispatch = useAppDispatch()
const tabs = useAppSelector((state) => state.tabs.tabs)
const activeTabId = useAppSelector((state) => state.tabs.activeTabId)
const isFullscreen = useFullscreen()
const getTabId = (path: string): string => {
if (path === '/') return 'home'
const segments = path.split('/')
return segments[1] // 获取第一个路径段作为 id
}
const shouldCreateTab = (path: string) => {
if (path === '/') return false
return !tabs.some((tab) => tab.id === getTabId(path))
}
useEffect(() => {
const tabId = getTabId(location.pathname)
const currentTab = tabs.find((tab) => tab.id === tabId)
if (!currentTab && shouldCreateTab(location.pathname)) {
dispatch(
addTab({
id: tabId,
path: location.pathname
})
)
} else if (currentTab) {
dispatch(setActiveTab(currentTab.id))
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [dispatch, location.pathname])
const closeTab = (tabId: string) => {
const tabToClose = tabs.find((tab) => tab.id === tabId)
if (!tabToClose) return
if (tabs.length === 1) return
if (tabId === activeTabId) {
const remainingTabs = tabs.filter((tab) => tab.id !== tabId)
const lastTab = remainingTabs[remainingTabs.length - 1]
navigate(lastTab.path)
}
dispatch(removeTab(tabId))
}
const handleAddTab = () => {
navigate('/launchpad')
}
return (
<Container>
<TabsBar $isFullscreen={isFullscreen}>
{tabs.map((tab) => (
<Tab key={tab.id} active={tab.id === activeTabId} onClick={() => navigate(tab.path)}>
<TabHeader>
{tab.id && <TabIcon>{getTabIcon(tab.id)}</TabIcon>}
<TabTitle>{t(`title.${tab.id}`)}</TabTitle>
</TabHeader>
{tab.id !== 'home' && (
<CloseButton
className="close-button"
onClick={(e) => {
e.stopPropagation()
closeTab(tab.id)
}}>
<X size={12} />
</CloseButton>
)}
</Tab>
))}
<AddTabButton onClick={handleAddTab}>
<PlusOutlined />
</AddTabButton>
</TabsBar>
<TabContent>{children}</TabContent>
</Container>
)
}
const Container = styled.div`
display: flex;
flex-direction: column;
height: 100%;
`
const TabsBar = styled.div<{ $isFullscreen: boolean }>`
display: flex;
flex-direction: row;
align-items: center;
gap: 5px;
padding-left: ${({ $isFullscreen }) => (!$isFullscreen && isMac ? '80px' : '8px')};
-webkit-app-region: drag;
height: var(--navbar-height);
`
const Tab = styled.div<{ active?: boolean }>`
display: flex;
align-items: center;
justify-content: space-between;
padding: 4px 10px;
background: ${(props) => (props.active ? 'var(--color-background)' : 'transparent')};
border-radius: 8px;
cursor: pointer;
user-select: none;
-webkit-app-region: none;
min-width: 100px;
transition: background 0.2s;
.close-button {
opacity: 0;
transition: opacity 0.2s;
margin-right: -2px;
}
&:hover {
background: ${(props) => (props.active ? 'var(--color-background)' : 'var(--color-background-soft)')};
.close-button {
opacity: 1;
}
}
`
const TabHeader = styled.div`
display: flex;
align-items: center;
gap: 6px;
`
const TabIcon = styled.span`
display: flex;
align-items: center;
margin-right: 6px;
color: var(--color-text-2);
`
const TabTitle = styled.span`
color: var(--color-text);
font-size: 13px;
margin-right: 8px;
display: flex;
align-items: center;
`
const CloseButton = styled.span`
display: flex;
align-items: center;
justify-content: center;
width: 16px;
height: 16px;
`
const AddTabButton = styled.div`
display: flex;
align-items: center;
justify-content: center;
width: 30px;
height: 30px;
cursor: pointer;
color: var(--color-text);
-webkit-app-region: none;
&:hover {
background: var(--color-background-soft);
border-radius: 8px;
}
`
const TabContent = styled.div`
display: flex;
flex: 1;
overflow: hidden;
width: calc(100vw - 12px);
margin: 6px;
margin-top: 0;
border-radius: 8px;
overflow: hidden;
`
export default TabsContainer

View File

@@ -80,7 +80,7 @@ const TranslateButton: FC<Props> = ({ text, onTranslated, disabled, style, isLoa
mouseLeaveDelay={0} mouseLeaveDelay={0}
arrow> arrow>
<ToolbarButton onClick={handleTranslate} disabled={disabled || isTranslating} style={style} type="text"> <ToolbarButton onClick={handleTranslate} disabled={disabled || isTranslating} style={style} type="text">
{isTranslating ? <LoadingOutlined spin /> : <Languages size={18} />} {isTranslating ? <LoadingOutlined spin /> : <Languages size={16} />}
</ToolbarButton> </ToolbarButton>
</Tooltip> </Tooltip>
) )

View File

@@ -1,24 +1,13 @@
import { isLinux, isMac, isWin } from '@renderer/config/constant' import { isLinux, isMac, isWin } from '@renderer/config/constant'
import { useFullscreen } from '@renderer/hooks/useFullscreen' import { useFullscreen } from '@renderer/hooks/useFullscreen'
import useNavBackgroundColor from '@renderer/hooks/useNavBackgroundColor' import { useShowAssistants } from '@renderer/hooks/useStore'
import type { FC, PropsWithChildren } from 'react' import type { FC, HTMLAttributes, PropsWithChildren } from 'react'
import type { HTMLAttributes } from 'react'
import styled from 'styled-components' import styled from 'styled-components'
type Props = PropsWithChildren & HTMLAttributes<HTMLDivElement> type Props = PropsWithChildren & HTMLAttributes<HTMLDivElement>
export const Navbar: FC<Props> = ({ children, ...props }) => { export const Navbar: FC<Props> = ({ children, ...props }) => {
const backgroundColor = useNavBackgroundColor() return <NavbarContainer {...props}>{children}</NavbarContainer>
return (
<NavbarContainer {...props} style={{ backgroundColor }}>
{children}
</NavbarContainer>
)
}
export const NavbarLeft: FC<Props> = ({ children, ...props }) => {
return <NavbarLeftContainer {...props}>{children}</NavbarLeftContainer>
} }
export const NavbarCenter: FC<Props> = ({ children, ...props }) => { export const NavbarCenter: FC<Props> = ({ children, ...props }) => {
@@ -36,8 +25,10 @@ export const NavbarRight: FC<Props> = ({ children, ...props }) => {
export const NavbarMain: FC<Props> = ({ children, ...props }) => { export const NavbarMain: FC<Props> = ({ children, ...props }) => {
const isFullscreen = useFullscreen() const isFullscreen = useFullscreen()
const { showAssistants } = useShowAssistants()
return ( return (
<NavbarMainContainer {...props} $isFullscreen={isFullscreen}> <NavbarMainContainer {...props} $isFullscreen={isFullscreen} $showAssistants={showAssistants}>
{children} {children}
</NavbarMainContainer> </NavbarMainContainer>
) )
@@ -49,28 +40,8 @@ const NavbarContainer = styled.div`
flex-direction: row; flex-direction: row;
min-height: var(--navbar-height); min-height: var(--navbar-height);
max-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; -webkit-app-region: drag;
` background-color: var(--color-background);
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);
` `
const NavbarRightContainer = styled.div<{ $isFullscreen: boolean }>` const NavbarRightContainer = styled.div<{ $isFullscreen: boolean }>`
@@ -82,14 +53,45 @@ const NavbarRightContainer = styled.div<{ $isFullscreen: boolean }>`
justify-content: flex-end; justify-content: flex-end;
` `
const NavbarMainContainer = styled.div<{ $isFullscreen: boolean }>` const NavbarMainContainer = styled.div<{ $isFullscreen: boolean; $showAssistants: boolean }>`
flex: 1; flex: 1;
display: flex; display: flex;
flex-direction: row; flex-direction: row;
align-items: center; align-items: center;
background-color: var(--color-background);
height: var(--navbar-height);
max-height: var(--navbar-height);
min-height: var(--navbar-height);
justify-content: space-between; justify-content: space-between;
padding: 0 ${isMac ? '20px' : 0};
font-weight: bold; font-weight: bold;
color: var(--color-text-1); color: var(--color-text-1);
padding-right: ${({ $isFullscreen }) => ($isFullscreen ? '12px' : isWin ? '140px' : isLinux ? '120px' : '12px')}; -webkit-app-region: drag;
padding: 0 12px;
padding-left: ${({ $showAssistants }) => (isMac && !$showAssistants ? '70px' : '10px')};
` `
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);
`
// const rotateAnimation = keyframes`
// from {
// transform: rotate(-180deg);
// }
// to {
// transform: rotate(0);
// }
// `
// const AnimatedButton = styled(Button)`
// animation: ${rotateAnimation} 0.4s ease-out;
// `

View File

@@ -1,8 +1,6 @@
import EmojiAvatar from '@renderer/components/Avatar/EmojiAvatar'
import { isMac } from '@renderer/config/constant' import { isMac } from '@renderer/config/constant'
import { AppLogo, UserAvatar } from '@renderer/config/env' import { AppLogo } from '@renderer/config/env'
import { useTheme } from '@renderer/context/ThemeProvider' import { useTheme } from '@renderer/context/ThemeProvider'
import useAvatar from '@renderer/hooks/useAvatar'
import { useFullscreen } from '@renderer/hooks/useFullscreen' import { useFullscreen } from '@renderer/hooks/useFullscreen'
import { useMinappPopup } from '@renderer/hooks/useMinappPopup' import { useMinappPopup } from '@renderer/hooks/useMinappPopup'
import { useMinapps } from '@renderer/hooks/useMinapps' import { useMinapps } from '@renderer/hooks/useMinapps'
@@ -11,9 +9,8 @@ import { modelGenerating, useRuntime } from '@renderer/hooks/useRuntime'
import { useSettings } from '@renderer/hooks/useSettings' import { useSettings } from '@renderer/hooks/useSettings'
import i18n from '@renderer/i18n' import i18n from '@renderer/i18n'
import { ThemeMode } from '@renderer/types' import { ThemeMode } from '@renderer/types'
import { isEmoji } from '@renderer/utils'
import type { MenuProps } from 'antd' import type { MenuProps } from 'antd'
import { Avatar, Dropdown, Tooltip } from 'antd' import { Dropdown, Tooltip } from 'antd'
import { import {
CircleHelp, CircleHelp,
FileSearch, FileSearch,
@@ -35,7 +32,6 @@ import styled from 'styled-components'
import { DraggableList } from '../DraggableList' import { DraggableList } from '../DraggableList'
import MinAppIcon from '../Icons/MinAppIcon' import MinAppIcon from '../Icons/MinAppIcon'
import UserPopup from '../Popups/UserPopup'
const Sidebar: FC = () => { const Sidebar: FC = () => {
const { hideMinappPopup, openMinapp } = useMinappPopup() const { hideMinappPopup, openMinapp } = useMinappPopup()
@@ -47,11 +43,8 @@ const Sidebar: FC = () => {
const navigate = useNavigate() const navigate = useNavigate()
const { theme, settedTheme, toggleTheme } = useTheme() const { theme, settedTheme, toggleTheme } = useTheme()
const avatar = useAvatar()
const { t } = useTranslation() const { t } = useTranslation()
const onEditUser = () => UserPopup.show()
const backgroundColor = useNavBackgroundColor() const backgroundColor = useNavBackgroundColor()
const showPinnedApps = pinned.length > 0 && sidebarIcons.visible.includes('minapp') const showPinnedApps = pinned.length > 0 && sidebarIcons.visible.includes('minapp')
@@ -79,13 +72,6 @@ const Sidebar: FC = () => {
$isFullscreen={isFullscreen} $isFullscreen={isFullscreen}
id="app-sidebar" id="app-sidebar"
style={{ backgroundColor, zIndex: minappShow ? 10000 : 'initial' }}> style={{ backgroundColor, zIndex: minappShow ? 10000 : 'initial' }}>
{isEmoji(avatar) ? (
<EmojiAvatar onClick={onEditUser} className="sidebar-avatar" size={31} fontSize={18}>
{avatar}
</EmojiAvatar>
) : (
<AvatarImg src={avatar || UserAvatar} draggable={false} className="nodrag" onClick={onEditUser} />
)}
<MainMenusContainer> <MainMenusContainer>
<Menus onClick={hideMinappPopup}> <Menus onClick={hideMinappPopup}>
<MainMenus /> <MainMenus />
@@ -339,16 +325,6 @@ const Container = styled.div<{ $isFullscreen: boolean }>`
} }
` `
const AvatarImg = styled(Avatar)`
width: 31px;
height: 31px;
background-color: var(--color-background-soft);
margin-bottom: ${isMac ? '12px' : '12px'};
margin-top: ${isMac ? '0px' : '2px'};
border: none;
cursor: pointer;
`
const MainMenusContainer = styled.div` const MainMenusContainer = styled.div`
display: flex; display: flex;
flex: 1; flex: 1;

View File

@@ -47,7 +47,7 @@ As [role name], with [list skills], strictly adhering to [list constraints], usi
` `
export const SUMMARIZE_PROMPT = 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 // https://github.com/ItzCrazyKns/Perplexica/blob/master/src/lib/prompts/webSearch.ts
export const SEARCH_SUMMARY_PROMPT = ` export const SEARCH_SUMMARY_PROMPT = `

View File

@@ -23,7 +23,7 @@ interface ThemeProviderProps extends PropsWithChildren {
export const ThemeProvider: React.FC<ThemeProviderProps> = ({ children }) => { export const ThemeProvider: React.FC<ThemeProviderProps> = ({ children }) => {
// 用户设置的主题 // 用户设置的主题
const { theme: settedTheme, setTheme: setSettedTheme } = useSettings() const { theme: settedTheme, setTheme: setSettedTheme, transparentWindow } = useSettings()
const [actualTheme, setActualTheme] = useState<ThemeMode>( const [actualTheme, setActualTheme] = useState<ThemeMode>(
window.matchMedia('(prefers-color-scheme: dark)').matches ? ThemeMode.dark : ThemeMode.light window.matchMedia('(prefers-color-scheme: dark)').matches ? ThemeMode.dark : ThemeMode.light
) )
@@ -56,7 +56,7 @@ export const ThemeProvider: React.FC<ThemeProviderProps> = ({ children }) => {
document.body.setAttribute('theme-mode', actualTheme) document.body.setAttribute('theme-mode', actualTheme)
setActualTheme(actualTheme) setActualTheme(actualTheme)
}) })
}, [actualTheme, initUserTheme, setSettedTheme, settedTheme]) }, [actualTheme, initUserTheme, setSettedTheme, settedTheme, transparentWindow])
useEffect(() => { useEffect(() => {
window.api.setTheme(settedTheme) window.api.setTheme(settedTheme)

View File

@@ -1,21 +1,25 @@
import SettingsPopup from '@renderer/components/Popups/SettingsPopup'
import NavigationService from '@renderer/services/NavigationService'
import { useAppSelector } from '@renderer/store' import { useAppSelector } from '@renderer/store'
import { useEffect } from 'react'
import { useHotkeys } from 'react-hotkeys-hook' import { useHotkeys } from 'react-hotkeys-hook'
import { useLocation, useNavigate } from 'react-router-dom' import { useNavigate } from 'react-router-dom'
const NavigationHandler: React.FC = () => { const NavigationHandler: React.FC = () => {
const location = useLocation()
const navigate = useNavigate() const navigate = useNavigate()
const showSettingsShortcutEnabled = useAppSelector( const showSettingsShortcutEnabled = useAppSelector(
(state) => state.shortcuts.shortcuts.find((s) => s.key === 'show_settings')?.enabled (state) => state.shortcuts.shortcuts.find((s) => s.key === 'show_settings')?.enabled
) )
useEffect(() => {
NavigationService.setNavigate(navigate)
}, [navigate])
useHotkeys( useHotkeys(
'meta+, ! ctrl+,', 'meta+, ! ctrl+,',
function () { function () {
if (location.pathname.startsWith('/settings')) { SettingsPopup.show({ defaultTab: 'provider' })
return
}
navigate('/settings/provider')
}, },
{ {
splitKey: '!', splitKey: '!',

View File

@@ -1,28 +0,0 @@
import { useAppDispatch, useAppSelector } from '@renderer/store'
import { addAgent, removeAgent, updateAgent, updateAgents, updateAgentSettings } from '@renderer/store/agents'
import { Agent, AssistantSettings } from '@renderer/types'
export function useAgents() {
const agents = useAppSelector((state) => state.agents.agents)
const dispatch = useAppDispatch()
return {
agents,
updateAgents: (agents: Agent[]) => dispatch(updateAgents(agents)),
addAgent: (agent: Agent) => dispatch(addAgent(agent)),
removeAgent: (id: string) => dispatch(removeAgent({ id }))
}
}
export function useAgent(id: string) {
const agent = useAppSelector((state) => state.agents.agents.find((a) => a.id === id) as Agent)
const dispatch = useAppDispatch()
return {
agent,
updateAgent: (agent: Agent) => dispatch(updateAgent(agent)),
updateAgentSettings: (settings: Partial<AssistantSettings>) => {
dispatch(updateAgentSettings({ assistantId: agent.id, settings }))
}
}
}

View File

@@ -1,6 +1,4 @@
import { isMac } from '@renderer/config/constant'
import { isLocalAi } from '@renderer/config/env' import { isLocalAi } from '@renderer/config/env'
import { useTheme } from '@renderer/context/ThemeProvider'
import db from '@renderer/databases' import db from '@renderer/databases'
import i18n from '@renderer/i18n' import i18n from '@renderer/i18n'
import KnowledgeQueue from '@renderer/queue/KnowledgeQueue' import KnowledgeQueue from '@renderer/queue/KnowledgeQueue'
@@ -13,17 +11,14 @@ import { useEffect } from 'react'
import { useDefaultModel } from './useAssistant' import { useDefaultModel } from './useAssistant'
import useFullScreenNotice from './useFullScreenNotice' import useFullScreenNotice from './useFullScreenNotice'
import { useRuntime } from './useRuntime'
import { useSettings } from './useSettings' import { useSettings } from './useSettings'
import useUpdateHandler from './useUpdateHandler' import useUpdateHandler from './useUpdateHandler'
export function useAppInit() { export function useAppInit() {
const dispatch = useAppDispatch() const dispatch = useAppDispatch()
const { proxyUrl, language, windowStyle, autoCheckUpdate, proxyMode, customCss, enableDataCollection } = useSettings() const { proxyUrl, language, autoCheckUpdate, proxyMode, customCss, enableDataCollection } = useSettings()
const { minappShow } = useRuntime()
const { setDefaultModel, setTopicNamingModel, setTranslateModel } = useDefaultModel() const { setDefaultModel, setTopicNamingModel, setTranslateModel } = useDefaultModel()
const avatar = useLiveQuery(() => db.settings.get('image://avatar')) const avatar = useLiveQuery(() => db.settings.get('image://avatar'))
const { theme } = useTheme()
useEffect(() => { useEffect(() => {
document.getElementById('spinner')?.remove() document.getElementById('spinner')?.remove()
@@ -70,18 +65,6 @@ export function useAppInit() {
i18n.changeLanguage(language || navigator.language || defaultLanguage) i18n.changeLanguage(language || navigator.language || defaultLanguage)
}, [language]) }, [language])
useEffect(() => {
const transparentWindow = windowStyle === 'transparent' && isMac && !minappShow
if (minappShow) {
window.root.style.background =
windowStyle === 'transparent' && isMac ? 'var(--color-background)' : 'var(--navbar-background)'
return
}
window.root.style.background = transparentWindow ? 'var(--navbar-background-mac)' : 'var(--navbar-background)'
}, [windowStyle, minappShow, theme])
useEffect(() => { useEffect(() => {
if (isLocalAi) { if (isLocalAi) {
const model = JSON.parse(import.meta.env.VITE_RENDERER_INTEGRATED_MODEL) const model = JSON.parse(import.meta.env.VITE_RENDERER_INTEGRATED_MODEL)

View File

@@ -1,45 +1,56 @@
import { db } from '@renderer/databases' import { db } from '@renderer/databases'
import { getDefaultTopic } from '@renderer/services/AssistantService'
import { useAppDispatch, useAppSelector } from '@renderer/store' import { useAppDispatch, useAppSelector } from '@renderer/store'
import { import {
addAssistant, addAssistant,
addTopic, createAssistantFromTemplate,
removeAllTopics,
removeAssistant, removeAssistant,
removeTopic, selectActiveAssistants,
selectTemplates,
setModel, setModel,
updateAssistant, updateAssistant,
updateAssistants, updateAssistants,
updateAssistantSettings, updateAssistantSettings,
updateDefaultAssistant, updateDefaultAssistant
updateTopic,
updateTopics
} from '@renderer/store/assistants' } from '@renderer/store/assistants'
import { setDefaultModel, setTopicNamingModel, setTranslateModel } from '@renderer/store/llm' import { setDefaultModel, setTopicNamingModel, setTranslateModel } from '@renderer/store/llm'
import { selectTopicsForAssistant, topicsActions } from '@renderer/store/topics'
import { Assistant, AssistantSettings, Model, Topic } from '@renderer/types' import { Assistant, AssistantSettings, Model, Topic } from '@renderer/types'
import { useCallback, useMemo } from 'react' import { useCallback, useMemo } from 'react'
import { TopicManager } from './useTopic'
export function useAssistants() { export function useAssistants() {
const { assistants } = useAppSelector((state) => state.assistants) const assistants = useAppSelector(selectActiveAssistants)
const templates = useAppSelector(selectTemplates)
const dispatch = useAppDispatch() const dispatch = useAppDispatch()
const getAssistantById = useCallback((id: string) => assistants.find((a) => a.id === id), [assistants])
return { return {
assistants, assistants,
templates,
getAssistantById,
updateAssistants: (assistants: Assistant[]) => dispatch(updateAssistants(assistants)), updateAssistants: (assistants: Assistant[]) => dispatch(updateAssistants(assistants)),
addAssistant: (assistant: Assistant) => dispatch(addAssistant(assistant)), addAssistant: (assistant: Assistant) => {
dispatch(addAssistant({ ...assistant, isTemplate: false }))
dispatch(topicsActions.addDefaultTopic({ assistantId: assistant.id }))
},
addTemplate: (template: Assistant) => {
dispatch(addAssistant({ ...template, isTemplate: true }))
},
removeAssistant: (id: string) => { removeAssistant: (id: string) => {
dispatch(removeAssistant({ id })) dispatch(removeAssistant({ id }))
const assistant = assistants.find((a) => a.id === id) // Remove all topics for this assistant
const topics = assistant?.topics || [] dispatch(topicsActions.removeAllTopics({ assistantId: id }))
topics.forEach(({ id }) => TopicManager.removeTopic(id)) },
createAssistantFromTemplate: (templateId: string, assistantId: string) => {
dispatch(createAssistantFromTemplate({ templateId, assistantId }))
dispatch(topicsActions.addDefaultTopic({ assistantId }))
} }
} }
} }
export function useAssistant(id: string) { export function useAssistant(id: string) {
const assistant = useAppSelector((state) => state.assistants.assistants.find((a) => a.id === id) as Assistant) const assistant = useAppSelector((state) => state.assistants.assistants.find((a) => a.id === id) as Assistant)
const topics = useTopicsForAssistant(id)
const dispatch = useAppDispatch() const dispatch = useAppDispatch()
const { defaultModel } = useDefaultModel() const { defaultModel } = useDefaultModel()
@@ -48,19 +59,18 @@ export function useAssistant(id: string) {
throw new Error(`Assistant model is not set for assistant with name: ${assistant?.name ?? 'unknown'}`) throw new Error(`Assistant model is not set for assistant with name: ${assistant?.name ?? 'unknown'}`)
} }
const assistantWithModel = useMemo(() => ({ ...assistant, model }), [assistant, model]) const assistantWithModel = useMemo(() => ({ ...assistant, model, topics }), [assistant, model, topics])
return { return {
assistant: assistantWithModel, assistant: assistantWithModel,
model, model,
addTopic: (topic: Topic) => dispatch(addTopic({ assistantId: assistant.id, topic })), topics,
addTopic: (topic: Topic) => dispatch(topicsActions.addTopic({ assistantId: id, topic })),
removeTopic: (topic: Topic) => { removeTopic: (topic: Topic) => {
TopicManager.removeTopic(topic.id) dispatch(topicsActions.removeTopic({ assistantId: id, topicId: topic.id }))
dispatch(removeTopic({ assistantId: assistant.id, topic }))
}, },
moveTopic: (topic: Topic, toAssistant: Assistant) => { moveTopic: (topic: Topic, toAssistant: Assistant) => {
dispatch(addTopic({ assistantId: toAssistant.id, topic: { ...topic, assistantId: toAssistant.id } })) dispatch(topicsActions.moveTopic({ fromAssistantId: id, toAssistantId: toAssistant.id, topicId: topic.id }))
dispatch(removeTopic({ assistantId: assistant.id, topic }))
// update topic messages in database // update topic messages in database
db.topics db.topics
.where('id') .where('id')
@@ -74,9 +84,9 @@ export function useAssistant(id: string) {
} }
}) })
}, },
updateTopic: (topic: Topic) => dispatch(updateTopic({ assistantId: assistant.id, topic })), updateTopic: (topic: Topic) => dispatch(topicsActions.updateTopic({ assistantId: id, topic })),
updateTopics: (topics: Topic[]) => dispatch(updateTopics({ assistantId: assistant.id, topics })), updateTopics: (topics: Topic[]) => dispatch(topicsActions.updateTopics({ assistantId: id, topics })),
removeAllTopics: () => dispatch(removeAllTopics({ assistantId: assistant.id })), removeAllTopics: () => dispatch(topicsActions.removeAllTopics({ assistantId: id })),
setModel: useCallback( setModel: useCallback(
(model: Model) => assistant && dispatch(setModel({ assistantId: assistant?.id, model })), (model: Model) => assistant && dispatch(setModel({ assistantId: assistant?.id, model })),
[assistant, dispatch] [assistant, dispatch]
@@ -88,16 +98,19 @@ export function useAssistant(id: string) {
} }
} }
export function useTopicsForAssistant(assistantId: string) {
return useAppSelector((state) => selectTopicsForAssistant(state, assistantId))
}
/**
* 默认助手模板
*/
export function useDefaultAssistant() { export function useDefaultAssistant() {
const defaultAssistant = useAppSelector((state) => state.assistants.defaultAssistant) const defaultAssistant = useAppSelector((state) => state.assistants.defaultAssistant)
const dispatch = useAppDispatch() const dispatch = useAppDispatch()
const memoizedTopics = useMemo(() => [getDefaultTopic(defaultAssistant.id)], [defaultAssistant.id])
return { return {
defaultAssistant: { defaultAssistant,
...defaultAssistant,
topics: memoizedTopics
},
updateDefaultAssistant: (assistant: Assistant) => dispatch(updateDefaultAssistant({ assistant })) updateDefaultAssistant: (assistant: Assistant) => dispatch(updateDefaultAssistant({ assistant }))
} }
} }

View File

@@ -0,0 +1,92 @@
import { EVENT_NAMES, EventEmitter } from '@renderer/services/EventService'
import { useAppDispatch, useAppSelector } from '@renderer/store'
import { loadTopicMessagesThunk } from '@renderer/store/thunk/messageThunk'
import { Assistant, Topic } from '@renderer/types'
import { createContext, use, useCallback, useEffect, useMemo, useState } from 'react'
import { useLocation, useNavigate } from 'react-router-dom'
import { useTopicsForAssistant } from './useAssistant'
import { useSettings } from './useSettings'
interface ChatContextType {
activeAssistant: Assistant
activeTopic: Topic
setActiveAssistant: (assistant: Assistant) => void
setActiveTopic: (topic: Topic) => void
}
const ChatContext = createContext<ChatContextType | null>(null)
export const ChatProvider = ({ children }) => {
const assistants = useAppSelector((state) => state.assistants.assistants)
const [activeAssistant, setActiveAssistantBase] = useState<Assistant>(assistants[0])
const topics = useTopicsForAssistant(activeAssistant.id)
const [activeTopic, setActiveTopic] = useState<Topic>(topics[0])
const { clickAssistantToShowTopic } = useSettings()
const dispatch = useAppDispatch()
const navigate = useNavigate()
const location = useLocation()
// 包装setActiveAssistant以添加导航逻辑
const setActiveAssistant = useCallback(
(assistant: Assistant) => {
setActiveAssistantBase(assistant)
// 如果当前不在聊天页面,导航到聊天页面
if (location.pathname !== '/') {
navigate('/')
}
},
[setActiveAssistantBase, location.pathname, navigate]
)
// 当 topics 变化时,如果当前 activeTopic 不在 topics 中,设置第一个 topic
useEffect(() => {
if (!topics.find((topic) => topic.id === activeTopic?.id)) {
const firstTopic = topics[0]
firstTopic && setActiveTopic(firstTopic)
}
}, [topics, activeTopic?.id])
// 当 activeTopic 变化时加载消息
useEffect(() => {
if (activeTopic) {
dispatch(loadTopicMessagesThunk(activeTopic.id))
EventEmitter.emit(EVENT_NAMES.CHANGE_TOPIC, activeTopic)
}
}, [activeTopic, dispatch])
// 处理点击助手显示话题侧边栏
useEffect(() => {
if (clickAssistantToShowTopic) {
EventEmitter.emit(EVENT_NAMES.SHOW_TOPIC_SIDEBAR)
}
}, [clickAssistantToShowTopic, activeAssistant])
useEffect(() => {
const subscriptions = [
EventEmitter.on(EVENT_NAMES.SET_ASSISTANT, setActiveAssistant),
EventEmitter.on(EVENT_NAMES.SET_TOPIC, setActiveTopic)
]
return () => subscriptions.forEach((subscription) => subscription())
}, [setActiveAssistant])
const value = useMemo(
() => ({
activeAssistant,
activeTopic,
setActiveAssistant,
setActiveTopic
}),
[activeAssistant, activeTopic, setActiveAssistant]
)
return <ChatContext value={value}>{children}</ChatContext>
}
export const useChat = () => {
const context = use(ChatContext)
if (!context) {
throw new Error('useChat must be used within ChatProvider')
}
return context
}

View File

@@ -3,7 +3,7 @@ import { EVENT_NAMES, EventEmitter } from '@renderer/services/EventService'
import { RootState } from '@renderer/store' import { RootState } from '@renderer/store'
import { messageBlocksSelectors } from '@renderer/store/messageBlock' import { messageBlocksSelectors } from '@renderer/store/messageBlock'
import { selectMessagesForTopic } from '@renderer/store/newMessage' import { selectMessagesForTopic } from '@renderer/store/newMessage'
import { setActiveTopic, setSelectedMessageIds, toggleMultiSelectMode } from '@renderer/store/runtime' import { setSelectedMessageIds, toggleMultiSelectMode } from '@renderer/store/runtime'
import { Topic } from '@renderer/types' import { Topic } from '@renderer/types'
import { useCallback, useEffect, useState } from 'react' import { useCallback, useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
@@ -27,10 +27,6 @@ export const useChatContext = (activeTopic: Topic) => {
return () => unsubscribe() return () => unsubscribe()
}, [dispatch]) }, [dispatch])
useEffect(() => {
dispatch(setActiveTopic(activeTopic))
}, [dispatch, activeTopic])
const handleToggleMultiSelectMode = useCallback( const handleToggleMultiSelectMode = useCallback(
(value: boolean) => { (value: boolean) => {
dispatch(toggleMultiSelectMode(value)) dispatch(toggleMultiSelectMode(value))

View File

@@ -23,7 +23,6 @@ import { useCallback, useEffect, useState } from 'react'
import { useDispatch, useSelector } from 'react-redux' import { useDispatch, useSelector } from 'react-redux'
import { v4 as uuidv4 } from 'uuid' import { v4 as uuidv4 } from 'uuid'
import { useAgents } from './useAgents'
import { useAssistants } from './useAssistant' import { useAssistants } from './useAssistant'
export const useKnowledge = (baseId: string) => { export const useKnowledge = (baseId: string) => {
@@ -295,7 +294,6 @@ export const useKnowledgeBases = () => {
const dispatch = useDispatch() const dispatch = useDispatch()
const bases = useSelector((state: RootState) => state.knowledge.bases) const bases = useSelector((state: RootState) => state.knowledge.bases)
const { assistants, updateAssistants } = useAssistants() const { assistants, updateAssistants } = useAssistants()
const { agents, updateAgents } = useAgents()
const addKnowledgeBase = (base: KnowledgeBase) => { const addKnowledgeBase = (base: KnowledgeBase) => {
dispatch(addBase(base)) dispatch(addBase(base))
@@ -319,19 +317,7 @@ export const useKnowledgeBases = () => {
return assistant return assistant
}) })
// remove agent knowledge_base
const _agents = agents.map((agent) => {
if (agent.knowledge_bases?.find((kb) => kb.id === baseId)) {
return {
...agent,
knowledge_bases: agent.knowledge_bases.filter((kb) => kb.id !== baseId)
}
}
return agent
})
updateAssistants(_assistants) updateAssistants(_assistants)
updateAgents(_agents)
} }
const updateKnowledgeBases = (bases: KnowledgeBase[]) => { const updateKnowledgeBases = (bases: KnowledgeBase[]) => {

View File

@@ -1,6 +1,7 @@
import { DEFAULT_MIN_APPS } from '@renderer/config/minapps' import { DEFAULT_MIN_APPS } from '@renderer/config/minapps'
import { useRuntime } from '@renderer/hooks/useRuntime' import { useRuntime } from '@renderer/hooks/useRuntime'
import { useSettings } from '@renderer/hooks/useSettings' // 使用设置中的值 import { useSettings } from '@renderer/hooks/useSettings' // 使用设置中的值
import { EVENT_NAMES, EventEmitter } from '@renderer/services/EventService'
import { useAppDispatch } from '@renderer/store' import { useAppDispatch } from '@renderer/store'
import { import {
setCurrentMinappId, setCurrentMinappId,
@@ -33,6 +34,7 @@ export const useMinappPopup = () => {
/** Open a minapp (popup shows and minapp loaded) */ /** Open a minapp (popup shows and minapp loaded) */
const openMinapp = useCallback( const openMinapp = useCallback(
(app: MinAppType, keepAlive: boolean = false) => { (app: MinAppType, keepAlive: boolean = false) => {
EventEmitter.emit(EVENT_NAMES.OPEN_MINAPP, app)
if (keepAlive) { if (keepAlive) {
// 如果小程序已经打开,只切换显示 // 如果小程序已经打开,只切换显示
if (openedKeepAliveMinapps.some((item) => item.id === app.id)) { if (openedKeepAliveMinapps.some((item) => item.id === app.id)) {

View File

@@ -1,13 +1,7 @@
import { isMac } from '@renderer/config/constant' import { isMac } from '@renderer/config/constant'
import { useSettings } from './useSettings'
function useNavBackgroundColor() { function useNavBackgroundColor() {
const { windowStyle } = useSettings() if (isMac) {
const macTransparentWindow = isMac && windowStyle === 'transparent'
if (macTransparentWindow) {
return 'transparent' return 'transparent'
} }

View File

@@ -9,7 +9,6 @@ import {
setLaunchToTray, setLaunchToTray,
setPinTopicsToTop, setPinTopicsToTop,
setSendMessageShortcut as _setSendMessageShortcut, setSendMessageShortcut as _setSendMessageShortcut,
setShowTokens,
setSidebarIcons, setSidebarIcons,
setTargetLanguage, setTargetLanguage,
setTestChannel as _setTestChannel, setTestChannel as _setTestChannel,
@@ -17,9 +16,9 @@ import {
setTheme, setTheme,
SettingsState, SettingsState,
setTopicPosition, setTopicPosition,
setTransparentWindow,
setTray as _setTray, setTray as _setTray,
setTrayOnClose, setTrayOnClose
setWindowStyle
} from '@renderer/store/settings' } from '@renderer/store/settings'
import { SidebarIcon, ThemeMode, TranslateLanguageVarious } from '@renderer/types' import { SidebarIcon, ThemeMode, TranslateLanguageVarious } from '@renderer/types'
import { UpgradeChannel } from '@shared/config/constant' import { UpgradeChannel } from '@shared/config/constant'
@@ -75,9 +74,6 @@ export function useSettings() {
setTheme(theme: ThemeMode) { setTheme(theme: ThemeMode) {
dispatch(setTheme(theme)) dispatch(setTheme(theme))
}, },
setWindowStyle(windowStyle: 'transparent' | 'opaque') {
dispatch(setWindowStyle(windowStyle))
},
setTargetLanguage(targetLanguage: TranslateLanguageVarious) { setTargetLanguage(targetLanguage: TranslateLanguageVarious) {
dispatch(setTargetLanguage(targetLanguage)) dispatch(setTargetLanguage(targetLanguage))
}, },
@@ -99,8 +95,8 @@ export function useSettings() {
setAssistantIconType(assistantIconType: AssistantIconType) { setAssistantIconType(assistantIconType: AssistantIconType) {
dispatch(setAssistantIconType(assistantIconType)) dispatch(setAssistantIconType(assistantIconType))
}, },
setShowTokens(showTokens: boolean) { setTransparentWindow(transparentWindow: boolean) {
dispatch(setShowTokens(showTokens)) dispatch(setTransparentWindow(transparentWindow))
}, },
setDisableHardwareAcceleration(disableHardwareAcceleration: boolean) { setDisableHardwareAcceleration(disableHardwareAcceleration: boolean) {
dispatch(setDisableHardwareAcceleration(disableHardwareAcceleration)) dispatch(setDisableHardwareAcceleration(disableHardwareAcceleration))

View File

@@ -0,0 +1,64 @@
import { useAppDispatch, useAppSelector } from '@renderer/store'
import type { Tab } from '@renderer/store/tabs'
import { addTab, removeTab, setActiveTab, updateTab } from '@renderer/store/tabs'
import { useNavigate } from 'react-router-dom'
export function useTabs() {
const navigate = useNavigate()
const dispatch = useAppDispatch()
const tabs = useAppSelector((state) => state.tabs.tabs)
const activeTabId = useAppSelector((state) => state.tabs.activeTabId)
const activeTab = useAppSelector((state) => state.tabs.tabs.find((tab) => tab.id === activeTabId))
const getTabId = (path: string): string => {
if (path === '/') return 'home'
const segments = path.split('/')
return segments[1]
}
const shouldCreateTab = (path: string) => {
if (path === '/') return false
return !tabs.some((tab) => tab.id === getTabId(path))
}
const addNewTab = (tab: Tab) => {
dispatch(addTab(tab))
navigate(tab.path)
}
const closeTab = (tabId: string) => {
if (tabs.length === 1) return
if (tabId === activeTabId) {
const remainingTabs = tabs.filter((tab) => tab.id !== tabId)
const lastTab = remainingTabs[remainingTabs.length - 1]
navigate(lastTab.path)
}
dispatch(removeTab(tabId))
}
const switchTab = (tabId: string) => {
const tab = tabs.find((tab) => tab.id === tabId)
if (tab) {
dispatch(setActiveTab(tabId))
navigate(tab.path)
}
}
const updateCurrentTab = (updates: Partial<Tab>) => {
dispatch(updateTab({ id: activeTabId, updates }))
}
return {
tabs,
activeTab,
activeTabId,
addNewTab,
closeTab,
switchTab,
getTabId,
shouldCreateTab,
updateCurrentTab
}
}

View File

@@ -26,7 +26,7 @@ export const useTags = () => {
// 计算所有标签 // 计算所有标签
const allTags = useMemo(() => { const allTags = useMemo(() => {
const tags = uniq(flatMap(assistants, (assistant) => assistant.tags || [])) const tags = uniq(flatMap(assistants, (assistant) => assistant?.tags || []))
if (savedTagsOrder.length > 0) { if (savedTagsOrder.length > 0) {
return [ return [
...savedTagsOrder.filter((tag) => tags.includes(tag)), ...savedTagsOrder.filter((tag) => tags.includes(tag)),
@@ -50,6 +50,7 @@ export const useTags = () => {
// 按标签分组并构建结果 // 按标签分组并构建结果
const grouped = Object.entries(groupBy(assistantsByTags, 'tag')).map(([tag, group]) => ({ const grouped = Object.entries(groupBy(assistantsByTags, 'tag')).map(([tag, group]) => ({
id: tag,
tag, tag,
assistants: group.map((g) => g.assistant) assistants: group.map((g) => g.assistant)
})) }))

View File

@@ -1,59 +1,23 @@
import db from '@renderer/databases' import db from '@renderer/databases'
import i18n from '@renderer/i18n' import i18n from '@renderer/i18n'
import { fetchMessagesSummary } from '@renderer/services/ApiService' import { fetchMessagesSummary } from '@renderer/services/ApiService'
import { EVENT_NAMES, EventEmitter } from '@renderer/services/EventService'
import { deleteMessageFiles } from '@renderer/services/MessagesService' import { deleteMessageFiles } from '@renderer/services/MessagesService'
import store from '@renderer/store' import store from '@renderer/store'
import { updateTopic } from '@renderer/store/assistants'
import { setNewlyRenamedTopics, setRenamingTopics } from '@renderer/store/runtime' import { setNewlyRenamedTopics, setRenamingTopics } from '@renderer/store/runtime'
import { loadTopicMessagesThunk } from '@renderer/store/thunk/messageThunk' import { loadTopicMessagesThunk } from '@renderer/store/thunk/messageThunk'
import { selectTopicById, topicsActions } from '@renderer/store/topics'
import { Assistant, Topic } from '@renderer/types' import { Assistant, Topic } from '@renderer/types'
import { findMainTextBlocks } from '@renderer/utils/messageUtils/find' import { findMainTextBlocks } from '@renderer/utils/messageUtils/find'
import { find, isEmpty } from 'lodash' import { isEmpty } from 'lodash'
import { useEffect, useState } from 'react'
import { useAssistant } from './useAssistant'
import { getStoreSetting } from './useSettings' import { getStoreSetting } from './useSettings'
let _activeTopic: Topic export function getTopic(topicId: string) {
let _setActiveTopic: (topic: Topic) => void return selectTopicById(store.getState(), topicId)
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)
}
export function getTopic(assistant: Assistant, topicId: string) {
return assistant?.topics.find((topic) => topic.id === topicId)
} }
export async function getTopicById(topicId: string) { export async function getTopicById(topicId: string) {
const assistants = store.getState().assistants.assistants const topic = selectTopicById(store.getState(), topicId)
const topics = assistants.map((assistant) => assistant.topics).flat()
const topic = topics.find((topic) => topic.id === topicId)
const messages = await TopicManager.getTopicMessages(topicId) const messages = await TopicManager.getTopicMessages(topicId)
return { ...topic, messages } as Topic return { ...topic, messages } as Topic
} }
@@ -122,8 +86,7 @@ export const autoRenameTopic = async (assistant: Assistant, topicId: string) =>
startTopicRenaming(topicId) startTopicRenaming(topicId)
const data = { ...topic, name: topicName } as Topic const data = { ...topic, name: topicName } as Topic
topic.id === _activeTopic.id && _setActiveTopic(data) store.dispatch(topicsActions.updateTopic({ assistantId: assistant.id, topic: data }))
store.dispatch(updateTopic({ assistantId: assistant.id, topic: data }))
} finally { } finally {
finishTopicRenaming(topicId) finishTopicRenaming(topicId)
} }
@@ -137,8 +100,7 @@ export const autoRenameTopic = async (assistant: Assistant, topicId: string) =>
const summaryText = await fetchMessagesSummary({ messages: topic.messages, assistant }) const summaryText = await fetchMessagesSummary({ messages: topic.messages, assistant })
if (summaryText) { if (summaryText) {
const data = { ...topic, name: summaryText } const data = { ...topic, name: summaryText }
topic.id === _activeTopic.id && _setActiveTopic(data) store.dispatch(topicsActions.updateTopic({ assistantId: assistant.id, topic: data }))
store.dispatch(updateTopic({ assistantId: assistant.id, topic: data }))
} }
} finally { } finally {
finishTopicRenaming(topicId) finishTopicRenaming(topicId)

View File

@@ -1,5 +1,17 @@
{ {
"translation": { "translation": {
"title": {
"home": "Home",
"agents": "Agents",
"paintings": "Paintings",
"translate": "Translate",
"files": "Files",
"knowledge": "Knowledge Base",
"apps": "Apps",
"mcp-servers": "MCP Servers",
"settings": "Settings",
"launchpad": "Launchpad"
},
"agents": { "agents": {
"add.button": "Add to Assistant", "add.button": "Add to Assistant",
"add.knowledge_base": "Knowledge Base", "add.knowledge_base": "Knowledge Base",
@@ -463,6 +475,7 @@
"pinyin.asc": "Sort by Pinyin (A-Z)", "pinyin.asc": "Sort by Pinyin (A-Z)",
"pinyin.desc": "Sort by Pinyin (Z-A)" "pinyin.desc": "Sort by Pinyin (Z-A)"
}, },
"apps": "Apps",
"success": "Success", "success": "Success",
"swap": "Swap", "swap": "Swap",
"topics": "Topics", "topics": "Topics",
@@ -522,7 +535,7 @@
"count": "files", "count": "files",
"created_at": "Created At", "created_at": "Created At",
"delete": "Delete", "delete": "Delete",
"delete.content": "Deleting a file will delete its reference from all messages. Are you sure you want to delete this file?", "delete.content": "Deleting a file will delete its reference from all messages. Are you sure you want to delete {{count}} files?",
"delete.paintings.warning": "Image contains this file, deletion is not possible", "delete.paintings.warning": "Image contains this file, deletion is not possible",
"delete.title": "Delete File", "delete.title": "Delete File",
"document": "Document", "document": "Document",
@@ -534,7 +547,9 @@
"size": "Size", "size": "Size",
"text": "Text", "text": "Text",
"title": "Files", "title": "Files",
"type": "Type" "type": "Type",
"batch_operation": "Batch Operation",
"batch_delete": "Batch Delete"
}, },
"gpustack": { "gpustack": {
"keep_alive_time.description": "The time in minutes to keep the connection alive, default is 5 minutes.", "keep_alive_time.description": "The time in minutes to keep the connection alive, default is 5 minutes.",

View File

@@ -1,5 +1,17 @@
{ {
"translation": { "translation": {
"title": {
"home": "ホーム",
"agents": "エージェント",
"paintings": "ペインティング",
"translate": "翻訳",
"files": "ファイル",
"knowledge": "ナレッジベース",
"apps": "アプリ",
"mcp-servers": "MCP サーバー",
"settings": "設定",
"launchpad": "ランチパッド"
},
"agents": { "agents": {
"add.button": "アシスタントに追加", "add.button": "アシスタントに追加",
"add.knowledge_base": "ナレッジベース", "add.knowledge_base": "ナレッジベース",
@@ -463,6 +475,7 @@
"pinyin.asc": "ピンインで昇順ソート", "pinyin.asc": "ピンインで昇順ソート",
"pinyin.desc": "ピンインで降順ソート" "pinyin.desc": "ピンインで降順ソート"
}, },
"apps": "アプリ",
"success": "成功", "success": "成功",
"swap": "交換", "swap": "交換",
"topics": "トピック", "topics": "トピック",
@@ -522,7 +535,7 @@
"count": "ファイル", "count": "ファイル",
"created_at": "作成日", "created_at": "作成日",
"delete": "削除", "delete": "削除",
"delete.content": "ファイルを削除すると、ファイルがすべてのメッセージで参照されることを削除します。このファイルを削除してもよろしいですか?", "delete.content": "ファイルを削除すると、ファイルがすべてのメッセージで参照されることを削除します。この{{count}}ファイルを削除してもよろしいですか?",
"delete.paintings.warning": "画像に含まれているため、削除できません", "delete.paintings.warning": "画像に含まれているため、削除できません",
"delete.title": "ファイルを削除", "delete.title": "ファイルを削除",
"document": "ドキュメント", "document": "ドキュメント",
@@ -534,7 +547,9 @@
"size": "サイズ", "size": "サイズ",
"text": "テキスト", "text": "テキスト",
"title": "ファイル", "title": "ファイル",
"type": "タイプ" "type": "タイプ",
"batch_operation": "一括操作",
"batch_delete": "一括削除"
}, },
"gpustack": { "gpustack": {
"keep_alive_time.description": "モデルがメモリに保持される時間デフォルト5分", "keep_alive_time.description": "モデルがメモリに保持される時間デフォルト5分",

View File

@@ -1,5 +1,17 @@
{ {
"translation": { "translation": {
"title": {
"home": "Главная",
"agents": "Агенты",
"paintings": "Рисунки",
"translate": "Перевод",
"files": "Файлы",
"knowledge": "База знаний",
"apps": "Приложения",
"mcp-servers": "MCP серверы",
"settings": "Настройки",
"launchpad": "Запуск"
},
"agents": { "agents": {
"add.button": "Добавить в ассистента", "add.button": "Добавить в ассистента",
"add.knowledge_base": "База знаний", "add.knowledge_base": "База знаний",
@@ -467,7 +479,8 @@
"swap": "Поменять местами", "swap": "Поменять местами",
"topics": "Топики", "topics": "Топики",
"warning": "Предупреждение", "warning": "Предупреждение",
"you": "Вы" "you": "Вы",
"apps": "Приложения"
}, },
"docs": { "docs": {
"title": "Документация" "title": "Документация"
@@ -522,7 +535,7 @@
"count": "файлов", "count": "файлов",
"created_at": "Дата создания", "created_at": "Дата создания",
"delete": "Удалить", "delete": "Удалить",
"delete.content": "Удаление файла удалит его из всех сообщений, вы уверены, что хотите удалить этот файл?", "delete.content": "Удаление файла удалит его из всех сообщений, вы уверены, что хотите удалить этот {{count}} файл",
"delete.paintings.warning": "В изображениях содержится этот файл, удаление невозможно", "delete.paintings.warning": "В изображениях содержится этот файл, удаление невозможно",
"delete.title": "Удалить файл", "delete.title": "Удалить файл",
"document": "Документ", "document": "Документ",
@@ -534,7 +547,9 @@
"size": "Размер", "size": "Размер",
"text": "Текст", "text": "Текст",
"title": "Файлы", "title": "Файлы",
"type": "Тип" "type": "Тип",
"batch_operation": "Пакетная операция",
"batch_delete": "Пакетное удаление"
}, },
"gpustack": { "gpustack": {
"keep_alive_time.description": "Время в минутах, в течение которого модель остается активной, по умолчанию 5 минут.", "keep_alive_time.description": "Время в минутах, в течение которого модель остается активной, по умолчанию 5 минут.",

View File

@@ -1,5 +1,17 @@
{ {
"translation": { "translation": {
"title": {
"home": "首页",
"agents": "智能体",
"paintings": "绘画",
"translate": "翻译",
"files": "文件",
"knowledge": "知识库",
"apps": "小程序",
"mcp-servers": "MCP 服务器",
"settings": "设置",
"launchpad": "启动台"
},
"agents": { "agents": {
"add.button": "添加到助手", "add.button": "添加到助手",
"add.knowledge_base": "知识库", "add.knowledge_base": "知识库",
@@ -467,7 +479,8 @@
"swap": "交换", "swap": "交换",
"topics": "话题", "topics": "话题",
"warning": "警告", "warning": "警告",
"you": "用户" "you": "用户",
"apps": "应用"
}, },
"docs": { "docs": {
"title": "帮助文档" "title": "帮助文档"
@@ -522,7 +535,7 @@
"count": "个文件", "count": "个文件",
"created_at": "创建时间", "created_at": "创建时间",
"delete": "删除", "delete": "删除",
"delete.content": "删除文件会删除文件在所有消息中的引用,确定要删除文件吗", "delete.content": "删除文件会删除文件在所有消息中的引用,确定要删除这{{count}}个文件吗?",
"delete.paintings.warning": "绘图中包含该图片,暂时无法删除", "delete.paintings.warning": "绘图中包含该图片,暂时无法删除",
"delete.title": "删除文件", "delete.title": "删除文件",
"document": "文档", "document": "文档",
@@ -534,7 +547,9 @@
"size": "大小", "size": "大小",
"text": "文本", "text": "文本",
"title": "文件", "title": "文件",
"type": "类型" "type": "类型",
"batch_operation": "批量操作",
"batch_delete": "批量删除"
}, },
"gpustack": { "gpustack": {
"keep_alive_time.description": "模型在内存中保持的时间默认5 分钟)", "keep_alive_time.description": "模型在内存中保持的时间默认5 分钟)",

View File

@@ -1,5 +1,17 @@
{ {
"translation": { "translation": {
"title": {
"home": "主頁",
"agents": "智能體",
"paintings": "繪畫",
"translate": "翻譯",
"files": "文件",
"knowledge": "知識庫",
"apps": "小程序",
"mcp-servers": "MCP 伺服器",
"settings": "設定",
"launchpad": "啟動台"
},
"agents": { "agents": {
"add.button": "新增到助手", "add.button": "新增到助手",
"add.knowledge_base": "知識庫", "add.knowledge_base": "知識庫",
@@ -467,7 +479,8 @@
"swap": "交換", "swap": "交換",
"topics": "話題", "topics": "話題",
"warning": "警告", "warning": "警告",
"you": "您" "you": "您",
"apps": "應用"
}, },
"docs": { "docs": {
"title": "說明文件" "title": "說明文件"
@@ -522,7 +535,7 @@
"count": "個檔案", "count": "個檔案",
"created_at": "建立時間", "created_at": "建立時間",
"delete": "刪除", "delete": "刪除",
"delete.content": "刪除檔案會刪除檔案在所有訊息中的引用,確定要刪除檔案嗎?", "delete.content": "刪除檔案會刪除檔案在所有訊息中的引用,確定要刪除這{{count}}個檔案嗎?",
"delete.paintings.warning": "繪圖中包含該圖片,暫時無法刪除", "delete.paintings.warning": "繪圖中包含該圖片,暫時無法刪除",
"delete.title": "刪除檔案", "delete.title": "刪除檔案",
"document": "文件", "document": "文件",
@@ -534,7 +547,9 @@
"size": "大小", "size": "大小",
"text": "文字", "text": "文字",
"title": "檔案", "title": "檔案",
"type": "類型" "type": "類型",
"batch_operation": "批量操作",
"batch_delete": "批量刪除"
}, },
"gpustack": { "gpustack": {
"keep_alive_time.description": "模型在記憶體中保持的時間(預設為 5 分鐘)", "keep_alive_time.description": "模型在記憶體中保持的時間(預設為 5 分鐘)",
@@ -2448,4 +2463,4 @@
"visualization": "視覺化" "visualization": "視覺化"
} }
} }
} }

View File

@@ -1575,7 +1575,6 @@
"theme.light": "Φωτεινό", "theme.light": "Φωτεινό",
"theme.system": "Σύστημα", "theme.system": "Σύστημα",
"theme.title": "Θέμα", "theme.title": "Θέμα",
"theme.window.style.opaque": "Μη διαφανή παράθυρα",
"theme.window.style.title": "Στυλ παραθύρων", "theme.window.style.title": "Στυλ παραθύρων",
"theme.window.style.transparent": "Διαφανή παράθυρα", "theme.window.style.transparent": "Διαφανή παράθυρα",
"title": "Ρυθμίσεις", "title": "Ρυθμίσεις",

View File

@@ -1574,7 +1574,6 @@
"theme.light": "Claro", "theme.light": "Claro",
"theme.system": "Sistema", "theme.system": "Sistema",
"theme.title": "Tema", "theme.title": "Tema",
"theme.window.style.opaque": "Ventana opaca",
"theme.window.style.title": "Estilo de ventana", "theme.window.style.title": "Estilo de ventana",
"theme.window.style.transparent": "Ventana transparente", "theme.window.style.transparent": "Ventana transparente",
"title": "Configuración", "title": "Configuración",

View File

@@ -1575,7 +1575,6 @@
"theme.light": "Clair", "theme.light": "Clair",
"theme.system": "Système", "theme.system": "Système",
"theme.title": "Thème", "theme.title": "Thème",
"theme.window.style.opaque": "Fenêtre opaque",
"theme.window.style.title": "Style de fenêtre", "theme.window.style.title": "Style de fenêtre",
"theme.window.style.transparent": "Fenêtre transparente", "theme.window.style.transparent": "Fenêtre transparente",
"title": "Paramètres", "title": "Paramètres",

View File

@@ -1576,7 +1576,6 @@
"theme.light": "Claro", "theme.light": "Claro",
"theme.system": "Sistema", "theme.system": "Sistema",
"theme.title": "Tema", "theme.title": "Tema",
"theme.window.style.opaque": "Janela opaca",
"theme.window.style.title": "Estilo de janela", "theme.window.style.title": "Estilo de janela",
"theme.window.style.transparent": "Janela transparente", "theme.window.style.transparent": "Janela transparente",
"title": "Configurações", "title": "Configurações",

View File

@@ -1,9 +1,9 @@
import { ImportOutlined, PlusOutlined } from '@ant-design/icons' import { ImportOutlined, PlusOutlined } from '@ant-design/icons'
import { Navbar, NavbarCenter } from '@renderer/components/app/Navbar' import { NavbarCenter, NavbarMain } from '@renderer/components/app/Navbar'
import CustomTag from '@renderer/components/CustomTag' import CustomTag from '@renderer/components/CustomTag'
import ListItem from '@renderer/components/ListItem' import ListItem from '@renderer/components/ListItem'
import Scrollbar from '@renderer/components/Scrollbar' import Scrollbar from '@renderer/components/Scrollbar'
import { useAgents } from '@renderer/hooks/useAgents' import { useAssistants } from '@renderer/hooks/useAssistant'
import { createAssistantFromAgent } from '@renderer/services/AssistantService' import { createAssistantFromAgent } from '@renderer/services/AssistantService'
import { Agent } from '@renderer/types' import { Agent } from '@renderer/types'
import { uuid } from '@renderer/utils' import { uuid } from '@renderer/utils'
@@ -28,7 +28,7 @@ const AgentsPage: FC = () => {
const [activeGroup, setActiveGroup] = useState('我的') const [activeGroup, setActiveGroup] = useState('我的')
const [agentGroups, setAgentGroups] = useState<Record<string, Agent[]>>({}) const [agentGroups, setAgentGroups] = useState<Record<string, Agent[]>>({})
const systemAgents = useSystemAgents() const systemAgents = useSystemAgents()
const { agents: userAgents } = useAgents() const { templates: userAgents } = useAssistants()
useEffect(() => { useEffect(() => {
const systemAgentsGroupList = groupByCategories(systemAgents) const systemAgentsGroupList = groupByCategories(systemAgents)
@@ -152,7 +152,7 @@ const AgentsPage: FC = () => {
return ( return (
<Container> <Container>
<Navbar> <NavbarMain>
<NavbarCenter style={{ borderRight: 'none', justifyContent: 'space-between' }}> <NavbarCenter style={{ borderRight: 'none', justifyContent: 'space-between' }}>
{t('agents.title')} {t('agents.title')}
<Input <Input
@@ -169,9 +169,9 @@ const AgentsPage: FC = () => {
onChange={(e) => setSearchInput(e.target.value)} onChange={(e) => setSearchInput(e.target.value)}
onPressEnter={handleSearch} onPressEnter={handleSearch}
/> />
<div style={{ width: 80 }} /> <div style={{ width: 1 }} />
</NavbarCenter> </NavbarCenter>
</Navbar> </NavbarMain>
<Main id="content-container"> <Main id="content-container">
<AgentsGroupList> <AgentsGroupList>
@@ -321,7 +321,7 @@ const AgentDescription = styled.div`
` `
const AgentPrompt = styled.div` const AgentPrompt = styled.div`
max-height: 60vh; max-height: 50vh;
overflow-y: scroll; overflow-y: scroll;
background-color: var(--color-background-soft); background-color: var(--color-background-soft);
padding: 8px; padding: 8px;

View File

@@ -4,7 +4,7 @@ import { CheckOutlined, LoadingOutlined, RollbackOutlined, ThunderboltOutlined }
import EmojiPicker from '@renderer/components/EmojiPicker' import EmojiPicker from '@renderer/components/EmojiPicker'
import { TopView } from '@renderer/components/TopView' import { TopView } from '@renderer/components/TopView'
import { AGENT_PROMPT } from '@renderer/config/prompts' import { AGENT_PROMPT } from '@renderer/config/prompts'
import { useAgents } from '@renderer/hooks/useAgents' import { useAssistants } from '@renderer/hooks/useAssistant'
import { useSidebarIconShow } from '@renderer/hooks/useSidebarIcon' import { useSidebarIconShow } from '@renderer/hooks/useSidebarIcon'
import { fetchGenerate } from '@renderer/services/ApiService' import { fetchGenerate } from '@renderer/services/ApiService'
import { getDefaultModel } from '@renderer/services/AssistantService' import { getDefaultModel } from '@renderer/services/AssistantService'
@@ -14,6 +14,7 @@ import { Agent, KnowledgeBase } from '@renderer/types'
import { getLeadingEmoji, uuid } from '@renderer/utils' import { getLeadingEmoji, uuid } from '@renderer/utils'
import { Button, Form, FormInstance, Input, Modal, Popover, Select, SelectProps } from 'antd' import { Button, Form, FormInstance, Input, Modal, Popover, Select, SelectProps } from 'antd'
import TextArea from 'antd/es/input/TextArea' import TextArea from 'antd/es/input/TextArea'
import { ChevronDown } from 'lucide-react'
import { useEffect, useRef, useState } from 'react' import { useEffect, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import stringWidth from 'string-width' import stringWidth from 'string-width'
@@ -34,7 +35,7 @@ const PopupContainer: React.FC<Props> = ({ resolve }) => {
const [open, setOpen] = useState(true) const [open, setOpen] = useState(true)
const [form] = Form.useForm() const [form] = Form.useForm()
const { t } = useTranslation() const { t } = useTranslation()
const { addAgent } = useAgents() const { addTemplate } = useAssistants()
const formRef = useRef<FormInstance>(null) const formRef = useRef<FormInstance>(null)
const [emoji, setEmoji] = useState('') const [emoji, setEmoji] = useState('')
const [loading, setLoading] = useState(false) const [loading, setLoading] = useState(false)
@@ -83,12 +84,12 @@ const PopupContainer: React.FC<Props> = ({ resolve }) => {
emoji: _emoji, emoji: _emoji,
prompt: values.prompt, prompt: values.prompt,
defaultModel: getDefaultModel(), defaultModel: getDefaultModel(),
type: 'agent',
topics: [], topics: [],
messages: [] messages: [],
isTemplate: true
} }
addAgent(_agent) addTemplate(_agent)
resolve(_agent) resolve(_agent)
setOpen(false) setOpen(false)
} }
@@ -239,6 +240,7 @@ const PopupContainer: React.FC<Props> = ({ resolve }) => {
.toLowerCase() .toLowerCase()
.includes(input.toLowerCase()) .includes(input.toLowerCase())
} }
suffixIcon={<ChevronDown size={16} color="var(--color-border)" />}
/> />
</Form.Item> </Form.Item>
)} )}

View File

@@ -7,7 +7,7 @@ import {
SortAscendingOutlined SortAscendingOutlined
} from '@ant-design/icons' } from '@ant-design/icons'
import CustomTag from '@renderer/components/CustomTag' import CustomTag from '@renderer/components/CustomTag'
import { useAgents } from '@renderer/hooks/useAgents' import { useAssistants } from '@renderer/hooks/useAssistant'
import AssistantSettingsPopup from '@renderer/pages/settings/AssistantSettings' import AssistantSettingsPopup from '@renderer/pages/settings/AssistantSettings'
import { createAssistantFromAgent } from '@renderer/services/AssistantService' import { createAssistantFromAgent } from '@renderer/services/AssistantService'
import type { Agent } from '@renderer/types' import type { Agent } from '@renderer/types'
@@ -27,7 +27,7 @@ interface Props {
} }
const AgentCard: FC<Props> = ({ agent, onClick, activegroup, getLocalizedGroupName }) => { const AgentCard: FC<Props> = ({ agent, onClick, activegroup, getLocalizedGroupName }) => {
const { removeAgent } = useAgents() const { removeAssistant: removeAgent } = useAssistants()
const [isVisible, setIsVisible] = useState(false) const [isVisible, setIsVisible] = useState(false)
const cardRef = useRef<HTMLDivElement>(null) const cardRef = useRef<HTMLDivElement>(null)

View File

@@ -1,5 +1,5 @@
import { TopView } from '@renderer/components/TopView' import { TopView } from '@renderer/components/TopView'
import { useAgents } from '@renderer/hooks/useAgents' import { useAssistants } from '@renderer/hooks/useAssistant'
import { getDefaultModel } from '@renderer/services/AssistantService' import { getDefaultModel } from '@renderer/services/AssistantService'
import { EVENT_NAMES, EventEmitter } from '@renderer/services/EventService' import { EVENT_NAMES, EventEmitter } from '@renderer/services/EventService'
import { Agent } from '@renderer/types' import { Agent } from '@renderer/types'
@@ -16,7 +16,7 @@ const PopupContainer: React.FC<Props> = ({ resolve }) => {
const [open, setOpen] = useState(true) const [open, setOpen] = useState(true)
const [form] = Form.useForm() const [form] = Form.useForm()
const { t } = useTranslation() const { t } = useTranslation()
const { addAgent } = useAgents() const { addTemplate: addAgent } = useAssistants()
const [importType, setImportType] = useState<'url' | 'file'>('url') const [importType, setImportType] = useState<'url' | 'file'>('url')
const [loading, setLoading] = useState(false) const [loading, setLoading] = useState(false)

View File

@@ -2,7 +2,7 @@ import { MenuOutlined } from '@ant-design/icons'
import { DraggableList } from '@renderer/components/DraggableList' import { DraggableList } from '@renderer/components/DraggableList'
import { Box, HStack } from '@renderer/components/Layout' import { Box, HStack } from '@renderer/components/Layout'
import { TopView } from '@renderer/components/TopView' import { TopView } from '@renderer/components/TopView'
import { useAgents } from '@renderer/hooks/useAgents' import { useAssistants } from '@renderer/hooks/useAssistant'
import { Empty, Modal } from 'antd' import { Empty, Modal } from 'antd'
import { useEffect, useState } from 'react' import { useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
@@ -11,7 +11,8 @@ import styled from 'styled-components'
const PopupContainer: React.FC = () => { const PopupContainer: React.FC = () => {
const [open, setOpen] = useState(true) const [open, setOpen] = useState(true)
const { t } = useTranslation() const { t } = useTranslation()
const { agents, updateAgents } = useAgents() const { assistants, updateAssistants: updateAgents } = useAssistants()
const agents = assistants.filter((a) => a.isTemplate)
const onOk = () => { const onOk = () => {
setOpen(false) setOpen(false)

View File

@@ -1,7 +1,7 @@
import { Navbar, NavbarMain } from '@renderer/components/app/Navbar' import { NavbarCenter, NavbarMain } from '@renderer/components/app/Navbar'
import { useMinapps } from '@renderer/hooks/useMinapps' import { useMinapps } from '@renderer/hooks/useMinapps'
import { Button, Input } from 'antd' import { Button, Input } from 'antd'
import { Search, SettingsIcon, X } from 'lucide-react' import { Search, SettingsIcon } from 'lucide-react'
import React, { FC, useEffect, useState } from 'react' import React, { FC, useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { useLocation } from 'react-router' import { useLocation } from 'react-router'
@@ -41,8 +41,8 @@ const AppsPage: FC = () => {
return ( return (
<Container onContextMenu={handleContextMenu}> <Container onContextMenu={handleContextMenu}>
<Navbar> <NavbarMain>
<NavbarMain> <NavbarCenter>
{t('minapp.title')} {t('minapp.title')}
<Input <Input
placeholder={t('common.search')} placeholder={t('common.search')}
@@ -50,10 +50,7 @@ const AppsPage: FC = () => {
style={{ style={{
width: '30%', width: '30%',
height: 28, height: 28,
borderRadius: 15, borderRadius: 15
position: 'absolute',
left: '50vw',
transform: 'translateX(-50%)'
}} }}
size="small" size="small"
variant="filled" variant="filled"
@@ -65,11 +62,11 @@ const AppsPage: FC = () => {
<Button <Button
type="text" type="text"
className="nodrag" className="nodrag"
icon={isSettingsOpen ? <X size={18} /> : <SettingsIcon size={18} color="var(--color-text-2)" />} icon={<SettingsIcon size={18} color={isSettingsOpen ? 'var(--color-primary)' : 'var(--color-text-2)'} />}
onClick={() => setIsSettingsOpen(!isSettingsOpen)} onClick={() => setIsSettingsOpen(!isSettingsOpen)}
/> />
</NavbarMain> </NavbarCenter>
</Navbar> </NavbarMain>
<ContentContainer id="content-container"> <ContentContainer id="content-container">
{isSettingsOpen && <MiniAppSettings />} {isSettingsOpen && <MiniAppSettings />}
{!isSettingsOpen && ( {!isSettingsOpen && (

View File

@@ -1,17 +1,12 @@
import { DeleteOutlined, ExclamationCircleOutlined } from '@ant-design/icons'
import { handleDelete } from '@renderer/services/FileAction'
import FileManager from '@renderer/services/FileManager'
import { FileMetadata, FileTypes } from '@renderer/types' import { FileMetadata, FileTypes } from '@renderer/types'
import { formatFileSize } from '@renderer/utils'
import { Col, Image, Row, Spin } from 'antd'
import { t } from 'i18next' import { t } from 'i18next'
import VirtualList from 'rc-virtual-list' import VirtualList from 'rc-virtual-list'
import React, { memo } from 'react' import React, { memo } from 'react'
import styled from 'styled-components'
import FileItem from './FileItem' import FileItem from './FileItem'
import ImageList from './ImageList'
interface FileItemProps { interface FileListProps {
id: FileTypes | 'all' | string id: FileTypes | 'all' | string
list: { list: {
key: FileTypes | 'all' | string key: FileTypes | 'all' | string
@@ -26,55 +21,9 @@ interface FileItemProps {
files?: FileMetadata[] files?: FileMetadata[]
} }
const FileList: React.FC<FileItemProps> = ({ id, list, files }) => { const FileList: React.FC<FileListProps> = ({ id, list, files }) => {
if (id === FileTypes.IMAGE && files?.length && files?.length > 0) { if (id === FileTypes.IMAGE && files?.length && files?.length > 0) {
return ( return <ImageList files={files}></ImageList>
<div style={{ padding: 16, overflowY: 'auto' }}>
<Image.PreviewGroup>
<Row gutter={[16, 16]}>
{files?.map((file) => (
<Col key={file.id} xs={24} sm={12} md={8} lg={4} xl={3}>
<ImageWrapper>
<LoadingWrapper>
<Spin />
</LoadingWrapper>
<Image
src={FileManager.getFileUrl(file)}
style={{ height: '100%', objectFit: 'cover', cursor: 'pointer' }}
preview={{ mask: false }}
onLoad={(e) => {
const img = e.target as HTMLImageElement
img.parentElement?.classList.add('loaded')
}}
/>
<ImageInfo>
<div>{formatFileSize(file.size)}</div>
</ImageInfo>
<DeleteButton
title={t('files.delete.title')}
onClick={(e) => {
e.stopPropagation()
window.modal.confirm({
title: t('files.delete.title'),
content: t('files.delete.content'),
okText: t('common.confirm'),
cancelText: t('common.cancel'),
centered: true,
onOk: () => {
handleDelete(file.id, t)
},
icon: <ExclamationCircleOutlined style={{ color: 'red' }} />
})
}}>
<DeleteOutlined />
</DeleteButton>
</ImageWrapper>
</Col>
))}
</Row>
</Image.PreviewGroup>
</div>
)
} }
return ( return (
@@ -113,92 +62,4 @@ const FileList: React.FC<FileItemProps> = ({ id, list, files }) => {
) )
} }
const ImageWrapper = styled.div`
position: relative;
aspect-ratio: 1;
overflow: hidden;
border-radius: 8px;
background-color: var(--color-background-soft);
display: flex;
align-items: center;
justify-content: center;
border: 0.5px solid var(--color-border);
.ant-image {
height: 100%;
width: 100%;
opacity: 0;
transition:
opacity 0.3s ease,
transform 0.3s ease;
&.loaded {
opacity: 1;
}
}
&:hover {
.ant-image.loaded {
transform: scale(1.05);
}
div:last-child {
opacity: 1;
}
}
`
const LoadingWrapper = styled.div`
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
display: flex;
align-items: center;
justify-content: center;
background-color: var(--color-background-soft);
`
const ImageInfo = styled.div`
position: absolute;
bottom: 0;
left: 0;
right: 0;
background: rgba(0, 0, 0, 0.6);
color: white;
padding: 5px 8px;
opacity: 0;
transition: opacity 0.3s ease;
font-size: 12px;
> div:first-child {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
`
const DeleteButton = styled.div`
position: absolute;
top: 8px;
right: 8px;
width: 24px;
height: 24px;
border-radius: 50%;
background-color: rgba(0, 0, 0, 0.6);
color: white;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
opacity: 0;
transition: opacity 0.3s ease;
z-index: 1;
&:hover {
background-color: rgba(255, 0, 0, 0.8);
}
`
export default memo(FileList) export default memo(FileList)

View File

@@ -5,18 +5,19 @@ import {
SortAscendingOutlined, SortAscendingOutlined,
SortDescendingOutlined SortDescendingOutlined
} from '@ant-design/icons' } from '@ant-design/icons'
import { Navbar, NavbarCenter } from '@renderer/components/app/Navbar' import { NavbarCenter, NavbarMain } from '@renderer/components/app/Navbar'
import ListItem from '@renderer/components/ListItem' import ListItem from '@renderer/components/ListItem'
import db from '@renderer/databases' import db from '@renderer/databases'
import { handleDelete, handleRename, sortFiles, tempFilesSort } from '@renderer/services/FileAction' import { handleDelete, handleRename, sortFiles, tempFilesSort } from '@renderer/services/FileAction'
import FileManager from '@renderer/services/FileManager' import FileManager from '@renderer/services/FileManager'
import store from '@renderer/store'
import { FileMetadata, FileTypes } from '@renderer/types' import { FileMetadata, FileTypes } from '@renderer/types'
import { formatFileSize } from '@renderer/utils' import { formatFileSize } from '@renderer/utils'
import { Button, Empty, Flex, Popconfirm } from 'antd' import { Button, Checkbox, Dropdown, Empty, Flex, Popconfirm } from 'antd'
import dayjs from 'dayjs' import dayjs from 'dayjs'
import { useLiveQuery } from 'dexie-react-hooks' import { useLiveQuery } from 'dexie-react-hooks'
import { File as FileIcon, FileImage, FileText, FileType as FileTypeIcon } from 'lucide-react' import { File as FileIcon, FileImage, FileText, FileType as FileTypeIcon } from 'lucide-react'
import { FC, useState } from 'react' import { FC, useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import styled from 'styled-components' import styled from 'styled-components'
@@ -30,6 +31,11 @@ const FilesPage: FC = () => {
const [fileType, setFileType] = useState<string>('document') const [fileType, setFileType] = useState<string>('document')
const [sortField, setSortField] = useState<SortField>('created_at') const [sortField, setSortField] = useState<SortField>('created_at')
const [sortOrder, setSortOrder] = useState<SortOrder>('desc') const [sortOrder, setSortOrder] = useState<SortOrder>('desc')
const [selectedFileIds, setSelectedFileIds] = useState<string[]>([])
useEffect(() => {
setSelectedFileIds([])
}, [fileType])
const files = useLiveQuery<FileMetadata[]>(() => { const files = useLiveQuery<FileMetadata[]>(() => {
if (fileType === 'all') { if (fileType === 'all') {
@@ -40,6 +46,46 @@ const FilesPage: FC = () => {
const sortedFiles = files ? sortFiles(files, sortField, sortOrder) : [] const sortedFiles = files ? sortFiles(files, sortField, sortOrder) : []
const handleBatchDelete = async () => {
const selectedFiles = await Promise.all(selectedFileIds.map((id) => FileManager.getFile(id)))
const validFiles = selectedFiles.filter((file): file is FileType => file !== null && file !== undefined)
const paintings = await store.getState().paintings.paintings
const paintingsFiles = paintings.flatMap((p) => p.files)
const filesInPaintings = validFiles.filter((file) => paintingsFiles.some((p) => p.id === file.id))
if (filesInPaintings.length > 0) {
window.modal.warning({
content: t('files.delete.paintings.warning', { count: filesInPaintings.length }),
centered: true
})
return
}
for (const fileId of selectedFileIds) {
await handleDelete(fileId, t, setSelectedFileIds)
}
setSelectedFileIds([])
}
const handleSelectFile = (fileId: string, checked: boolean) => {
if (checked) {
setSelectedFileIds((prev) => [...prev, fileId])
} else {
setSelectedFileIds((prev) => prev.filter((id) => id !== fileId))
}
}
const handleSelectAll = (checked: boolean) => {
if (checked) {
setSelectedFileIds(sortedFiles.map((file) => file.id))
} else {
setSelectedFileIds([])
}
}
const dataSource = sortedFiles?.map((file) => { const dataSource = sortedFiles?.map((file) => {
return { return {
key: file.id, key: file.id,
@@ -56,13 +102,20 @@ const FilesPage: FC = () => {
<Button type="text" icon={<EditOutlined />} onClick={() => handleRename(file.id)} /> <Button type="text" icon={<EditOutlined />} onClick={() => handleRename(file.id)} />
<Popconfirm <Popconfirm
title={t('files.delete.title')} title={t('files.delete.title')}
description={t('files.delete.content')} description={t('files.delete.content', { count: 1 })}
okText={t('common.confirm')} okText={t('common.confirm')}
cancelText={t('common.cancel')} cancelText={t('common.cancel')}
onConfirm={() => handleDelete(file.id, t)} onConfirm={() => handleDelete(file.id, t, setSelectedFileIds)}
icon={<ExclamationCircleOutlined style={{ color: 'red' }} />}> icon={<ExclamationCircleOutlined style={{ color: 'red' }} />}>
<Button type="text" danger icon={<DeleteOutlined />} /> <Button type="text" danger icon={<DeleteOutlined />} />
</Popconfirm> </Popconfirm>
{fileType !== 'image' && (
<Checkbox
checked={selectedFileIds.includes(file.id)}
onChange={(e) => handleSelectFile(file.id, e.target.checked)}
style={{ margin: '0 8px' }}
/>
)}
</Flex> </Flex>
) )
} }
@@ -77,10 +130,72 @@ const FilesPage: FC = () => {
return ( return (
<Container> <Container>
<Navbar> <NavbarMain>
<NavbarCenter style={{ borderRight: 'none' }}>{t('files.title')}</NavbarCenter> <NavbarCenter style={{ borderRight: 'none' }}>{t('files.title')}</NavbarCenter>
</Navbar> </NavbarMain>
<ContentContainer id="content-container"> <ContentContainer id="content-container">
<MainContent>
<SortContainer>
<Flex gap={8} align="center">
{['created_at', 'size', 'name'].map((field) => (
<Button
color="default"
key={field}
variant={sortField === field ? 'filled' : 'text'}
onClick={() => {
if (sortField === field) {
setSortOrder(sortOrder === 'asc' ? 'desc' : 'asc')
} else {
setSortField(field as 'created_at' | 'size' | 'name')
setSortOrder('desc')
}
}}>
{t(`files.${field}`)}
{sortField === field &&
(sortOrder === 'desc' ? <SortDescendingOutlined /> : <SortAscendingOutlined />)}
</Button>
))}
</Flex>
{fileType !== 'image' && (
<Dropdown.Button
style={{ width: 'auto' }}
menu={{
items: [
{
key: 'delete',
disabled: selectedFileIds.length === 0,
danger: true,
label: (
<Popconfirm
disabled={selectedFileIds.length === 0}
title={t('files.delete.title')}
description={t('files.delete.content', { count: selectedFileIds.length })}
okText={t('common.confirm')}
cancelText={t('common.cancel')}
onConfirm={handleBatchDelete}
icon={<ExclamationCircleOutlined />}>
{t('files.batch_delete')} ({selectedFileIds.length})
</Popconfirm>
)
}
]
}}
trigger={['click']}>
<Checkbox
indeterminate={selectedFileIds.length > 0 && selectedFileIds.length < sortedFiles.length}
checked={selectedFileIds.length === sortedFiles.length}
onChange={(e) => handleSelectAll(e.target.checked)}>
{t('files.batch_operation')}
</Checkbox>
</Dropdown.Button>
)}
</SortContainer>
{dataSource && dataSource?.length > 0 ? (
<FileList id={fileType} list={dataSource} files={sortedFiles} />
) : (
<Empty image={Empty.PRESENTED_IMAGE_SIMPLE} />
)}
</MainContent>
<SideNav> <SideNav>
{menuItems.map((item) => ( {menuItems.map((item) => (
<ListItem <ListItem
@@ -92,31 +207,6 @@ const FilesPage: FC = () => {
/> />
))} ))}
</SideNav> </SideNav>
<MainContent>
<SortContainer>
{['created_at', 'size', 'name'].map((field) => (
<SortButton
key={field}
active={sortField === field}
onClick={() => {
if (sortField === field) {
setSortOrder(sortOrder === 'asc' ? 'desc' : 'asc')
} else {
setSortField(field as 'created_at' | 'size' | 'name')
setSortOrder('desc')
}
}}>
{t(`files.${field}`)}
{sortField === field && (sortOrder === 'desc' ? <SortDescendingOutlined /> : <SortAscendingOutlined />)}
</SortButton>
))}
</SortContainer>
{dataSource && dataSource?.length > 0 ? (
<FileList id={fileType} list={dataSource} files={sortedFiles} />
) : (
<Empty image={Empty.PRESENTED_IMAGE_SIMPLE} />
)}
</MainContent>
</ContentContainer> </ContentContainer>
</Container> </Container>
) )
@@ -138,6 +228,7 @@ const MainContent = styled.div`
const SortContainer = styled.div` const SortContainer = styled.div`
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: space-between;
gap: 8px; gap: 8px;
padding: 8px 16px; padding: 8px 16px;
border-bottom: 0.5px solid var(--color-border); border-bottom: 0.5px solid var(--color-border);
@@ -184,25 +275,4 @@ const SideNav = styled.div`
} }
` `
const SortButton = styled(Button)<{ active?: boolean }>`
display: flex;
align-items: center;
gap: 4px;
padding: 4px 12px;
height: 30px;
border-radius: var(--list-item-border-radius);
border: 0.5px solid ${(props) => (props.active ? 'var(--color-border)' : 'transparent')};
background-color: ${(props) => (props.active ? 'var(--color-background-soft)' : 'transparent')};
color: ${(props) => (props.active ? 'var(--color-text)' : 'var(--color-text-secondary)')};
&:hover {
background-color: var(--color-background-soft);
color: var(--color-text);
}
.anticon {
font-size: 12px;
}
`
export default FilesPage export default FilesPage

View File

@@ -0,0 +1,149 @@
import { DeleteOutlined, ExclamationCircleOutlined } from '@ant-design/icons'
import { handleDelete } from '@renderer/services/FileAction'
import FileManager from '@renderer/services/FileManager'
import { FileType } from '@renderer/types'
import { formatFileSize } from '@renderer/utils'
import { Col, Image, Spin } from 'antd'
import { memo, useState } from 'react'
import { useTranslation } from 'react-i18next'
import styled from 'styled-components'
interface ImageItemProps {
file: FileType
}
const ImageItem: React.FC<ImageItemProps> = ({ file }) => {
const [loading, setLoading] = useState(true)
const { t } = useTranslation()
return (
<Col xs={24} sm={12} md={8} lg={4} xl={3}>
<ImageWrapper>
{loading && (
<LoadingWrapper>
<Spin />
</LoadingWrapper>
)}
<Image
src={FileManager.getFileUrl(file)}
style={{ height: '100%', objectFit: 'cover', cursor: 'pointer' }}
preview={{ mask: false }}
onLoad={(e) => {
const img = e.target as HTMLImageElement
img.parentElement?.classList.add('loaded')
setLoading(false)
}}
/>
<ImageInfo>
<div>{formatFileSize(file.size)}</div>
</ImageInfo>
<DeleteButton
title={t('files.delete.title')}
onClick={(e) => {
e.stopPropagation()
window.modal.confirm({
title: t('files.delete.title'),
content: t('files.delete.content'),
okText: t('common.confirm'),
cancelText: t('common.cancel'),
centered: true,
onOk: () => {
handleDelete(file.id, t)
},
icon: <ExclamationCircleOutlined style={{ color: 'red' }} />
})
}}>
<DeleteOutlined />
</DeleteButton>
</ImageWrapper>
</Col>
)
}
const ImageWrapper = styled.div`
position: relative;
aspect-ratio: 1;
overflow: hidden;
border-radius: 8px;
background-color: var(--color-background-soft);
display: flex;
align-items: center;
justify-content: center;
border: 0.5px solid var(--color-border);
.ant-image {
height: 100%;
width: 100%;
opacity: 0;
transition:
opacity 0.3s ease,
transform 0.3s ease;
&.loaded {
opacity: 1;
}
}
&:hover {
.ant-image.loaded {
transform: scale(1.05);
}
div:last-child {
opacity: 1;
}
}
`
const LoadingWrapper = styled.div`
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
display: flex;
align-items: center;
justify-content: center;
background-color: var(--color-background-soft);
`
const ImageInfo = styled.div`
position: absolute;
bottom: 0;
left: 0;
right: 0;
background: rgba(0, 0, 0, 0.6);
color: white;
padding: 5px 8px;
opacity: 0;
transition: opacity 0.3s ease;
font-size: 12px;
> div:first-child {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
`
const DeleteButton = styled.div`
position: absolute;
top: 8px;
right: 8px;
width: 24px;
height: 24px;
border-radius: 50%;
background-color: rgba(0, 0, 0, 0.6);
color: white;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
opacity: 0;
transition: opacity 0.3s ease;
z-index: 1;
&:hover {
background-color: rgba(255, 0, 0, 0.8);
}
`
export default memo(ImageItem)

View File

@@ -0,0 +1,21 @@
import { FileType } from '@renderer/types'
import { Image, Row } from 'antd'
import { memo } from 'react'
import ImageItem from './ImageItem'
interface ImageListProps {
files?: FileType[]
}
const ImageList: React.FC<ImageListProps> = ({ files }) => {
return (
<div style={{ padding: 16, overflowY: 'auto' }}>
<Image.PreviewGroup>
<Row gutter={[16, 16]}>{files?.map((file) => <ImageItem key={file.id} file={file}></ImageItem>)}</Row>
</Image.PreviewGroup>
</div>
)
}
export default memo(ImageList)

View File

@@ -1,4 +1,5 @@
import { HStack } from '@renderer/components/Layout' import { HStack } from '@renderer/components/Layout'
import { ChatProvider } from '@renderer/hooks/useChat'
import { useAppDispatch } from '@renderer/store' import { useAppDispatch } from '@renderer/store'
import { loadTopicMessagesThunk } from '@renderer/store/thunk/messageThunk' import { loadTopicMessagesThunk } from '@renderer/store/thunk/messageThunk'
import { Topic } from '@renderer/types' import { Topic } from '@renderer/types'
@@ -72,51 +73,53 @@ const TopicsPage: FC = () => {
}, []) }, [])
return ( return (
<Container> <ChatProvider>
<HStack style={{ padding: '0 12px', marginTop: 8 }}> <Container>
<Input <HStack style={{ padding: '0 12px', marginTop: 8 }}>
prefix={ <Input
stack.length > 1 ? ( prefix={
<SearchIcon className="back-icon" onClick={goBack}> stack.length > 1 ? (
<ChevronLeft size={16} /> <SearchIcon className="back-icon" onClick={goBack}>
</SearchIcon> <ChevronLeft size={16} />
) : ( </SearchIcon>
<SearchIcon> ) : (
<Search size={15} /> <SearchIcon>
</SearchIcon> <Search size={15} />
) </SearchIcon>
} )
suffix={search.length >= 2 ? <CornerDownLeft size={16} /> : null} }
ref={inputRef} suffix={search.length >= 2 ? <CornerDownLeft size={16} /> : null}
placeholder={t('history.search.placeholder')} ref={inputRef}
value={search} placeholder={t('history.search.placeholder')}
onChange={(e) => setSearch(e.target.value.trimStart())} value={search}
allowClear onChange={(e) => setSearch(e.target.value.trimStart())}
autoFocus allowClear
spellCheck={false} autoFocus
style={{ paddingLeft: 0 }} spellCheck={false}
variant="borderless" style={{ paddingLeft: 0 }}
size="middle" variant="borderless"
onPressEnter={onSearch} size="middle"
/> onPressEnter={onSearch}
</HStack> />
<Divider style={{ margin: 0, marginTop: 4, borderBlockStartWidth: 0.5 }} /> </HStack>
<Divider style={{ margin: 0, marginTop: 4, borderBlockStartWidth: 0.5 }} />
<TopicsHistory <TopicsHistory
keywords={search} keywords={search}
onClick={onTopicClick as any} onClick={onTopicClick as any}
onSearch={onSearch} onSearch={onSearch}
style={{ display: isShow('topics') }} style={{ display: isShow('topics') }}
/> />
<TopicMessages topic={topic} style={{ display: isShow('topic') }} /> <TopicMessages topic={topic} style={{ display: isShow('topic') }} />
<SearchResults <SearchResults
keywords={isShow('search') ? searchKeywords : ''} keywords={isShow('search') ? searchKeywords : ''}
onMessageClick={onMessageClick} onMessageClick={onMessageClick}
onTopicClick={onTopicClick} onTopicClick={onTopicClick}
style={{ display: isShow('search') }} style={{ display: isShow('search') }}
/> />
<SearchMessage message={message} style={{ display: isShow('message') }} /> <SearchMessage message={message} style={{ display: isShow('message') }} />
</Container> </Container>
</ChatProvider>
) )
} }

View File

@@ -3,7 +3,6 @@ import { MessageEditingProvider } from '@renderer/context/MessageEditingContext'
import { getTopicById } from '@renderer/hooks/useTopic' import { getTopicById } from '@renderer/hooks/useTopic'
import { default as MessageItem } from '@renderer/pages/home/Messages/Message' import { default as MessageItem } from '@renderer/pages/home/Messages/Message'
import { locateToMessage } from '@renderer/services/MessagesService' import { locateToMessage } from '@renderer/services/MessagesService'
import NavigationService from '@renderer/services/NavigationService'
import { Topic } from '@renderer/types' import { Topic } from '@renderer/types'
import type { Message } from '@renderer/types/newMessage' import type { Message } from '@renderer/types/newMessage'
import { runAsyncFunction } from '@renderer/utils' import { runAsyncFunction } from '@renderer/utils'
@@ -18,7 +17,6 @@ interface Props extends React.HTMLAttributes<HTMLDivElement> {
} }
const SearchMessage: FC<Props> = ({ message, ...props }) => { const SearchMessage: FC<Props> = ({ message, ...props }) => {
const navigate = NavigationService.navigate!
const { t } = useTranslation() const { t } = useTranslation()
const [topic, setTopic] = useState<Topic | null>(null) const [topic, setTopic] = useState<Topic | null>(null)
@@ -48,11 +46,11 @@ const SearchMessage: FC<Props> = ({ message, ...props }) => {
type="text" type="text"
size="middle" size="middle"
style={{ color: 'var(--color-text-3)', position: 'absolute', right: 16, top: 16 }} style={{ color: 'var(--color-text-3)', position: 'absolute', right: 16, top: 16 }}
onClick={() => locateToMessage(navigate, message)} onClick={() => locateToMessage(message)}
icon={<Forward size={16} />} icon={<Forward size={16} />}
/> />
<HStack mt="10px" justifyContent="center"> <HStack mt="10px" justifyContent="center">
<Button onClick={() => locateToMessage(navigate, message)} icon={<Forward size={16} />}> <Button onClick={() => locateToMessage(message)} icon={<Forward size={16} />}>
{t('history.locate.message')} {t('history.locate.message')}
</Button> </Button>
</HStack> </HStack>

View File

@@ -2,11 +2,11 @@ import { MessageOutlined } from '@ant-design/icons'
import { HStack } from '@renderer/components/Layout' import { HStack } from '@renderer/components/Layout'
import SearchPopup from '@renderer/components/Popups/SearchPopup' import SearchPopup from '@renderer/components/Popups/SearchPopup'
import { MessageEditingProvider } from '@renderer/context/MessageEditingContext' import { MessageEditingProvider } from '@renderer/context/MessageEditingContext'
import { useChat } from '@renderer/hooks/useChat'
import useScrollPosition from '@renderer/hooks/useScrollPosition' import useScrollPosition from '@renderer/hooks/useScrollPosition'
import { getAssistantById } from '@renderer/services/AssistantService' import { getAssistantById } from '@renderer/services/AssistantService'
import { EVENT_NAMES, EventEmitter } from '@renderer/services/EventService' import { EVENT_NAMES, EventEmitter } from '@renderer/services/EventService'
import { isGenerating, locateToMessage } from '@renderer/services/MessagesService' import { locateToMessage } from '@renderer/services/MessagesService'
import NavigationService from '@renderer/services/NavigationService'
import { useAppDispatch } from '@renderer/store' import { useAppDispatch } from '@renderer/store'
import { loadTopicMessagesThunk } from '@renderer/store/thunk/messageThunk' import { loadTopicMessagesThunk } from '@renderer/store/thunk/messageThunk'
import { Topic } from '@renderer/types' import { Topic } from '@renderer/types'
@@ -23,9 +23,9 @@ interface Props extends React.HTMLAttributes<HTMLDivElement> {
} }
const TopicMessages: FC<Props> = ({ topic, ...props }) => { const TopicMessages: FC<Props> = ({ topic, ...props }) => {
const navigate = NavigationService.navigate!
const { handleScroll, containerRef } = useScrollPosition('TopicMessages') const { handleScroll, containerRef } = useScrollPosition('TopicMessages')
const dispatch = useAppDispatch() const dispatch = useAppDispatch()
const { setActiveAssistant, setActiveTopic } = useChat()
useEffect(() => { useEffect(() => {
topic && dispatch(loadTopicMessagesThunk(topic.id)) topic && dispatch(loadTopicMessagesThunk(topic.id))
@@ -38,11 +38,13 @@ const TopicMessages: FC<Props> = ({ topic, ...props }) => {
} }
const onContinueChat = async (topic: Topic) => { const onContinueChat = async (topic: Topic) => {
await isGenerating()
SearchPopup.hide() SearchPopup.hide()
const assistant = getAssistantById(topic.assistantId) const assistant = getAssistantById(topic.assistantId)
navigate('/', { state: { assistant, topic } }) if (assistant) {
setTimeout(() => EventEmitter.emit(EVENT_NAMES.SHOW_TOPIC_SIDEBAR), 100) setActiveAssistant(assistant)
setActiveTopic(topic)
setTimeout(() => EventEmitter.emit(EVENT_NAMES.SHOW_TOPIC_SIDEBAR), 100)
}
} }
return ( return (
@@ -56,7 +58,7 @@ const TopicMessages: FC<Props> = ({ topic, ...props }) => {
type="text" type="text"
size="middle" size="middle"
style={{ color: 'var(--color-text-3)', position: 'absolute', right: 0, top: 5 }} style={{ color: 'var(--color-text-3)', position: 'absolute', right: 0, top: 5 }}
onClick={() => locateToMessage(navigate, message)} onClick={() => locateToMessage(message)}
icon={<Forward size={16} />} icon={<Forward size={16} />}
/> />
<Divider style={{ margin: '8px auto 15px' }} variant="dashed" /> <Divider style={{ margin: '8px auto 15px' }} variant="dashed" />

View File

@@ -1,8 +1,9 @@
import { SearchOutlined } from '@ant-design/icons' import { SearchOutlined } from '@ant-design/icons'
import { VStack } from '@renderer/components/Layout' import { VStack } from '@renderer/components/Layout'
import { useAssistants } from '@renderer/hooks/useAssistant'
import useScrollPosition from '@renderer/hooks/useScrollPosition' import useScrollPosition from '@renderer/hooks/useScrollPosition'
import { getTopicById } from '@renderer/hooks/useTopic' import { getTopicById } from '@renderer/hooks/useTopic'
import { useAppSelector } from '@renderer/store'
import { selectActiveAssistants } from '@renderer/store/assistants'
import { Topic } from '@renderer/types' import { Topic } from '@renderer/types'
import { Button, Divider, Empty, Segmented } from 'antd' import { Button, Divider, Empty, Segmented } from 'antd'
import dayjs from 'dayjs' import dayjs from 'dayjs'
@@ -20,7 +21,7 @@ type Props = {
} & React.HTMLAttributes<HTMLDivElement> } & React.HTMLAttributes<HTMLDivElement>
const TopicsHistory: React.FC<Props> = ({ keywords, onClick, onSearch, ...props }) => { const TopicsHistory: React.FC<Props> = ({ keywords, onClick, onSearch, ...props }) => {
const { assistants } = useAssistants() const assistants = useAppSelector(selectActiveAssistants)
const { t } = useTranslation() const { t } = useTranslation()
const { handleScroll, containerRef } = useScrollPosition('TopicsHistory') const { handleScroll, containerRef } = useScrollPosition('TopicsHistory')
const [sortType, setSortType] = useState<SortType>('createdAt') const [sortType, setSortType] = useState<SortType>('createdAt')
@@ -35,6 +36,15 @@ const TopicsHistory: React.FC<Props> = ({ keywords, onClick, onSearch, ...props
return dayjs(topic[sortType]).format('MM/DD') return dayjs(topic[sortType]).format('MM/DD')
}) })
// 创建助手映射表
const assistantMap = assistants.reduce(
(map, assistant) => {
map[assistant.id] = assistant
return map
},
{} as Record<string, any>
)
if (isEmpty(filteredTopics)) { if (isEmpty(filteredTopics)) {
return ( return (
<ListContainer {...props}> <ListContainer {...props}>
@@ -65,17 +75,27 @@ const TopicsHistory: React.FC<Props> = ({ keywords, onClick, onSearch, ...props
<ListItem key={date}> <ListItem key={date}>
<Date>{date}</Date> <Date>{date}</Date>
<Divider style={{ margin: '5px 0' }} /> <Divider style={{ margin: '5px 0' }} />
{items.map((topic) => ( {items.map((topic) => {
<TopicItem const assistant = assistantMap[topic.assistantId]
key={topic.id} return (
onClick={async () => { <TopicItem
const _topic = await getTopicById(topic.id) key={topic.id}
onClick(_topic) onClick={async () => {
}}> const _topic = await getTopicById(topic.id)
<TopicName>{topic.name.substring(0, 50)}</TopicName> onClick(_topic)
<TopicDate>{dayjs(topic[sortType]).format('HH:mm')}</TopicDate> }}>
</TopicItem> <TopicContent>
))} <TopicName>{topic.name.substring(0, 50)}</TopicName>
{assistant && (
<AssistantTag>
{assistant.emoji} {assistant.name}
</AssistantTag>
)}
</TopicContent>
<TopicDate>{dayjs(topic[sortType]).format('HH:mm')}</TopicDate>
</TopicItem>
)
})}
</ListItem> </ListItem>
))} ))}
{keywords.length >= 2 && ( {keywords.length >= 2 && (
@@ -127,7 +147,15 @@ const TopicItem = styled.div`
flex-direction: row; flex-direction: row;
align-items: center; align-items: center;
justify-content: space-between; justify-content: space-between;
height: 30px; min-height: 30px;
padding: 4px 0;
`
const TopicContent = styled.div`
display: flex;
flex-direction: column;
gap: 4px;
flex: 1;
` `
const TopicName = styled.div` const TopicName = styled.div`
@@ -135,10 +163,21 @@ const TopicName = styled.div`
color: var(--color-text); color: var(--color-text);
` `
const AssistantTag = styled.div`
font-size: 12px;
color: var(--color-text-3);
background: var(--color-fill-quaternary);
padding: 2px 6px;
border-radius: 4px;
display: inline-block;
width: fit-content;
`
const TopicDate = styled.div` const TopicDate = styled.div`
font-size: 14px; font-size: 14px;
color: var(--color-text-3); color: var(--color-text-3);
margin-left: 10px; margin-left: 10px;
flex-shrink: 0;
` `
export default TopicsHistory export default TopicsHistory

View File

@@ -1,47 +1,29 @@
import { ContentSearch, ContentSearchRef } from '@renderer/components/ContentSearch' import { ContentSearch, ContentSearchRef } from '@renderer/components/ContentSearch'
import MultiSelectActionPopup from '@renderer/components/Popups/MultiSelectionPopup' import MultiSelectActionPopup from '@renderer/components/Popups/MultiSelectionPopup'
import { QuickPanelProvider } from '@renderer/components/QuickPanel' import { QuickPanelProvider } from '@renderer/components/QuickPanel'
import { useAssistant } from '@renderer/hooks/useAssistant' import { useChat } from '@renderer/hooks/useChat'
import { useChatContext } from '@renderer/hooks/useChatContext' import { useChatContext } from '@renderer/hooks/useChatContext'
import { useSettings } from '@renderer/hooks/useSettings' import { useSettings } from '@renderer/hooks/useSettings'
import { useShortcut } from '@renderer/hooks/useShortcuts' import { useShortcut } from '@renderer/hooks/useShortcuts'
import { useShowTopics } from '@renderer/hooks/useStore'
import { Assistant, Topic } from '@renderer/types'
import { classNames } from '@renderer/utils' import { classNames } from '@renderer/utils'
import { Flex } from 'antd' import { Flex } from 'antd'
import { debounce } from 'lodash' import { debounce } from 'lodash'
import React, { FC, useMemo, useState } from 'react' import React, { FC, useState } from 'react'
import { useHotkeys } from 'react-hotkeys-hook' import { useHotkeys } from 'react-hotkeys-hook'
import styled from 'styled-components' import styled from 'styled-components'
import Inputbar from './Inputbar/Inputbar' import Inputbar from './Inputbar/Inputbar'
import Messages from './Messages/Messages' import Messages from './Messages/Messages'
import Tabs from './Tabs'
interface Props { const Chat: FC = () => {
assistant: Assistant const { activeAssistant, activeTopic, setActiveTopic } = useChat()
activeTopic: Topic const { messageStyle } = useSettings()
setActiveTopic: (topic: Topic) => void const { isMultiSelectMode } = useChatContext(activeTopic)
setActiveAssistant: (assistant: Assistant) => void
}
const Chat: FC<Props> = (props) => {
const { assistant } = useAssistant(props.assistant.id)
const { topicPosition, messageStyle, showAssistants } = useSettings()
const { showTopics } = useShowTopics()
const { isMultiSelectMode } = useChatContext(props.activeTopic)
const mainRef = React.useRef<HTMLDivElement>(null) const mainRef = React.useRef<HTMLDivElement>(null)
const contentSearchRef = React.useRef<ContentSearchRef>(null) const contentSearchRef = React.useRef<ContentSearchRef>(null)
const [filterIncludeUser, setFilterIncludeUser] = useState(false) const [filterIncludeUser, setFilterIncludeUser] = useState(false)
const maxWidth = useMemo(() => {
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})`
}, [showAssistants, showTopics, topicPosition])
useHotkeys('esc', () => { useHotkeys('esc', () => {
contentSearchRef.current?.disable() contentSearchRef.current?.disable()
}) })
@@ -100,48 +82,36 @@ const Chat: FC<Props> = (props) => {
} }
return ( return (
<Container id="chat" className={classNames([messageStyle, { 'multi-select-mode': isMultiSelectMode }])}> <Main
<Main ref={mainRef} id="chat-main" vertical flex={1} justify="space-between" style={{ maxWidth }}> ref={mainRef}
<Messages id="chat-main"
key={props.activeTopic.id} className={classNames([messageStyle, { 'multi-select-mode': isMultiSelectMode }])}
assistant={assistant} vertical
topic={props.activeTopic} flex={1}
setActiveTopic={props.setActiveTopic} justify="space-between">
onComponentUpdate={messagesComponentUpdateHandler} <Messages
onFirstUpdate={messagesComponentFirstUpdateHandler} key={activeTopic.id}
/> assistant={activeAssistant}
<ContentSearch topic={activeTopic}
ref={contentSearchRef} setActiveTopic={setActiveTopic}
searchTarget={mainRef as React.RefObject<HTMLElement>} onComponentUpdate={messagesComponentUpdateHandler}
filter={contentSearchFilter} onFirstUpdate={messagesComponentFirstUpdateHandler}
includeUser={filterIncludeUser} />
onIncludeUserChange={userOutlinedItemClickHandler} <ContentSearch
/> ref={contentSearchRef}
<QuickPanelProvider> searchTarget={mainRef as React.RefObject<HTMLElement>}
<Inputbar assistant={assistant} setActiveTopic={props.setActiveTopic} topic={props.activeTopic} /> filter={contentSearchFilter}
{isMultiSelectMode && <MultiSelectActionPopup topic={props.activeTopic} />} includeUser={filterIncludeUser}
</QuickPanelProvider> onIncludeUserChange={userOutlinedItemClickHandler}
</Main> />
{topicPosition === 'right' && showTopics && ( <QuickPanelProvider>
<Tabs <Inputbar />
activeAssistant={assistant} {isMultiSelectMode && <MultiSelectActionPopup topic={activeTopic} />}
activeTopic={props.activeTopic} </QuickPanelProvider>
setActiveAssistant={props.setActiveAssistant} </Main>
setActiveTopic={props.setActiveTopic}
position="right"
/>
)}
</Container>
) )
} }
const Container = styled.div`
display: flex;
flex-direction: row;
height: 100%;
flex: 1;
`
const Main = styled(Flex)` const Main = styled(Flex)`
height: calc(100vh - var(--navbar-height)); height: calc(100vh - var(--navbar-height));
transform: translateZ(0); transform: translateZ(0);

View File

@@ -0,0 +1,91 @@
import { NavbarMain } from '@renderer/components/app/Navbar'
import { HStack } from '@renderer/components/Layout'
import SearchPopup from '@renderer/components/Popups/SearchPopup'
import { isMac } from '@renderer/config/constant'
import { useAssistant } from '@renderer/hooks/useAssistant'
import { useChat } from '@renderer/hooks/useChat'
import { useShortcut } from '@renderer/hooks/useShortcuts'
import { useShowAssistants } from '@renderer/hooks/useStore'
import { Tooltip } from 'antd'
import { t } from 'i18next'
import { PanelLeft, PanelRight, Search } from 'lucide-react'
import { FC } from 'react'
import styled from 'styled-components'
import SelectModelButton from './components/SelectModelButton'
import UpdateAppButton from './components/UpdateAppButton'
const ChatNavbar: FC = () => {
const { activeAssistant } = useChat()
const { assistant } = useAssistant(activeAssistant.id)
const { showAssistants, toggleShowAssistants } = useShowAssistants()
useShortcut('search_message', SearchPopup.show)
return (
<NavbarMain className="home-navbar" style={{ minHeight: 50 }}>
<HStack alignItems="center" gap={8}>
<NavbarIcon onClick={() => toggleShowAssistants()}>
{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>
)}
</HStack>
</NavbarMain>
)
}
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);
}
&.active {
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,58 +1,15 @@
import { useAssistants } from '@renderer/hooks/useAssistant'
import { useSettings } from '@renderer/hooks/useSettings' import { useSettings } from '@renderer/hooks/useSettings'
import { useActiveTopic } from '@renderer/hooks/useTopic' import { FC, useEffect } from 'react'
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 { useLocation, useNavigate } from 'react-router-dom'
import styled from 'styled-components' import styled from 'styled-components'
import Chat from './Chat' import Chat from './Chat'
import Navbar from './Navbar' import ChatNavbar from './ChatNavbar'
import HomeTabs from './Tabs'
let _activeAssistant: Assistant const HomePage: FC<{ style?: React.CSSProperties }> = ({ style }) => {
const HomePage: FC = () => {
const { assistants } = useAssistants()
const navigate = useNavigate()
const location = useLocation()
const state = location.state
const [activeAssistant, setActiveAssistant] = useState(state?.assistant || _activeAssistant || assistants[0])
const { activeTopic, setActiveTopic } = useActiveTopic(activeAssistant, state?.topic)
const { showAssistants, showTopics, topicPosition } = useSettings() const { showAssistants, showTopics, topicPosition } = useSettings()
_activeAssistant = activeAssistant
useEffect(() => { useEffect(() => {
NavigationService.setNavigate(navigate) window.api.window.setMinimumSize(showAssistants ? 1080 : 520, 600)
}, [navigate])
useEffect(() => {
state?.assistant && setActiveAssistant(state?.assistant)
state?.topic && setActiveTopic(state?.topic)
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [state])
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 = topicPosition == 'left' ? !showAssistants : !showAssistants && !showTopics
window.api.window.setMinimumSize(canMinimize ? 520 : 1080, 600)
return () => { return () => {
window.api.window.resetMinimumSize() window.api.window.resetMinimumSize()
@@ -60,47 +17,22 @@ const HomePage: FC = () => {
}, [showAssistants, showTopics, topicPosition]) }, [showAssistants, showTopics, topicPosition])
return ( return (
<Container id="home-page"> <Container style={style}>
<Navbar <ChatNavbar />
activeAssistant={activeAssistant}
activeTopic={activeTopic}
setActiveTopic={setActiveTopic}
setActiveAssistant={setActiveAssistant}
position="left"
/>
<ContentContainer id="content-container"> <ContentContainer id="content-container">
{showAssistants && ( <Chat />
<HomeTabs
activeAssistant={activeAssistant}
activeTopic={activeTopic}
setActiveAssistant={setActiveAssistant}
setActiveTopic={setActiveTopic}
position="left"
/>
)}
<Chat
assistant={activeAssistant}
activeTopic={activeTopic}
setActiveTopic={setActiveTopic}
setActiveAssistant={setActiveAssistant}
/>
</ContentContainer> </ContentContainer>
</Container> </Container>
) )
} }
const Container = styled.div` const Container = styled.div`
min-width: 0;
display: flex; display: flex;
flex: 1; flex: 1;
flex-direction: column; flex-direction: column;
max-width: calc(100vw - var(--sidebar-width));
` `
const ContentContainer = styled.div` const ContentContainer = styled.div``
display: flex;
flex: 1;
flex-direction: row;
overflow: hidden;
`
export default HomePage export default HomePage

View File

@@ -24,7 +24,7 @@ const GenerateImageButton: FC<Props> = ({ model, ToolbarButton, assistant, onEna
mouseLeaveDelay={0} mouseLeaveDelay={0}
arrow> arrow>
<ToolbarButton type="text" disabled={!isGenerateImageModel(model)} onClick={onEnableGenerateImage}> <ToolbarButton type="text" disabled={!isGenerateImageModel(model)} onClick={onEnableGenerateImage}>
<Image size={18} color={assistant.enableGenerateImage ? 'var(--color-link)' : 'var(--color-icon)'} /> <Image size={18} color={assistant.enableGenerateImage ? 'var(--color-primary)' : 'var(--color-icon)'} />
</ToolbarButton> </ToolbarButton>
</Tooltip> </Tooltip>
) )

View File

@@ -14,9 +14,10 @@ import {
} from '@renderer/config/models' } from '@renderer/config/models'
import db from '@renderer/databases' import db from '@renderer/databases'
import { useAssistant } from '@renderer/hooks/useAssistant' import { useAssistant } from '@renderer/hooks/useAssistant'
import { useChat } from '@renderer/hooks/useChat'
import { useKnowledgeBases } from '@renderer/hooks/useKnowledge' import { useKnowledgeBases } from '@renderer/hooks/useKnowledge'
import { useMessageOperations, useTopicLoading } from '@renderer/hooks/useMessageOperations' import { useMessageOperations, useTopicLoading } from '@renderer/hooks/useMessageOperations'
import { modelGenerating, useRuntime } from '@renderer/hooks/useRuntime' import { useRuntime } from '@renderer/hooks/useRuntime'
import { useSettings } from '@renderer/hooks/useSettings' import { useSettings } from '@renderer/hooks/useSettings'
import { useShortcut, useShortcutDisplay } from '@renderer/hooks/useShortcuts' import { useShortcut, useShortcutDisplay } from '@renderer/hooks/useShortcuts'
import { useSidebarIconShow } from '@renderer/hooks/useSidebarIcon' import { useSidebarIconShow } from '@renderer/hooks/useSidebarIcon'
@@ -32,7 +33,7 @@ import WebSearchService from '@renderer/services/WebSearchService'
import { useAppDispatch, useAppSelector } from '@renderer/store' import { useAppDispatch, useAppSelector } from '@renderer/store'
import { setSearching } from '@renderer/store/runtime' import { setSearching } from '@renderer/store/runtime'
import { sendMessage as _sendMessage } from '@renderer/store/thunk/messageThunk' import { sendMessage as _sendMessage } from '@renderer/store/thunk/messageThunk'
import { Assistant, FileType, FileTypes, KnowledgeBase, KnowledgeItem, Model, Topic } from '@renderer/types' import { FileType, FileTypes, KnowledgeBase, KnowledgeItem, Model } from '@renderer/types'
import type { MessageInputBaseParams } from '@renderer/types/newMessage' import type { MessageInputBaseParams } from '@renderer/types/newMessage'
import { classNames, delay, formatFileSize, getFileExtension } from '@renderer/utils' import { classNames, delay, formatFileSize, getFileExtension } from '@renderer/utils'
import { formatQuotedText } from '@renderer/utils/formats' import { formatQuotedText } from '@renderer/utils/formats'
@@ -55,21 +56,17 @@ import InputbarTools, { InputbarToolsRef } from './InputbarTools'
import KnowledgeBaseInput from './KnowledgeBaseInput' import KnowledgeBaseInput from './KnowledgeBaseInput'
import MentionModelsInput from './MentionModelsInput' import MentionModelsInput from './MentionModelsInput'
import SendMessageButton from './SendMessageButton' import SendMessageButton from './SendMessageButton'
import SettingButton from './SettingButton'
import TokenCount from './TokenCount' import TokenCount from './TokenCount'
interface Props {
assistant: Assistant
setActiveTopic: (topic: Topic) => void
topic: Topic
}
let _text = '' let _text = ''
let _files: FileType[] = [] let _files: FileType[] = []
const Inputbar: FC<Props> = ({ assistant: _assistant, setActiveTopic, topic }) => { const Inputbar: FC = () => {
const { activeAssistant, activeTopic: topic, setActiveTopic } = useChat()
const [text, setText] = useState(_text) const [text, setText] = useState(_text)
const [inputFocus, setInputFocus] = useState(false) const [inputFocus, setInputFocus] = useState(false)
const { assistant, addTopic, model, setModel, updateAssistant } = useAssistant(_assistant.id) const { assistant, addTopic, model, setModel, updateAssistant } = useAssistant(activeAssistant.id)
const { const {
targetLanguage, targetLanguage,
sendMessageShortcut, sendMessageShortcut,
@@ -432,8 +429,6 @@ const Inputbar: FC<Props> = ({ assistant: _assistant, setActiveTopic, topic }) =
} }
const addNewTopic = useCallback(async () => { const addNewTopic = useCallback(async () => {
await modelGenerating()
const topic = getDefaultTopic(assistant.id) const topic = getDefaultTopic(assistant.id)
await db.topics.add({ id: topic.id, messages: [] }) await db.topics.add({ id: topic.id, messages: [] })
@@ -662,11 +657,6 @@ const Inputbar: FC<Props> = ({ assistant: _assistant, setActiveTopic, topic }) =
useEffect(() => { useEffect(() => {
const _setEstimateTokenCount = debounce(setEstimateTokenCount, 100, { leading: false, trailing: true }) const _setEstimateTokenCount = debounce(setEstimateTokenCount, 100, { leading: false, trailing: true })
const unsubscribes = [ const unsubscribes = [
// EventEmitter.on(EVENT_NAMES.EDIT_MESSAGE, (message: Message) => {
// setText(message.content)
// textareaRef.current?.focus()
// setTimeout(() => resizeTextArea(), 0)
// }),
EventEmitter.on(EVENT_NAMES.ESTIMATED_TOKEN_COUNT, ({ tokensCount, contextCount }) => { EventEmitter.on(EVENT_NAMES.ESTIMATED_TOKEN_COUNT, ({ tokensCount, contextCount }) => {
_setEstimateTokenCount(tokensCount) _setEstimateTokenCount(tokensCount)
setContextCount({ current: contextCount.current, max: contextCount.max }) // 现在contextCount是一个对象而不是单个数值 setContextCount({ current: contextCount.current, max: contextCount.max }) // 现在contextCount是一个对象而不是单个数值
@@ -791,12 +781,7 @@ const Inputbar: FC<Props> = ({ assistant: _assistant, setActiveTopic, topic }) =
} else { } else {
textArea.style.height = 'auto' textArea.style.height = 'auto'
setTextareaHeight(undefined) setTextareaHeight(undefined)
requestAnimationFrame(() => { setTimeout(() => resizeTextArea(true), 0)
if (textArea) {
const contentHeight = textArea.scrollHeight
textArea.style.height = contentHeight > 400 ? '400px' : `${contentHeight}px`
}
})
} }
textareaRef.current?.focus() textareaRef.current?.focus()
@@ -907,11 +892,12 @@ const Inputbar: FC<Props> = ({ assistant: _assistant, setActiveTopic, topic }) =
ToolbarButton={ToolbarButton} ToolbarButton={ToolbarButton}
onClick={onNewContext} onClick={onNewContext}
/> />
<SettingButton assistant={assistant} ToolbarButton={ToolbarButton} />
<TranslateButton text={text} onTranslated={onTranslated} isLoading={isTranslating} /> <TranslateButton text={text} onTranslated={onTranslated} isLoading={isTranslating} />
{loading && ( {loading && (
<Tooltip placement="top" title={t('chat.input.pause')} mouseLeaveDelay={0} arrow> <Tooltip placement="top" title={t('chat.input.pause')} mouseEnterDelay={0} arrow>
<ToolbarButton type="text" onClick={onPause} style={{ marginRight: -2, marginTop: 1 }}> <ToolbarButton type="text" onClick={onPause} style={{ width: 30, height: 30, marginRight: 2 }}>
<CirclePause style={{ color: 'var(--color-error)', fontSize: 20 }} /> <CirclePause style={{ color: 'var(--color-error)' }} size={28} />
</ToolbarButton> </ToolbarButton>
</Tooltip> </Tooltip>
)} )}
@@ -959,9 +945,10 @@ const Container = styled.div`
` `
const InputBarContainer = styled.div` const InputBarContainer = styled.div`
border: 0.5px solid var(--color-border); border: 1px solid var(--color-border);
transition: all 0.2s ease; transition: all 0.2s ease;
position: relative; position: relative;
margin: 16px 20px;
border-radius: 20px; border-radius: 20px;
padding-top: 8px; // 为拖动手柄留出空间 padding-top: 8px; // 为拖动手柄留出空间
background-color: var(--color-background-opacity); background-color: var(--color-background-opacity);
@@ -1001,6 +988,9 @@ const Textarea = styled(TextArea)`
&.ant-input { &.ant-input {
line-height: 1.4; line-height: 1.4;
} }
.ant-input-textarea-show-count::after {
transition: none !important;
}
&::-webkit-scrollbar { &::-webkit-scrollbar {
width: 3px; width: 3px;
} }

View File

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

View File

@@ -13,7 +13,7 @@ const SendMessageButton: FC<Props> = ({ disabled, sendMessage }) => {
style={{ style={{
cursor: disabled ? 'not-allowed' : 'pointer', cursor: disabled ? 'not-allowed' : 'pointer',
color: disabled ? 'var(--color-text-3)' : 'var(--color-primary)', color: disabled ? 'var(--color-text-3)' : 'var(--color-primary)',
fontSize: 22, fontSize: 30,
transition: 'all 0.2s', transition: 'all 0.2s',
marginRight: 2 marginRight: 2
}} }}

View File

@@ -0,0 +1,32 @@
import { Assistant } from '@renderer/types'
import { Popover } from 'antd'
import { SlidersHorizontal } from 'lucide-react'
import { FC } from 'react'
import SettingsTab from '../Tabs/SettingsTab'
interface Props {
assistant: Assistant
ToolbarButton: any
}
const SettingButton: FC<Props> = ({ ToolbarButton }) => {
return (
<Popover
arrow={false}
placement="topLeft"
content={<SettingsTab />}
trigger="click"
styles={{
body: {
padding: '4px 2px 4px 2px'
}
}}>
<ToolbarButton type="text">
<SlidersHorizontal size={16} />
</ToolbarButton>
</Popover>
)
}
export default SettingButton

View File

@@ -107,7 +107,7 @@ const ThinkingButton: FC<Props> = ({ ref, model, assistant, ToolbarButton }): Re
}, [currentReasoningEffort, supportedOptions, updateAssistantSettings, model.id]) }, [currentReasoningEffort, supportedOptions, updateAssistantSettings, model.id])
const createThinkingIcon = useCallback((option?: ThinkingOption, isActive: boolean = false) => { const createThinkingIcon = useCallback((option?: ThinkingOption, isActive: boolean = false) => {
const iconColor = isActive ? 'var(--color-link)' : 'var(--color-icon)' const iconColor = isActive ? 'var(--color-primary)' : 'var(--color-icon)'
switch (true) { switch (true) {
case option === 'low': case option === 'low':

View File

@@ -136,7 +136,7 @@ const WebSearchButton: FC<Props> = ({ ref, assistant, ToolbarButton }) => {
<Globe <Globe
size={18} size={18}
style={{ style={{
color: enableWebSearch ? 'var(--color-link)' : 'var(--color-icon)' color: enableWebSearch ? 'var(--color-primary)' : 'var(--color-icon)'
}} }}
/> />
</ToolbarButton> </ToolbarButton>

View File

@@ -0,0 +1,64 @@
import { PanelLeftIcon } from '@renderer/components/Icons/PanelIcons'
import { isMac } from '@renderer/config/constant'
import { useShowAssistants } from '@renderer/hooks/useStore'
import { EVENT_NAMES, EventEmitter } from '@renderer/services/EventService'
import { Tooltip } from 'antd'
import { t } from 'i18next'
import { MessageSquareDiff } from 'lucide-react'
import { FC } from 'react'
import styled from 'styled-components'
interface Props {}
const HeaderNavbar: FC<Props> = () => {
const { showAssistants, toggleShowAssistants } = useShowAssistants()
return (
<Container>
{showAssistants && (
<NavbarIcon onClick={() => toggleShowAssistants()}>
<PanelLeftIcon size={18} expanded={true} />
</NavbarIcon>
)}
<Tooltip title={t('settings.shortcuts.new_topic')} mouseEnterDelay={0.8}>
<NavbarIcon onClick={() => EventEmitter.emit(EVENT_NAMES.ADD_NEW_TOPIC)}>
<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;
height: var(--navbar-height);
min-height: var(--navbar-height);
background-color: transparent;
-webkit-app-region: drag;
padding: 0 15px;
padding-left: ${isMac ? '75px' : '15px'};
`
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;
&:hover {
background-color: var(--color-list-item);
color: var(--color-icon-white);
}
`
export default HeaderNavbar

View File

@@ -0,0 +1,405 @@
import EmojiAvatar from '@renderer/components/Avatar/EmojiAvatar'
import UserPopup from '@renderer/components/Popups/UserPopup'
import { AppLogo, UserAvatar } from '@renderer/config/env'
import { useTheme } from '@renderer/context/ThemeProvider'
import useAvatar from '@renderer/hooks/useAvatar'
import { useChat } from '@renderer/hooks/useChat'
import { useMinappPopup } from '@renderer/hooks/useMinappPopup'
import { useSettings } from '@renderer/hooks/useSettings'
import { useShortcut } from '@renderer/hooks/useShortcuts'
import { useShowAssistants } from '@renderer/hooks/useStore'
import i18n from '@renderer/i18n'
import { getAssistantById } from '@renderer/services/AssistantService'
import { EVENT_NAMES, EventEmitter } from '@renderer/services/EventService'
import { Assistant, ThemeMode } from '@renderer/types'
import { isEmoji } from '@renderer/utils'
import { Avatar, Dropdown } from 'antd'
import {
Blocks,
ChevronDown,
ChevronRight,
CircleHelp,
EllipsisVertical,
FileSearch,
Folder,
Languages,
LayoutGrid,
Moon,
Palette,
Settings,
Sparkle,
SquareTerminal,
Sun,
SunMoon
} from 'lucide-react'
import { FC, useDeferredValue, useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { useLocation, useNavigate } from 'react-router-dom'
import styled from 'styled-components'
import AssistantsTab from '../Tabs/AssistantsTab'
import AssistantItem from '../Tabs/components/AssistantItem'
import TopicsTab from '../Tabs/TopicsTab'
import {
Container,
MainMenu,
MainMenuItem,
MainMenuItemIcon,
MainMenuItemLeft,
MainMenuItemRight,
MainMenuItemText,
SubMenu
} from './MainSidebarStyles'
import OpenedMinappTabs from './OpenedMinapps'
import SidebarSearch from './SidebarSearch'
type Tab = 'assistants' | 'topic'
const MainSidebar: FC = () => {
const navigate = useNavigate()
const [tab, setTab] = useState<Tab>('assistants')
const avatar = useAvatar()
const { userName, defaultPaintingProvider, transparentWindow } = useSettings()
const { t } = useTranslation()
const { theme, settedTheme, toggleTheme } = useTheme()
const [isAppMenuExpanded, setIsAppMenuExpanded] = useState(false)
const { showAssistants, toggleShowAssistants } = useShowAssistants()
const location = useLocation()
const { pathname } = location
const { activeAssistant, activeTopic, setActiveAssistant } = useChat()
const { showTopics, clickAssistantToShowTopic } = useSettings()
const { openMinapp } = useMinappPopup()
const [_searchValue, setSearchValue] = useState('')
const searchValue = useDeferredValue(_searchValue)
useShortcut('toggle_show_assistants', toggleShowAssistants)
useShortcut('toggle_show_topics', () => EventEmitter.emit(EVENT_NAMES.SWITCH_TOPIC_SIDEBAR))
useEffect(() => {
const unsubscribe = [
EventEmitter.on(EVENT_NAMES.SHOW_TOPIC_SIDEBAR, (assistant: Assistant) => {
if (clickAssistantToShowTopic) {
setTab('topic')
} else {
if (activeAssistant.id === assistant.id) {
setTab('topic')
}
}
}),
EventEmitter.on(EVENT_NAMES.SWITCH_TOPIC_SIDEBAR, () => {
setTab(tab === 'topic' ? 'assistants' : 'topic')
!showAssistants && toggleShowAssistants()
})
]
return () => unsubscribe.forEach((unsubscribe) => unsubscribe())
}, [
activeAssistant?.id,
activeTopic?.assistantId,
clickAssistantToShowTopic,
isAppMenuExpanded,
showAssistants,
tab,
toggleShowAssistants
])
useEffect(() => {
const unsubscribes = [
EventEmitter.on(EVENT_NAMES.SWITCH_ASSISTANT, (assistantId: string) => {
const newAssistant = getAssistantById(assistantId)
if (newAssistant) {
setActiveAssistant(newAssistant)
}
}),
EventEmitter.on(EVENT_NAMES.SWITCH_TOPIC_SIDEBAR, () => setTab(tab === 'topic' ? 'assistants' : 'topic')),
EventEmitter.on(EVENT_NAMES.OPEN_MINAPP, () => {
setTimeout(() => setIsAppMenuExpanded(false), 1000)
})
]
return () => unsubscribes.forEach((unsubscribe) => unsubscribe())
}, [setActiveAssistant, tab])
useEffect(() => {
const canMinimize = !showAssistants && !showTopics
window.api.window.setMinimumSize(canMinimize ? 520 : 1080, 600)
return () => {
window.api.window.resetMinimumSize()
}
}, [showAssistants, showTopics])
useEffect(() => {
setIsAppMenuExpanded(false)
}, [activeAssistant.id, activeTopic.id])
const appMenuItems = [
{ icon: <Sparkle size={18} className="icon" />, text: t('agents.title'), path: '/agents' },
{ 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('settings.mcp.title'), path: '/mcp-servers' },
{ icon: <Folder size={18} className="icon" />, text: t('files.title'), path: '/files' }
]
const isRoutes = (path: string): boolean => pathname.startsWith(path)
const docsId = 'cherrystudio-docs'
const onOpenDocs = () => {
const isChinese = i18n.language.startsWith('zh')
openMinapp({
id: docsId,
name: t('docs.title'),
url: isChinese ? 'https://docs.cherry-ai.com/' : 'https://docs.cherry-ai.com/cherry-studio-wen-dang/en-us',
logo: AppLogo
})
}
if (!showAssistants) {
return null
}
return (
<Container
id="main-sidebar"
transparent={transparentWindow}
style={{
width: showAssistants ? 'var(--assistants-width)' : '0px',
opacity: showAssistants ? 1 : 0,
overflow: showAssistants ? 'initial' : 'hidden'
}}>
<MainMenu>
<SidebarSearch onSearch={setSearchValue} />
<MainMenuItem active={isAppMenuExpanded} onClick={() => setIsAppMenuExpanded(!isAppMenuExpanded)}>
<MainMenuItemLeft>
<MainMenuItemIcon>
<Blocks size={19} className="icon" />
</MainMenuItemIcon>
<MainMenuItemText>{isAppMenuExpanded ? t('common.collapse') : 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>
)}
<OpenedMinappTabs />
</MainMenu>
{tab === 'topic' && (
<AssistantContainer onClick={() => setIsAppMenuExpanded(false)}>
<AssistantItem
key={activeAssistant.id}
assistant={activeAssistant}
isActive={false}
sortBy="list"
onSwitch={() => {}}
onDelete={() => {}}
addAssistant={() => {}}
onCreateDefaultAssistant={() => {}}
handleSortByChange={() => {}}
singleLine
/>
</AssistantContainer>
)}
<MainContainer>
{tab === 'assistants' && <AssistantsTab searchValue={searchValue} />}
{tab === 'topic' && <TopicsTab searchValue={searchValue} style={{ paddingTop: 4 }} />}
</MainContainer>
<UserMenu>
<UserMenuLeft onClick={() => UserPopup.show()}>
{isEmoji(avatar) ? (
<EmojiAvatar className="sidebar-avatar" size={31} fontSize={18}>
{avatar}
</EmojiAvatar>
) : (
<AvatarImg src={avatar || UserAvatar} draggable={false} className="nodrag" />
)}
{userName && <UserMenuText>{userName}</UserMenuText>}
</UserMenuLeft>
<Dropdown
placement="topRight"
trigger={['click']}
menu={{
items: [
{
key: 'theme',
label: (
<span
onClick={(e) => {
e.stopPropagation()
toggleTheme()
}}>
{t('settings.theme.title')}: {t(`settings.theme.${settedTheme}`)}
</span>
),
icon: ThemeIcon()
},
{
key: 'about',
label: t('docs.title'),
icon: <CircleHelp size={16} className="icon" />,
onClick: onOpenDocs
},
{
key: 'settings',
label: t('settings.title'),
icon: <Settings size={16} className="icon" />,
onClick: () => window.api.showSettingsWindow({ defaultTab: 'provider' })
}
]
}}>
<Icon theme={theme} className="settings-icon">
<EllipsisVertical size={16} />
</Icon>
</Dropdown>
</UserMenu>
</Container>
)
}
const ThemeIcon = () => {
const { settedTheme } = useTheme()
return settedTheme === ThemeMode.dark ? (
<Moon size={16} className="icon" />
) : settedTheme === ThemeMode.light ? (
<Sun size={16} className="icon" />
) : (
<SunMoon size={16} className="icon" />
)
}
const MainContainer = styled.div`
display: flex;
flex: 1;
flex-direction: column;
overflow: hidden;
height: 0;
min-height: 0;
`
const AssistantContainer = styled.div`
margin: 4px 10px;
display: flex;
margin-top: 0;
`
const UserMenu = styled.div`
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
margin: 10px;
margin-bottom: 10px;
gap: 5px;
border-radius: 8px;
`
const UserMenuLeft = styled.div`
display: flex;
flex-direction: row;
align-items: center;
gap: 8px;
padding: 4px 8px;
border-radius: 8px;
&:hover {
background-color: var(--color-list-item);
}
`
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;
margin-right: 3px;
`
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;
&.settings-icon {
width: 34px;
height: 34px;
}
&:hover {
background-color: var(--color-list-item);
opacity: 0.8;
cursor: pointer;
.icon {
color: var(--color-icon-white);
}
}
&.active {
background-color: var(--color-list-item);
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);
}
`
export default MainSidebar

View File

@@ -0,0 +1,99 @@
import Scrollbar from '@renderer/components/Scrollbar'
import styled from 'styled-components'
export 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;
opacity: ${({ active }) => (active ? 0.6 : 1)};
&.active {
background-color: var(--color-list-item);
}
&:hover {
background-color: ${({ active }) => (active ? 'var(--color-list-item)' : 'var(--color-list-item-hover)')};
}
`
export const MainMenuItemLeft = styled.div`
display: flex;
align-items: center;
gap: 10px;
`
export const MainMenuItemRight = styled.div`
display: flex;
align-items: center;
gap: 5px;
margin-right: -3px;
`
export const MainMenuItemIcon = styled.div`
display: flex;
align-items: center;
justify-content: center;
width: 20px;
height: 20px;
`
export const MainMenuItemText = styled.div`
font-size: 14px;
font-weight: 500;
`
export const Container = styled.div<{ transparent?: boolean }>`
display: flex;
flex-direction: column;
flex: 1;
width: var(--assistants-width);
max-width: var(--assistants-width);
border-right: 0.5px solid var(--color-border);
height: calc(var(--main-height) - 50px);
min-height: calc(var(--main-height) - 50px);
background: var(--color-background);
padding-top: 10px;
margin-top: 50px;
`
export const MainMenu = styled.div`
display: flex;
flex-direction: column;
padding: 0 10px;
`
export const SubMenu = styled.div`
display: flex;
flex-direction: column;
gap: 5px;
overflow: hidden;
padding: 5px 0;
`
export const TabsContainer = styled.div`
display: flex;
flex-direction: column;
align-items: center;
-webkit-app-region: none;
position: relative;
width: 100%;
margin-top: 5px;
&::-webkit-scrollbar {
display: none;
}
`
export const TabsWrapper = styled(Scrollbar as any)`
width: 100%;
max-height: 50vh;
`
export const Menus = styled.div`
display: flex;
flex-direction: column;
gap: 5px;
`

View File

@@ -0,0 +1,167 @@
import { DraggableList } from '@renderer/components/DraggableList'
import MinAppIcon from '@renderer/components/Icons/MinAppIcon'
import IndicatorLight from '@renderer/components/IndicatorLight'
import { Center } from '@renderer/components/Layout'
import { useMinappPopup } from '@renderer/hooks/useMinappPopup'
import { useMinapps } from '@renderer/hooks/useMinapps'
import { useRuntime } from '@renderer/hooks/useRuntime'
import { useSettings } from '@renderer/hooks/useSettings'
import type { MenuProps } from 'antd'
import { Empty } from 'antd'
import { Dropdown } from 'antd'
import { isEmpty } from 'lodash'
import { FC, useEffect, useMemo } from 'react'
import { useTranslation } from 'react-i18next'
import styled from 'styled-components'
import {
MainMenuItem,
MainMenuItemIcon,
MainMenuItemLeft,
MainMenuItemRight,
MainMenuItemText,
TabsContainer,
TabsWrapper
} from './MainSidebarStyles'
const OpenedMinapps: FC = () => {
const { minappShow, openedKeepAliveMinapps, currentMinappId } = useRuntime()
const { openMinappKeepAlive, hideMinappPopup, closeMinapp, closeAllMinapps } = useMinappPopup()
const { showOpenedMinappsInSidebar } = useSettings()
const { pinned, updatePinnedMinapps } = useMinapps()
const { t } = useTranslation()
// 合并并排序应用列表
const sortedApps = useMemo(() => {
// 分离已打开但未固定的应用
const openedNotPinned = openedKeepAliveMinapps.filter((app) => !pinned.find((p) => p.id === app.id))
// 获取固定应用列表(保持原有顺序)
const pinnedApps = pinned.map((app) => {
const openedApp = openedKeepAliveMinapps.find((o) => o.id === app.id)
return openedApp || app
})
// 把已启动但未固定的放到列表下面
return [...pinnedApps, ...openedNotPinned]
}, [openedKeepAliveMinapps, pinned])
const handleOnClick = (app) => {
if (minappShow && currentMinappId === app.id) {
hideMinappPopup()
} else {
openMinappKeepAlive(app)
}
}
useEffect(() => {
const iconDefaultHeight = 40
const iconDefaultOffset = 17
const container = document.querySelector('.TabsContainer') as HTMLElement
const activeIcon = document.querySelector('.TabsContainer .opened-active') as HTMLElement
let indicatorTop = 0,
indicatorRight = 0
if (minappShow && activeIcon && container) {
indicatorTop = activeIcon.offsetTop + activeIcon.offsetHeight / 2 - 4
indicatorRight = 0
} else {
indicatorTop =
((openedKeepAliveMinapps.length > 0 ? openedKeepAliveMinapps.length : 1) / 2) * iconDefaultHeight +
iconDefaultOffset -
4
indicatorRight = -50
}
container.style.setProperty('--indicator-top', `${indicatorTop}px`)
container.style.setProperty('--indicator-right', `${indicatorRight}px`)
}, [currentMinappId, openedKeepAliveMinapps, minappShow])
const isShowApps = showOpenedMinappsInSidebar && sortedApps.length > 0
if (!isShowApps) return <TabsContainer className="TabsContainer" />
return (
<TabsContainer className="TabsContainer" style={{ marginBottom: 4 }}>
<Divider />
<TabsWrapper>
<DraggableList
list={sortedApps}
onUpdate={(newList) => {
// 只更新固定应用的顺序
const newPinned = newList.filter((app) => pinned.find((p) => p.id === app.id))
updatePinnedMinapps(newPinned)
}}
listStyle={{ margin: '4px 0' }}>
{(app) => {
const isPinned = pinned.find((p) => p.id === app.id)
const isOpened = openedKeepAliveMinapps.find((o) => o.id === app.id)
const menuItems: MenuProps['items'] = [
{
key: 'togglePin',
label: isPinned ? t('minapp.sidebar.remove.title') : t('minapp.sidebar.pin.title'),
onClick: () => {
if (isPinned) {
const newPinned = pinned.filter((item) => item.id !== app.id)
updatePinnedMinapps(newPinned)
} else {
updatePinnedMinapps([...pinned, app])
}
}
}
]
if (isOpened) {
menuItems.push(
{
key: 'closeApp',
label: t('minapp.sidebar.close.title'),
onClick: () => closeMinapp(app.id)
},
{
key: 'closeAllApp',
label: t('minapp.sidebar.closeall.title'),
onClick: () => closeAllMinapps()
}
)
}
return (
<Dropdown menu={{ items: menuItems }} trigger={['contextMenu']} overlayStyle={{ zIndex: 10000 }}>
<MainMenuItem key={app.id} onClick={() => handleOnClick(app)}>
<MainMenuItemLeft>
<MainMenuItemIcon>
<MinAppIcon size={22} app={app} style={{ borderRadius: 6 }} sidebar />
</MainMenuItemIcon>
<MainMenuItemText>{app.name}</MainMenuItemText>
</MainMenuItemLeft>
{isOpened && (
<MainMenuItemRight style={{ marginRight: 4 }}>
<IndicatorLight color="var(--color-primary)" shadow={false} animation={false} size={5} />
</MainMenuItemRight>
)}
</MainMenuItem>
</Dropdown>
)
}}
</DraggableList>
{isEmpty(sortedApps) && (
<Center>
<Empty image={Empty.PRESENTED_IMAGE_SIMPLE} />
</Center>
)}
</TabsWrapper>
<Divider />
</TabsContainer>
)
}
const Divider = styled.div`
width: 100%;
height: 1px;
background-color: var(--color-border);
margin: 5px 0;
opacity: 0.5;
`
export default OpenedMinapps

View File

@@ -0,0 +1,106 @@
import { Input, InputRef } from 'antd'
import { Search } from 'lucide-react'
import React, { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next'
import styled from 'styled-components'
import { MainMenuItem, MainMenuItemIcon, MainMenuItemLeft, MainMenuItemText } from './MainSidebarStyles'
interface SidebarSearchProps {
onSearch: (text: string) => void
}
const SidebarSearch: React.FC<SidebarSearchProps> = ({ onSearch }) => {
const { t } = useTranslation()
const [isExpanded, setIsExpanded] = useState(false)
const [searchText, setSearchText] = useState('')
const inputRef = useRef<InputRef>(null)
const handleTextChange = useCallback(
(text: string) => {
setSearchText(text)
onSearch(text)
},
[onSearch]
)
const handleExpand = useCallback(() => {
setIsExpanded(true)
}, [])
const handleClear = useCallback(() => {
setSearchText('')
onSearch('')
}, [onSearch])
const handleCollapse = useCallback(() => {
setSearchText('')
setIsExpanded(false)
onSearch('')
}, [onSearch])
const handleInputKeyDown = useCallback(
(e: React.KeyboardEvent) => {
if (e.key === 'Escape') {
handleCollapse()
}
},
[handleCollapse]
)
useEffect(() => {
if (isExpanded && inputRef.current) {
inputRef.current.focus()
}
}, [isExpanded])
const renderInputBox = useMemo(() => {
return (
<Input
ref={inputRef}
value={searchText}
placeholder={t('chat.assistant.search.placeholder')}
onChange={(e) => handleTextChange(e.target.value)}
onKeyDown={handleInputKeyDown}
onBlur={(e) => {
// 如果输入框失焦且没有搜索内容,则收起
if (!e.target.value.trim()) {
handleCollapse()
}
}}
onClear={handleClear}
allowClear
style={{
paddingTop: 4
}}
prefix={
<MainMenuItemIcon style={{ margin: '0 6px 0 -2px' }}>
<Search size={18} className="icon" />
</MainMenuItemIcon>
}
spellCheck={false}
/>
)
}, [handleClear, handleCollapse, handleInputKeyDown, handleTextChange, searchText, t])
const renderMenuItem = useMemo(() => {
return (
<MainMenuItem onClick={handleExpand} style={{ cursor: 'pointer' }}>
<MainMenuItemLeft>
<MainMenuItemIcon>
<Search size={18} className="icon" />
</MainMenuItemIcon>
<MainMenuItemText>{t('chat.assistant.search.placeholder')}</MainMenuItemText>
</MainMenuItemLeft>
</MainMenuItem>
)
}, [handleExpand, t])
return <SearchBarWrapper>{isExpanded ? renderInputBox : renderMenuItem}</SearchBarWrapper>
}
const SearchBarWrapper = styled.div`
height: 2.2rem;
`
export default memo(SidebarSearch)

View File

@@ -96,15 +96,19 @@ const Markdown: FC<Props> = ({ block }) => {
} as Partial<Components> } as Partial<Components>
}, [onSaveCodeBlock, block.id]) }, [onSaveCodeBlock, block.id])
if (messageContent.includes('<style>')) {
components.style = MarkdownShadowDOMRenderer as any
}
const urlTransform = useCallback((value: string) => { const urlTransform = useCallback((value: string) => {
if (value.startsWith('data:image/png') || value.startsWith('data:image/jpeg')) return value if (value.startsWith('data:image/png') || value.startsWith('data:image/jpeg')) return value
return defaultUrlTransform(value) return defaultUrlTransform(value)
}, []) }, [])
// if (role === 'user' && !renderInputMessageAsMarkdown) {
// return <p style={{ marginBottom: 5, whiteSpace: 'pre-wrap' }}>{messageContent}</p>
// }
if (messageContent.includes('<style>')) {
components.style = MarkdownShadowDOMRenderer as any
}
return ( return (
<div className="markdown"> <div className="markdown">
<ReactMarkdown <ReactMarkdown

View File

@@ -47,7 +47,7 @@ const MainTextBlock: React.FC<Props> = ({ block, citationBlockId, role, mentions
</Flex> </Flex>
)} )}
{role === 'user' && !renderInputMessageAsMarkdown ? ( {role === 'user' && !renderInputMessageAsMarkdown ? (
<p className="markdown" style={{ whiteSpace: 'pre-wrap' }}> <p className="markdown" style={{ marginBottom: 5, whiteSpace: 'pre-wrap' }}>
{block.content} {block.content}
</p> </p>
) : ( ) : (

View File

@@ -6,7 +6,6 @@ import {
VerticalAlignBottomOutlined, VerticalAlignBottomOutlined,
VerticalAlignTopOutlined VerticalAlignTopOutlined
} from '@ant-design/icons' } from '@ant-design/icons'
import { useSettings } from '@renderer/hooks/useSettings'
import { RootState } from '@renderer/store' import { RootState } from '@renderer/store'
// import { selectCurrentTopicId } from '@renderer/store/newMessage' // import { selectCurrentTopicId } from '@renderer/store/newMessage'
import { Button, Drawer, Tooltip } from 'antd' import { Button, Drawer, Tooltip } from 'antd'
@@ -44,8 +43,6 @@ const ChatNavigation: FC<ChatNavigationProps> = ({ containerId }) => {
const [manuallyClosedUntil, setManuallyClosedUntil] = useState<number | null>(null) const [manuallyClosedUntil, setManuallyClosedUntil] = useState<number | null>(null)
const currentTopicId = useSelector((state: RootState) => state.messages.currentTopicId) const currentTopicId = useSelector((state: RootState) => state.messages.currentTopicId)
const lastMoveTime = useRef(0) const lastMoveTime = useRef(0)
const { topicPosition, showTopics } = useSettings()
const showRightTopics = topicPosition === 'right' && showTopics
// Reset hide timer and make buttons visible // Reset hide timer and make buttons visible
const resetHideTimer = useCallback(() => { const resetHideTimer = useCallback(() => {
@@ -274,14 +271,7 @@ const ChatNavigation: FC<ChatNavigationProps> = ({ containerId }) => {
// Calculate if the mouse is in the trigger area // Calculate if the mouse is in the trigger area
const triggerWidth = 60 // Same as the width in styled component const triggerWidth = 60 // Same as the width in styled component
// Safe way to calculate position when using calc expressions const rightPosition = window.innerWidth - triggerWidth
let rightOffset = RIGHT_GAP // Default right offset
if (showRightTopics) {
// When topics are shown on right, we need to account for topic list width
rightOffset += 275 // --topic-list-width
}
const rightPosition = window.innerWidth - rightOffset - triggerWidth
const topPosition = window.innerHeight * 0.35 // 35% from top const topPosition = window.innerHeight * 0.35 // 35% from top
const height = window.innerHeight * 0.3 // 30% of window height const height = window.innerHeight * 0.3 // 30% of window height
@@ -326,16 +316,7 @@ const ChatNavigation: FC<ChatNavigationProps> = ({ containerId }) => {
clearTimeout(hideTimer) clearTimeout(hideTimer)
} }
} }
}, [ }, [containerId, hideTimer, resetHideTimer, isNearButtons, handleMouseEnter, handleMouseLeave, manuallyClosedUntil])
containerId,
hideTimer,
resetHideTimer,
isNearButtons,
handleMouseEnter,
handleMouseLeave,
showRightTopics,
manuallyClosedUntil
])
return ( return (
<> <>

View File

@@ -151,9 +151,7 @@ const WebSearchCitation: React.FC<{ citation: Citation }> = ({ citation }) => {
<CitationLink className="text-nowrap" href={citation.url} onClick={(e) => handleLinkClick(citation.url, e)}> <CitationLink className="text-nowrap" href={citation.url} onClick={(e) => handleLinkClick(citation.url, e)}>
{citation.title || <span className="hostname">{citation.hostname}</span>} {citation.title || <span className="hostname">{citation.hostname}</span>}
</CitationLink> </CitationLink>
<CitationIndex>{citation.number}</CitationIndex>s{fetchedContent && <CopyButton content={fetchedContent} />}
<CitationIndex>{citation.number}</CitationIndex>
{fetchedContent && <CopyButton content={fetchedContent} />}
</WebSearchCardHeader> </WebSearchCardHeader>
{isLoading ? ( {isLoading ? (
<Skeleton active paragraph={{ rows: 1 }} title={false} /> <Skeleton active paragraph={{ rows: 1 }} title={false} />

View File

@@ -3,7 +3,7 @@ import { useMessageEditing } from '@renderer/context/MessageEditingContext'
import { useAssistant } from '@renderer/hooks/useAssistant' import { useAssistant } from '@renderer/hooks/useAssistant'
import { useMessageOperations } from '@renderer/hooks/useMessageOperations' import { useMessageOperations } from '@renderer/hooks/useMessageOperations'
import { useModel } from '@renderer/hooks/useModel' import { useModel } from '@renderer/hooks/useModel'
import { useSettings } from '@renderer/hooks/useSettings' import { useMessageStyle, useSettings } from '@renderer/hooks/useSettings'
import { EVENT_NAMES, EventEmitter } from '@renderer/services/EventService' import { EVENT_NAMES, EventEmitter } from '@renderer/services/EventService'
import { getMessageModelId } from '@renderer/services/MessagesService' import { getMessageModelId } from '@renderer/services/MessagesService'
import { getModelUniqId } from '@renderer/services/ModelService' import { getModelUniqId } from '@renderer/services/ModelService'
@@ -21,6 +21,7 @@ import MessageEditor from './MessageEditor'
import MessageErrorBoundary from './MessageErrorBoundary' import MessageErrorBoundary from './MessageErrorBoundary'
import MessageHeader from './MessageHeader' import MessageHeader from './MessageHeader'
import MessageMenubar from './MessageMenubar' import MessageMenubar from './MessageMenubar'
import MessageTokens from './MessageTokens'
interface Props { interface Props {
message: Message message: Message
@@ -42,12 +43,14 @@ const MessageItem: FC<Props> = ({
index, index,
hideMenuBar = false, hideMenuBar = false,
isGrouped, isGrouped,
isStreaming = false isStreaming = false,
style
}) => { }) => {
const { t } = useTranslation() const { t } = useTranslation()
const { assistant, setModel } = useAssistant(message.assistantId) const { assistant, setModel } = useAssistant(message.assistantId)
const model = useModel(getMessageModelId(message), message.model?.provider) || message.model const model = useModel(getMessageModelId(message), message.model?.provider) || message.model
const { messageFont, fontSize, messageStyle } = useSettings() const { isBubbleStyle } = useMessageStyle()
const { showMessageDivider, messageFont, fontSize, messageStyle } = useSettings()
const { editMessageBlocks, resendUserMessageWithEdit, editMessage } = useMessageOperations(topic) const { editMessageBlocks, resendUserMessageWithEdit, editMessage } = useMessageOperations(topic)
const messageContainerRef = useRef<HTMLDivElement>(null) const messageContainerRef = useRef<HTMLDivElement>(null)
const { editingMessageId, stopEditing } = useMessageEditing() const { editingMessageId, stopEditing } = useMessageEditing()
@@ -99,6 +102,8 @@ const MessageItem: FC<Props> = ({
const isAssistantMessage = message.role === 'assistant' const isAssistantMessage = message.role === 'assistant'
const showMenubar = !hideMenuBar && !isStreaming && !message.status.includes('ing') && !isEditing const showMenubar = !hideMenuBar && !isStreaming && !message.status.includes('ing') && !isEditing
const messageBackground = getMessageBackground(isBubbleStyle, isAssistantMessage)
const messageHighlightHandler = useCallback((highlight: boolean = true) => { const messageHighlightHandler = useCallback((highlight: boolean = true) => {
if (messageContainerRef.current) { if (messageContainerRef.current) {
messageContainerRef.current.scrollIntoView({ behavior: 'smooth' }) messageContainerRef.current.scrollIntoView({ behavior: 'smooth' })
@@ -127,6 +132,29 @@ const MessageItem: FC<Props> = ({
) )
} }
if (isEditing) {
return (
<MessageContainer style={{ paddingTop: 15 }}>
<MessageHeader
message={message}
assistant={assistant}
model={model}
key={getModelUniqId(model)}
topic={topic}
/>
<div style={{ paddingLeft: messageStyle === 'plain' ? 46 : undefined }}>
<MessageEditor
message={message}
topicId={topic.id}
onSave={handleEditSave}
onResend={handleEditResend}
onCancel={handleEditCancel}
/>
</div>
</MessageContainer>
)
}
return ( return (
<MessageContainer <MessageContainer
key={message.id} key={message.id}
@@ -135,58 +163,55 @@ const MessageItem: FC<Props> = ({
'message-assistant': isAssistantMessage, 'message-assistant': isAssistantMessage,
'message-user': !isAssistantMessage 'message-user': !isAssistantMessage
})} })}
ref={messageContainerRef}> ref={messageContainerRef}
style={style}>
<MessageHeader message={message} assistant={assistant} model={model} key={getModelUniqId(model)} topic={topic} /> <MessageHeader message={message} assistant={assistant} model={model} key={getModelUniqId(model)} topic={topic} />
{isEditing && ( <MessageContentContainer
<MessageEditor className="message-content-container"
message={message} style={{
topicId={topic.id} fontFamily: messageFont === 'serif' ? 'var(--font-family-serif)' : 'var(--font-family)',
onSave={handleEditSave} fontSize,
onResend={handleEditResend} background: messageBackground,
onCancel={handleEditCancel} overflowY: 'visible'
/> }}>
)} <MessageErrorBoundary>
{!isEditing && ( <MessageContent message={message} />
<> </MessageErrorBoundary>
<MessageContentContainer </MessageContentContainer>
className="message-content-container" {showMenubar && (
style={{ <MessageFooter className="MessageFooter" $isLastMessage={isLastMessage} $messageStyle={messageStyle}>
fontFamily: messageFont === 'serif' ? 'var(--font-family-serif)' : 'var(--font-family)', <MessageMenubar
fontSize, message={message}
overflowY: 'visible' assistant={assistant}
}}> model={model}
<MessageErrorBoundary> index={index}
<MessageContent message={message} /> topic={topic}
</MessageErrorBoundary> isLastMessage={isLastMessage}
</MessageContentContainer> isAssistantMessage={isAssistantMessage}
{showMenubar && ( isGrouped={isGrouped}
<MessageFooter className="MessageFooter" $isLastMessage={isLastMessage} $messageStyle={messageStyle}> messageContainerRef={messageContainerRef as React.RefObject<HTMLDivElement>}
<MessageMenubar setModel={setModel}
message={message} />
assistant={assistant} </MessageFooter>
model={model}
index={index}
topic={topic}
isLastMessage={isLastMessage}
isAssistantMessage={isAssistantMessage}
isGrouped={isGrouped}
messageContainerRef={messageContainerRef as React.RefObject<HTMLDivElement>}
setModel={setModel}
/>
</MessageFooter>
)}
</>
)} )}
</MessageContainer> </MessageContainer>
) )
} }
const getMessageBackground = (isBubbleStyle: boolean, isAssistantMessage: boolean) => {
return isBubbleStyle
? isAssistantMessage
? 'var(--chat-background-assistant)'
: 'var(--chat-background-user)'
: undefined
}
const MessageContainer = styled.div` const MessageContainer = styled.div`
display: flex; display: flex;
flex-direction: column; flex-direction: column;
width: 100%;
position: relative; position: relative;
transition: background-color 0.3s ease; transition: background-color 0.3s ease;
padding: 0 24px;
transform: translateZ(0); transform: translateZ(0);
will-change: transform; will-change: transform;
padding: 10px; padding: 10px;
@@ -226,11 +251,11 @@ const MessageFooter = styled.div<{ $isLastMessage: boolean; $messageStyle: 'plai
gap: 10px; gap: 10px;
margin-left: 46px; margin-left: 46px;
margin-top: 8px; margin-top: 8px;
border-top: 0.5px dotted var(--color-border);
` `
const NewContextMessage = styled.div` const NewContextMessage = styled.div`
cursor: pointer; cursor: pointer;
flex: 1;
` `
export default memo(MessageItem) export default memo(MessageItem)

View File

@@ -1,6 +1,21 @@
import {
FileExcelFilled,
FileImageFilled,
FileMarkdownFilled,
FilePdfFilled,
FilePptFilled,
FileTextFilled,
FileUnknownFilled,
FileWordFilled,
FileZipFilled,
FolderOpenFilled,
GlobalOutlined,
LinkOutlined
} from '@ant-design/icons'
import CustomTag from '@renderer/components/CustomTag'
import FileManager from '@renderer/services/FileManager' import FileManager from '@renderer/services/FileManager'
import { FileType } from '@renderer/types'
import type { FileMessageBlock } from '@renderer/types/newMessage' import type { FileMessageBlock } from '@renderer/types/newMessage'
import { Upload } from 'antd'
import { FC } from 'react' import { FC } from 'react'
import styled from 'styled-components' import styled from 'styled-components'
@@ -8,17 +23,6 @@ interface Props {
block: FileMessageBlock block: FileMessageBlock
} }
const StyledUpload = styled(Upload)`
.ant-upload-list-item-name {
max-width: 220px;
display: inline-block;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
vertical-align: bottom;
}
`
const MessageAttachments: FC<Props> = ({ block }) => { const MessageAttachments: FC<Props> = ({ block }) => {
// const handleCopyImage = async (image: FileMetadata) => { // const handleCopyImage = async (image: FileMetadata) => {
// const data = await FileManager.readFile(image) // const data = await FileManager.readFile(image)
@@ -30,54 +34,84 @@ const MessageAttachments: FC<Props> = ({ block }) => {
if (!block.file) { if (!block.file) {
return null return null
} }
// 由图片块代替
// if (block.file.type === FileTypes.IMAGE) { const MAX_FILENAME_DISPLAY_LENGTH = 20
// return ( function truncateFileName(name: string, maxLength: number = MAX_FILENAME_DISPLAY_LENGTH) {
// <Container style={{ marginBottom: 8 }}> if (name.length <= maxLength) return name
// <Image return name.slice(0, maxLength - 3) + '...'
// src={FileManager.getFileUrl(block.file)} }
// key={block.file.id}
// width="33%" const getFileIcon = (type?: string) => {
// preview={{ if (!type) return <FileUnknownFilled />
// toolbarRender: (
// _, const ext = type.toLowerCase()
// {
// transform: { scale }, if (['.jpg', '.jpeg', '.png', '.gif', '.bmp', '.webp'].includes(ext)) {
// actions: { onFlipY, onFlipX, onRotateLeft, onRotateRight, onZoomOut, onZoomIn, onReset } return <FileImageFilled />
// } }
// ) => (
// <ToobarWrapper size={12} className="toolbar-wrapper"> if (['.doc', '.docx'].includes(ext)) {
// <SwapOutlined rotate={90} onClick={onFlipY} /> return <FileWordFilled />
// <SwapOutlined onClick={onFlipX} /> }
// <RotateLeftOutlined onClick={onRotateLeft} /> if (['.xls', '.xlsx'].includes(ext)) {
// <RotateRightOutlined onClick={onRotateRight} /> return <FileExcelFilled />
// <ZoomOutOutlined disabled={scale === 1} onClick={onZoomOut} /> }
// <ZoomInOutlined disabled={scale === 50} onClick={onZoomIn} /> if (['.ppt', '.pptx'].includes(ext)) {
// <UndoOutlined onClick={onReset} /> return <FilePptFilled />
// <CopyOutlined onClick={() => handleCopyImage(block.file)} /> }
// <DownloadOutlined onClick={() => download(FileManager.getFileUrl(block.file))} /> if (ext === '.pdf') {
// </ToobarWrapper> return <FilePdfFilled />
// ) }
// }} if (['.md', '.markdown'].includes(ext)) {
// /> return <FileMarkdownFilled />
// </Container> }
// )
// } if (['.zip', '.rar', '.7z', '.tar', '.gz'].includes(ext)) {
return <FileZipFilled />
}
if (['.txt', '.json', '.log', '.yml', '.yaml', '.xml', '.csv'].includes(ext)) {
return <FileTextFilled />
}
if (['.url'].includes(ext)) {
return <LinkOutlined />
}
if (['.sitemap'].includes(ext)) {
return <GlobalOutlined />
}
if (['.folder'].includes(ext)) {
return <FolderOpenFilled />
}
return <FileUnknownFilled />
}
const FileNameRender: FC<{ file: FileType }> = ({ file }) => {
const fullName = FileManager.formatFileName(file)
const displayName = truncateFileName(fullName)
return (
<FileName
onClick={() => {
const path = FileManager.getSafePath(file)
if (path) {
window.api.file.openPath(path)
}
}}
title={fullName}>
{displayName}
</FileName>
)
}
return ( return (
<Container style={{ marginTop: 2, marginBottom: 8 }} className="message-attachments"> <Container style={{ marginTop: 2, marginBottom: 8 }} className="message-attachments">
<StyledUpload <CustomTag key={block.file.id} icon={getFileIcon(block.file.ext)} color="#37a5aa">
listType="text" <FileNameRender file={block.file} />
disabled </CustomTag>
fileList={[
{
uid: block.file.id,
url: 'file://' + FileManager.getSafePath(block.file),
status: 'done' as const,
name: FileManager.formatFileName(block.file)
}
]}
/>
</Container> </Container>
) )
} }
@@ -89,23 +123,11 @@ const Container = styled.div`
margin-top: 8px; margin-top: 8px;
` `
// const Image = styled(AntdImage)` const FileName = styled.span`
// border-radius: 10px; cursor: pointer;
// ` &:hover {
text-decoration: underline;
// const ToobarWrapper = 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 MessageAttachments export default MessageAttachments

View File

@@ -14,7 +14,7 @@ const MessageContent: React.FC<Props> = ({ message }) => {
return ( return (
<> <>
{!isEmpty(message.mentions) && ( {!isEmpty(message.mentions) && (
<Flex gap="8px" wrap> <Flex gap="8px" wrap style={{ marginBottom: 10 }}>
{message.mentions?.map((model) => <MentionTag key={getModelUniqId(model)}>{'@' + model.name}</MentionTag>)} {message.mentions?.map((model) => <MentionTag key={getModelUniqId(model)}>{'@' + model.name}</MentionTag>)}
</Flex> </Flex>
)} )}

View File

@@ -365,7 +365,7 @@ const EditorContainer = styled.div`
padding-bottom: 5px; padding-bottom: 5px;
border: 0.5px solid var(--color-border); border: 0.5px solid var(--color-border);
transition: all 0.2s ease; transition: all 0.2s ease;
border-radius: 15px; border-radius: var(--list-item-border-radius);
margin-top: 18px; margin-top: 18px;
background-color: var(--color-background-opacity); background-color: var(--color-background-opacity);
width: 100%; width: 100%;

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