From 432b31c7b1f364dd911895320e74651000ea3b4e Mon Sep 17 00:00:00 2001 From: Pleasure1234 <3196812536@qq.com> Date: Wed, 17 Dec 2025 02:11:11 +0000 Subject: [PATCH 1/7] fix: Bind OAuth callback server to localhost (#11956) Updated the server to listen explicitly on 127.0.0.1 instead of all interfaces. The log message was also updated to reflect the new binding address. --- src/main/services/mcp/oauth/callback.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/services/mcp/oauth/callback.ts b/src/main/services/mcp/oauth/callback.ts index c13ecd5c07..7da7544585 100644 --- a/src/main/services/mcp/oauth/callback.ts +++ b/src/main/services/mcp/oauth/callback.ts @@ -128,8 +128,8 @@ export class CallBackServer { }) return new Promise((resolve, reject) => { - server.listen(port, () => { - logger.info(`OAuth callback server listening on port ${port}`) + server.listen(port, '127.0.0.1', () => { + logger.info(`OAuth callback server listening on 127.0.0.1:${port}`) resolve(server) }) From 784fdd4fed6eb7ebcf547d43b815b76b2b38c50f Mon Sep 17 00:00:00 2001 From: SuYao Date: Wed, 17 Dec 2025 13:36:13 +0800 Subject: [PATCH 2/7] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8D=E8=B7=A8=E5=B9=B3?= =?UTF-8?q?=E5=8F=B0=E6=81=A2=E5=A4=8D=E5=9C=BA=E6=99=AF=E4=B8=8B=E7=9A=84?= =?UTF-8?q?=E7=AC=94=E8=AE=B0=E7=9B=AE=E5=BD=95=E9=AA=8C=E8=AF=81=E5=92=8C?= =?UTF-8?q?=E9=BB=98=E8=AE=A4=E8=B7=AF=E5=BE=84=E9=87=8D=E7=BD=AE=E9=80=BB?= =?UTF-8?q?=E8=BE=91=20(#11950)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix: 修复跨平台恢复场景下的笔记目录验证和默认路径重置逻辑 * fix: 优化跨平台恢复场景下的笔记目录验证逻辑,跳过默认路径的验证 --- src/main/services/FileStorage.ts | 2 +- src/renderer/src/i18n/locales/en-us.json | 1 + src/renderer/src/i18n/locales/zh-cn.json | 1 + src/renderer/src/i18n/locales/zh-tw.json | 1 + src/renderer/src/i18n/translate/de-de.json | 1 + src/renderer/src/i18n/translate/el-gr.json | 1 + src/renderer/src/i18n/translate/es-es.json | 1 + src/renderer/src/i18n/translate/fr-fr.json | 1 + src/renderer/src/i18n/translate/ja-jp.json | 1 + src/renderer/src/i18n/translate/pt-pt.json | 1 + src/renderer/src/i18n/translate/ru-ru.json | 1 + src/renderer/src/pages/notes/NotesPage.tsx | 38 ++++++++++++++++++++++ 12 files changed, 49 insertions(+), 1 deletion(-) diff --git a/src/main/services/FileStorage.ts b/src/main/services/FileStorage.ts index 81f5c15bd9..78bffa6692 100644 --- a/src/main/services/FileStorage.ts +++ b/src/main/services/FileStorage.ts @@ -163,7 +163,7 @@ class FileStorage { fs.mkdirSync(this.storageDir, { recursive: true }) } if (!fs.existsSync(this.notesDir)) { - fs.mkdirSync(this.storageDir, { recursive: true }) + fs.mkdirSync(this.notesDir, { recursive: true }) } if (!fs.existsSync(this.tempDir)) { fs.mkdirSync(this.tempDir, { recursive: true }) diff --git a/src/renderer/src/i18n/locales/en-us.json b/src/renderer/src/i18n/locales/en-us.json index 298ffeb86c..3d12402e61 100644 --- a/src/renderer/src/i18n/locales/en-us.json +++ b/src/renderer/src/i18n/locales/en-us.json @@ -2152,6 +2152,7 @@ "collapse": "Collapse", "content_placeholder": "Please enter the note content...", "copyContent": "Copy Content", + "crossPlatformRestoreWarning": "Cross-platform configuration restored, but notes directory is empty. Please copy your note files to: {{path}}", "delete": "delete", "delete_confirm": "Are you sure you want to delete this {{type}}?", "delete_folder_confirm": "Are you sure you want to delete the folder \"{{name}}\" and all of its contents?", diff --git a/src/renderer/src/i18n/locales/zh-cn.json b/src/renderer/src/i18n/locales/zh-cn.json index 8c10f3687b..f8222c4123 100644 --- a/src/renderer/src/i18n/locales/zh-cn.json +++ b/src/renderer/src/i18n/locales/zh-cn.json @@ -2152,6 +2152,7 @@ "collapse": "收起", "content_placeholder": "请输入笔记内容...", "copyContent": "复制内容", + "crossPlatformRestoreWarning": "检测到从其他设备恢复配置,但笔记目录为空。请将笔记文件复制到: {{path}}", "delete": "删除", "delete_confirm": "确定要删除这个{{type}}吗?", "delete_folder_confirm": "确定要删除文件夹 \"{{name}}\" 及其所有内容吗?", diff --git a/src/renderer/src/i18n/locales/zh-tw.json b/src/renderer/src/i18n/locales/zh-tw.json index 162373b557..27eac8065d 100644 --- a/src/renderer/src/i18n/locales/zh-tw.json +++ b/src/renderer/src/i18n/locales/zh-tw.json @@ -2152,6 +2152,7 @@ "collapse": "收合", "content_placeholder": "請輸入筆記內容...", "copyContent": "複製內容", + "crossPlatformRestoreWarning": "偵測到從其他裝置恢復設定,但筆記目錄為空。請將筆記檔案複製到:{{path}}", "delete": "刪除", "delete_confirm": "確定要刪除此 {{type}} 嗎?", "delete_folder_confirm": "確定要刪除資料夾 \"{{name}}\" 及其所有內容嗎?", diff --git a/src/renderer/src/i18n/translate/de-de.json b/src/renderer/src/i18n/translate/de-de.json index 4bc992759c..6fd98193ac 100644 --- a/src/renderer/src/i18n/translate/de-de.json +++ b/src/renderer/src/i18n/translate/de-de.json @@ -2152,6 +2152,7 @@ "collapse": "Einklappen", "content_placeholder": "Bitte Notizinhalt eingeben...", "copyContent": "Inhalt kopieren", + "crossPlatformRestoreWarning": "[to be translated]:Cross-platform configuration restored, but notes directory is empty. Please copy your note files to: {{path}}", "delete": "Löschen", "delete_confirm": "Möchten Sie diesen {{type}} wirklich löschen?", "delete_folder_confirm": "Möchten Sie Ordner \"{{name}}\" und alle seine Inhalte wirklich löschen?", diff --git a/src/renderer/src/i18n/translate/el-gr.json b/src/renderer/src/i18n/translate/el-gr.json index 44fba429f7..0f002836ff 100644 --- a/src/renderer/src/i18n/translate/el-gr.json +++ b/src/renderer/src/i18n/translate/el-gr.json @@ -2152,6 +2152,7 @@ "collapse": "σύμπτυξη", "content_placeholder": "Παρακαλώ εισαγάγετε το περιεχόμενο των σημειώσεων...", "copyContent": "αντιγραφή περιεχομένου", + "crossPlatformRestoreWarning": "[to be translated]:Cross-platform configuration restored, but notes directory is empty. Please copy your note files to: {{path}}", "delete": "διαγραφή", "delete_confirm": "Είστε βέβαιοι ότι θέλετε να διαγράψετε αυτό το {{type}};", "delete_folder_confirm": "Θέλετε να διαγράψετε τον φάκελο «{{name}}» και όλο το περιεχόμενό του;", diff --git a/src/renderer/src/i18n/translate/es-es.json b/src/renderer/src/i18n/translate/es-es.json index 5cf620ed45..18832aeca5 100644 --- a/src/renderer/src/i18n/translate/es-es.json +++ b/src/renderer/src/i18n/translate/es-es.json @@ -2152,6 +2152,7 @@ "collapse": "ocultar", "content_placeholder": "Introduzca el contenido de la nota...", "copyContent": "copiar contenido", + "crossPlatformRestoreWarning": "[to be translated]:Cross-platform configuration restored, but notes directory is empty. Please copy your note files to: {{path}}", "delete": "eliminar", "delete_confirm": "¿Estás seguro de que deseas eliminar este {{type}}?", "delete_folder_confirm": "¿Está seguro de que desea eliminar la carpeta \"{{name}}\" y todo su contenido?", diff --git a/src/renderer/src/i18n/translate/fr-fr.json b/src/renderer/src/i18n/translate/fr-fr.json index fdb72727b8..e31cba94dd 100644 --- a/src/renderer/src/i18n/translate/fr-fr.json +++ b/src/renderer/src/i18n/translate/fr-fr.json @@ -2152,6 +2152,7 @@ "collapse": "réduire", "content_placeholder": "Veuillez saisir le contenu de la note...", "copyContent": "contenu copié", + "crossPlatformRestoreWarning": "[to be translated]:Cross-platform configuration restored, but notes directory is empty. Please copy your note files to: {{path}}", "delete": "supprimer", "delete_confirm": "Êtes-vous sûr de vouloir supprimer ce {{type}} ?", "delete_folder_confirm": "Êtes-vous sûr de vouloir supprimer le dossier \"{{name}}\" et tout son contenu ?", diff --git a/src/renderer/src/i18n/translate/ja-jp.json b/src/renderer/src/i18n/translate/ja-jp.json index d004d539e5..93d4219e22 100644 --- a/src/renderer/src/i18n/translate/ja-jp.json +++ b/src/renderer/src/i18n/translate/ja-jp.json @@ -2152,6 +2152,7 @@ "collapse": "閉じる", "content_placeholder": "メモの内容を入力してください...", "copyContent": "コンテンツをコピーします", + "crossPlatformRestoreWarning": "[to be translated]:Cross-platform configuration restored, but notes directory is empty. Please copy your note files to: {{path}}", "delete": "削除", "delete_confirm": "この{{type}}を本当に削除しますか?", "delete_folder_confirm": "「{{name}}」フォルダーとそのすべての内容を削除してもよろしいですか?", diff --git a/src/renderer/src/i18n/translate/pt-pt.json b/src/renderer/src/i18n/translate/pt-pt.json index 32c1965f54..11564b14bc 100644 --- a/src/renderer/src/i18n/translate/pt-pt.json +++ b/src/renderer/src/i18n/translate/pt-pt.json @@ -2152,6 +2152,7 @@ "collapse": "[minimizar]", "content_placeholder": "Introduza o conteúdo da nota...", "copyContent": "copiar conteúdo", + "crossPlatformRestoreWarning": "[to be translated]:Cross-platform configuration restored, but notes directory is empty. Please copy your note files to: {{path}}", "delete": "eliminar", "delete_confirm": "Tem a certeza de que deseja eliminar este {{type}}?", "delete_folder_confirm": "Tem a certeza de que deseja eliminar a pasta \"{{name}}\" e todos os seus conteúdos?", diff --git a/src/renderer/src/i18n/translate/ru-ru.json b/src/renderer/src/i18n/translate/ru-ru.json index 3d022ae24b..f0d9937dba 100644 --- a/src/renderer/src/i18n/translate/ru-ru.json +++ b/src/renderer/src/i18n/translate/ru-ru.json @@ -2152,6 +2152,7 @@ "collapse": "Свернуть", "content_placeholder": "Введите содержимое заметки...", "copyContent": "Копировать контент", + "crossPlatformRestoreWarning": "[to be translated]:Cross-platform configuration restored, but notes directory is empty. Please copy your note files to: {{path}}", "delete": "удалить", "delete_confirm": "Вы уверены, что хотите удалить этот объект {{type}}?", "delete_folder_confirm": "Вы уверены, что хотите удалить папку \"{{name}}\" со всем ее содержимым?", diff --git a/src/renderer/src/pages/notes/NotesPage.tsx b/src/renderer/src/pages/notes/NotesPage.tsx index 7692aa9975..22d4ba4459 100644 --- a/src/renderer/src/pages/notes/NotesPage.tsx +++ b/src/renderer/src/pages/notes/NotesPage.tsx @@ -39,6 +39,7 @@ import { } from '@renderer/store/note' import type { NotesSortType, NotesTreeNode } from '@renderer/types/note' import type { FileChangeEvent } from '@shared/config/types' +import { message } from 'antd' import { debounce } from 'lodash' import { AnimatePresence, motion } from 'motion/react' import type { FC } from 'react' @@ -246,6 +247,43 @@ const NotesPage: FC = () => { updateNotesPath(defaultPath) return } + + // 验证路径是否有效(处理跨平台恢复场景) + try { + // 获取当前平台的默认路径 + const info = await window.api.getAppInfo() + const defaultPath = info.notesPath + + // 如果当前路径就是默认路径,跳过验证(默认路径始终有效) + if (notesPath === defaultPath) { + return + } + + const isValid = await window.api.file.validateNotesDirectory(notesPath) + if (!isValid) { + logger.warn('Invalid notes path detected, resetting to default', { path: notesPath }) + + // 重置为默认路径 + updateNotesPath(defaultPath) + + // 检查默认路径下是否有笔记文件 + try { + const tree = await window.api.file.getDirectoryStructure(defaultPath) + if (!tree || tree.length === 0) { + // 默认目录为空,提示用户需要迁移文件 + message.warning({ + content: t('notes.crossPlatformRestoreWarning', { path: defaultPath }), + duration: 10 + }) + } + } catch (error) { + // 目录不存在或读取失败,会由 FileStorage 自动创建 + logger.debug('Default notes directory will be created', { error }) + } + } + } catch (error) { + logger.error('Failed to validate notes path:', error as Error) + } } initialize() From bfeef7ef911698af77f7f853bec03c239233e7e8 Mon Sep 17 00:00:00 2001 From: Pleasure1234 <3196812536@qq.com> Date: Wed, 17 Dec 2025 07:21:06 +0000 Subject: [PATCH 3/7] fix: refactor provider headers logic in providerConfig (#11849) Simplifies and centralizes header construction by merging defaultAppHeaders and extra_headers, and sets X-Api-Key for OpenAI providers. Removes redundant header assignment logic for improved maintainability. --- .../src/aiCore/provider/providerConfig.ts | 20 ++++++++----------- 1 file changed, 8 insertions(+), 12 deletions(-) diff --git a/src/renderer/src/aiCore/provider/providerConfig.ts b/src/renderer/src/aiCore/provider/providerConfig.ts index 1c410bf124..556b870e59 100644 --- a/src/renderer/src/aiCore/provider/providerConfig.ts +++ b/src/renderer/src/aiCore/provider/providerConfig.ts @@ -32,6 +32,7 @@ import { isSupportStreamOptionsProvider, isVertexProvider } from '@renderer/utils/provider' +import { defaultAppHeaders } from '@shared/utils' import { cloneDeep, isEmpty } from 'lodash' import type { AiSdkConfig } from '../types' @@ -197,18 +198,13 @@ export function providerToAiSdkConfig(actualProvider: Provider, model: Model): A extraOptions.mode = 'chat' } - // 添加额外headers - if (actualProvider.extra_headers) { - extraOptions.headers = actualProvider.extra_headers - // copy from openaiBaseClient/openaiResponseApiClient - if (aiSdkProviderId === 'openai') { - extraOptions.headers = { - ...extraOptions.headers, - 'HTTP-Referer': 'https://cherry-ai.com', - 'X-Title': 'Cherry Studio', - 'X-Api-Key': baseConfig.apiKey - } - } + extraOptions.headers = { + ...defaultAppHeaders(), + ...actualProvider.extra_headers + } + + if (aiSdkProviderId === 'openai') { + extraOptions.headers['X-Api-Key'] = baseConfig.apiKey } // azure // https://learn.microsoft.com/en-us/azure/ai-foundry/openai/latest From 782f8496e0c2568da69a18b4dade35fd9cbfaf09 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=BA=A2=E5=A5=8B=E7=8C=AB?= Date: Wed, 17 Dec 2025 15:37:11 +0800 Subject: [PATCH 4/7] feat: add tool use mode setting to default assistant settings (#11943) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: add tool use mode setting to default assistant settings - Add toolUseMode selector (prompt/function) to DefaultAssistantSettings - Add dividers between model parameter sections for better UI - Reduce slider margins for compact layout - Add migration (v185) to reset toolUseMode to 'function' for existing users 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 * fix: reset toolUseMode for all assistants during migration - Update migration logic to reset toolUseMode to 'function' for all assistants with a 'prompt' setting. - Ensure compatibility with function calling models by checking model type before resetting. --------- Co-authored-by: Claude Opus 4.5 --- .../DefaultAssistantSettings.tsx | 44 +++++++++++++++---- src/renderer/src/store/index.ts | 2 +- src/renderer/src/store/migrate.ts | 21 +++++++++ 3 files changed, 57 insertions(+), 10 deletions(-) diff --git a/src/renderer/src/pages/settings/ModelSettings/DefaultAssistantSettings.tsx b/src/renderer/src/pages/settings/ModelSettings/DefaultAssistantSettings.tsx index 2b1bed5ebe..65be642adc 100644 --- a/src/renderer/src/pages/settings/ModelSettings/DefaultAssistantSettings.tsx +++ b/src/renderer/src/pages/settings/ModelSettings/DefaultAssistantSettings.tsx @@ -2,13 +2,14 @@ import { CloseCircleFilled, QuestionCircleOutlined } from '@ant-design/icons' import EmojiPicker from '@renderer/components/EmojiPicker' import { ResetIcon } from '@renderer/components/Icons' import { HStack } from '@renderer/components/Layout' +import Selector from '@renderer/components/Selector' import { TopView } from '@renderer/components/TopView' import { DEFAULT_CONTEXTCOUNT, DEFAULT_MAX_TOKENS, DEFAULT_TEMPERATURE } from '@renderer/config/constant' import { useTheme } from '@renderer/context/ThemeProvider' import { useDefaultAssistant } from '@renderer/hooks/useAssistant' import type { AssistantSettings as AssistantSettingsType } from '@renderer/types' import { getLeadingEmoji, modalConfirm } from '@renderer/utils' -import { Button, Col, Flex, Input, InputNumber, Modal, Popover, Row, Slider, Switch, Tooltip } from 'antd' +import { Button, Col, Divider, Flex, Input, InputNumber, Modal, Popover, Row, Slider, Switch, Tooltip } from 'antd' import TextArea from 'antd/es/input/TextArea' import type { Dispatch, FC, SetStateAction } from 'react' import { useState } from 'react' @@ -26,6 +27,9 @@ const AssistantSettings: FC = () => { const [maxTokens, setMaxTokens] = useState(defaultAssistant?.settings?.maxTokens ?? 0) const [topP, setTopP] = useState(defaultAssistant.settings?.topP ?? 1) const [enableTopP, setEnableTopP] = useState(defaultAssistant.settings?.enableTopP ?? false) + const [toolUseMode, setToolUseMode] = useState( + defaultAssistant.settings?.toolUseMode ?? 'function' + ) const [emoji, setEmoji] = useState(defaultAssistant.emoji || getLeadingEmoji(defaultAssistant.name) || '') const [name, setName] = useState( defaultAssistant.name.replace(getLeadingEmoji(defaultAssistant.name) || '', '').trim() @@ -46,7 +50,8 @@ const AssistantSettings: FC = () => { maxTokens: settings.maxTokens ?? maxTokens, streamOutput: settings.streamOutput ?? true, topP: settings.topP ?? topP, - enableTopP: settings.enableTopP ?? enableTopP + enableTopP: settings.enableTopP ?? enableTopP, + toolUseMode: settings.toolUseMode ?? toolUseMode } }) } @@ -73,6 +78,7 @@ const AssistantSettings: FC = () => { setMaxTokens(0) setTopP(1) setEnableTopP(false) + setToolUseMode('function') updateDefaultAssistant({ ...defaultAssistant, settings: { @@ -84,7 +90,8 @@ const AssistantSettings: FC = () => { maxTokens: DEFAULT_MAX_TOKENS, streamOutput: true, topP: 1, - enableTopP: false + enableTopP: false, + toolUseMode: 'function' } }) } @@ -107,10 +114,9 @@ const AssistantSettings: FC = () => { return ( - {t('common.name')} - + } arrow trigger="click"> @@ -161,6 +167,7 @@ const AssistantSettings: FC = () => { +) + +const ManageDivider: FC = () =>
+ +const LeftGroup: FC = ({ children }) =>
{children}
+ +const RightGroup: FC = ({ children }) => ( +
{children}
+) + +const SelectedBadge: FC>> = ({ + children, + className, + ...props +}) => ( + + {children} + +) + +const SearchInputWrapper: FC = ({ children }) => ( +
{children}
+) + +interface SearchInputProps extends React.InputHTMLAttributes { + ref?: Ref +} + +const SearchInput: FC = ({ className, ref, ...props }) => ( + +) + +export default TopicManagePanel diff --git a/src/renderer/src/pages/home/Tabs/components/Topics.tsx b/src/renderer/src/pages/home/Tabs/components/Topics.tsx index 7284f9167c..29232e65d9 100644 --- a/src/renderer/src/pages/home/Tabs/components/Topics.tsx +++ b/src/renderer/src/pages/home/Tabs/components/Topics.tsx @@ -1,3 +1,4 @@ +import AssistantAvatar from '@renderer/components/Avatar/AssistantAvatar' import { DraggableVirtualList } from '@renderer/components/DraggableList' import { CopyIcon, DeleteIcon, EditIcon } from '@renderer/components/Icons' import ObsidianExportPopup from '@renderer/components/Popups/ObsidianExportPopup' @@ -37,8 +38,10 @@ import dayjs from 'dayjs' import { findIndex } from 'lodash' import { BrushCleaning, + CheckSquare, FolderOpen, HelpCircle, + ListChecks, MenuIcon, NotebookPen, PackagePlus, @@ -46,6 +49,7 @@ import { PinOffIcon, Save, Sparkles, + Square, UploadIcon, XIcon } from 'lucide-react' @@ -55,6 +59,7 @@ import { useDispatch, useSelector } from 'react-redux' import styled from 'styled-components' import AddButton from './AddButton' +import { TopicManagePanel, useTopicManageMode } from './TopicManageMode' interface Props { assistant: Assistant @@ -81,6 +86,10 @@ export const Topics: React.FC = ({ assistant: _assistant, activeTopic, se const deleteTimerRef = useRef(null) const [editingTopicId, setEditingTopicId] = useState(null) + // 管理模式状态 + const manageState = useTopicManageMode() + const { isManageMode, selectedIds, searchText, enterManageMode, exitManageMode, toggleSelectTopic } = manageState + const { startEdit, isEditing, inputProps } = useInPlaceEdit({ onSave: (name: string) => { const topic = assistant.topics.find((t) => t.id === editingTopicId) @@ -437,6 +446,7 @@ export const Topics: React.FC = ({ assistant: _assistant, activeTopic, se .map((a) => ({ label: a.name, key: a.id, + icon: , onClick: () => onMoveTopic(topic, a) })) }) @@ -492,107 +502,187 @@ export const Topics: React.FC = ({ assistant: _assistant, activeTopic, se return assistant.topics }, [assistant.topics, pinTopicsToTop]) + // Filter topics based on search text (only in manage mode) + // Supports: case-insensitive, space-separated keywords (all must match) + const deferredSearchText = useDeferredValue(searchText) + const filteredTopics = useMemo(() => { + if (!isManageMode || !deferredSearchText.trim()) { + return sortedTopics + } + // Split by spaces and filter out empty strings + const keywords = deferredSearchText + .toLowerCase() + .split(/\s+/) + .filter((k) => k.length > 0) + if (keywords.length === 0) { + return sortedTopics + } + // All keywords must match (AND logic) + return sortedTopics.filter((topic) => { + const lowerName = topic.name.toLowerCase() + return keywords.every((keyword) => lowerName.includes(keyword)) + }) + }, [sortedTopics, deferredSearchText, isManageMode]) + const singlealone = topicPosition === 'right' && position === 'right' return ( - - EventEmitter.emit(EVENT_NAMES.ADD_NEW_TOPIC)}> - {t('chat.add.topic.title')} - -
- - }> - {(topic) => { - const isActive = topic.id === activeTopic?.id - const topicName = topic.name.replace('`', '') - const topicPrompt = topic.prompt - const fullTopicPrompt = t('common.prompt') + ': ' + topicPrompt - - const getTopicNameClassName = () => { - if (isRenaming(topic.id)) return 'shimmer' - if (isNewlyRenamed(topic.id)) return 'typing' - return '' + <> + + EventEmitter.emit(EVENT_NAMES.ADD_NEW_TOPIC)}> + {t('chat.add.topic.title')} + + + + + + + } + disabled={isManageMode}> + {(topic) => { + const isActive = topic.id === activeTopic?.id + const topicName = topic.name.replace('`', '') + const topicPrompt = topic.prompt + const fullTopicPrompt = t('common.prompt') + ': ' + topicPrompt + const isSelected = selectedIds.has(topic.id) + const canSelect = !topic.pinned - return ( - - setTargetTopic(topic)} - className={classNames(isActive ? 'active' : '', singlealone ? 'singlealone' : '')} - onClick={editingTopicId === topic.id && isEditing ? undefined : () => onSwitchTopic(topic)} - style={{ - borderRadius, - cursor: editingTopicId === topic.id && isEditing ? 'default' : 'pointer' - }}> - {isPending(topic.id) && !isActive && } - {isFulfilled(topic.id) && !isActive && } - - {editingTopicId === topic.id && isEditing ? ( - e.stopPropagation()} /> - ) : ( - { - setEditingTopicId(topic.id) - startEdit(topic.name) - }}> - {topicName} - + const getTopicNameClassName = () => { + if (isRenaming(topic.id)) return 'shimmer' + if (isNewlyRenamed(topic.id)) return 'typing' + return '' + } + + const handleItemClick = () => { + if (isManageMode) { + if (canSelect) { + toggleSelectTopic(topic.id) + } + } else { + onSwitchTopic(topic) + } + } + + return ( + + setTargetTopic(topic)} + className={classNames( + isActive && !isManageMode ? 'active' : '', + singlealone ? 'singlealone' : '', + isManageMode && isSelected ? 'selected' : '', + isManageMode && !canSelect ? 'disabled' : '' )} - {!topic.pinned && ( - - {t('chat.topics.delete.shortcut', { key: isMac ? '⌘' : 'Ctrl' })} -
- }> - { - if (e.ctrlKey || e.metaKey) { - handleConfirmDelete(topic, e) - } else if (deletingTopicId === topic.id) { - handleConfirmDelete(topic, e) - } else { - handleDeleteClick(topic.id, e) - } - }}> - {deletingTopicId === topic.id ? ( - + onClick={editingTopicId === topic.id && isEditing ? undefined : handleItemClick} + style={{ + borderRadius, + cursor: + editingTopicId === topic.id && isEditing + ? 'default' + : isManageMode && !canSelect + ? 'not-allowed' + : 'pointer' + }}> + {isPending(topic.id) && !isActive && } + {isFulfilled(topic.id) && !isActive && } + + {isManageMode && ( + + {isSelected ? ( + ) : ( - + )} + + )} + {editingTopicId === topic.id && isEditing ? ( + e.stopPropagation()} /> + ) : ( + { + setEditingTopicId(topic.id) + startEdit(topic.name) + } + }> + {topicName} + + )} + {!topic.pinned && ( + + {t('chat.topics.delete.shortcut', { key: isMac ? '⌘' : 'Ctrl' })} + + }> + { + if (e.ctrlKey || e.metaKey) { + handleConfirmDelete(topic, e) + } else if (deletingTopicId === topic.id) { + handleConfirmDelete(topic, e) + } else { + handleDeleteClick(topic.id, e) + } + }}> + {deletingTopicId === topic.id ? ( + + ) : ( + + )} + + + )} + {topic.pinned && ( + + - + )} + + {topicPrompt && ( + + {fullTopicPrompt} + )} - {topic.pinned && ( - - - + {showTopicTime && ( + {dayjs(topic.createdAt).format('MM/DD HH:mm')} )} - - {topicPrompt && ( - - {fullTopicPrompt} - - )} - {showTopicTime && {dayjs(topic.createdAt).format('MM/DD HH:mm')}} - - - ) - }} - + + + ) + }} + + + {/* 管理模式底部面板 */} + + ) } @@ -640,6 +730,15 @@ const TopicListItem = styled.div` box-shadow: none; } } + + &.selected { + background-color: var(--color-primary-bg); + box-shadow: inset 0 0 0 1px var(--color-primary); + } + + &.disabled { + opacity: 0.5; + } ` const TopicNameContainer = styled.div` @@ -648,7 +747,6 @@ const TopicNameContainer = styled.div` align-items: center; gap: 4px; height: 20px; - justify-content: space-between; ` const TopicName = styled.div` @@ -659,6 +757,8 @@ const TopicName = styled.div` font-size: 13px; position: relative; will-change: background-position, width; + flex: 1; + text-align: left; --color-shimmer-mid: var(--color-text-1); --color-shimmer-end: color-mix(in srgb, var(--color-text-1) 25%, transparent); @@ -765,3 +865,49 @@ const MenuButton = styled.div` font-size: 12px; } ` + +const HeaderRow = styled.div` + display: flex; + flex-direction: row; + align-items: center; + gap: 6px; + padding-right: 10px; + margin-bottom: 5px; +` + +const HeaderIconButton = styled.div` + display: flex; + align-items: center; + justify-content: center; + width: 32px; + height: 32px; + min-width: 32px; + min-height: 32px; + border-radius: var(--list-item-border-radius); + cursor: pointer; + color: var(--color-text-2); + transition: all 0.2s; + + &:hover { + background-color: var(--color-background-mute); + color: var(--color-text-1); + } + + &.active { + color: var(--color-primary); + + &:hover { + background-color: var(--color-background-mute); + } + } +` + +const SelectIcon = styled.div` + display: flex; + align-items: center; + margin-right: 4px; + + &.disabled { + opacity: 0.5; + } +` diff --git a/src/renderer/src/pages/home/Tabs/index.tsx b/src/renderer/src/pages/home/Tabs/index.tsx index 6317ff3bca..4504550a16 100644 --- a/src/renderer/src/pages/home/Tabs/index.tsx +++ b/src/renderer/src/pages/home/Tabs/index.tsx @@ -145,6 +145,7 @@ const Container = styled.div` width: var(--assistants-width); transition: width 0.3s; height: calc(100vh - var(--navbar-height)); + position: relative; &.right { height: calc(100vh - var(--navbar-height)); From e85009fcd6cc625069b977834985a252a9781b94 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=BA=A2=E5=A5=8B=E7=8C=AB?= Date: Wed, 17 Dec 2025 17:54:44 +0800 Subject: [PATCH 7/7] feat(assistants): merge import/subscribe popups and add export to manage (#11946) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit feat(assistants): merge import and subscribe popups, add export to manage - Merge import and subscribe buttons into single unified popup - Add export functionality to manage assistant presets - Change delete mode to manage mode with both export and delete options - Show import count in success message - Default to manage mode when opening manage popup - Fix unsubscribe button to clear URL properly - Fix file import not working issue 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Opus 4.5 --- src/renderer/src/i18n/locales/en-us.json | 12 +- src/renderer/src/i18n/locales/zh-cn.json | 12 +- src/renderer/src/i18n/locales/zh-tw.json | 8 +- src/renderer/src/i18n/translate/de-de.json | 8 +- src/renderer/src/i18n/translate/el-gr.json | 8 +- src/renderer/src/i18n/translate/es-es.json | 8 +- src/renderer/src/i18n/translate/fr-fr.json | 8 +- src/renderer/src/i18n/translate/ja-jp.json | 8 +- src/renderer/src/i18n/translate/pt-pt.json | 8 +- src/renderer/src/i18n/translate/ru-ru.json | 8 +- .../presets/AssistantPresetsPage.tsx | 16 +- .../AssistantsSubscribeUrlSettings.tsx | 58 ------ .../components/ImportAssistantPresetPopup.tsx | 191 +++++++++++++----- .../ManageAssistantPresetsPopup.tsx | 55 +++-- 14 files changed, 260 insertions(+), 148 deletions(-) delete mode 100755 src/renderer/src/pages/store/assistants/presets/components/AssistantsSubscribeUrlSettings.tsx diff --git a/src/renderer/src/i18n/locales/en-us.json b/src/renderer/src/i18n/locales/en-us.json index b6f4b50cd6..f38cdc1def 100644 --- a/src/renderer/src/i18n/locales/en-us.json +++ b/src/renderer/src/i18n/locales/en-us.json @@ -472,6 +472,7 @@ "button": "Import", "error": { "fetch_failed": "Failed to fetch from URL", + "file_required": "Please select a file first", "invalid_format": "Invalid assistant format: missing required fields", "url_required": "Please enter a URL" }, @@ -486,11 +487,14 @@ }, "manage": { "batch_delete": { - "button": "Batch Delete", + "button": "Delete", "confirm": "Are you sure you want to delete the selected {{count}} assistants?" }, + "batch_export": { + "button": "Export" + }, "mode": { - "delete": "Delete", + "manage": "Manage", "sort": "Sort" }, "title": "Manage Assistants" @@ -1248,11 +1252,13 @@ } }, "stop": "Stop", + "subscribe": "Subscribe", "success": "Success", "swap": "Swap", "topics": "Topics", "unknown": "Unknown", "unnamed": "Unnamed", + "unsubscribe": "Unsubscribe", "update_success": "Update successfully", "upload_files": "Upload file", "warning": "Warning", @@ -1747,7 +1753,7 @@ "import": { "error": "Import failed" }, - "imported": "Imported successfully" + "imported": "Successfully imported {{count}} assistant(s)" }, "api": { "check": { diff --git a/src/renderer/src/i18n/locales/zh-cn.json b/src/renderer/src/i18n/locales/zh-cn.json index ceb8cad739..882b897ef5 100644 --- a/src/renderer/src/i18n/locales/zh-cn.json +++ b/src/renderer/src/i18n/locales/zh-cn.json @@ -472,6 +472,7 @@ "button": "导入", "error": { "fetch_failed": "从 URL 获取数据失败", + "file_required": "请先选择文件", "invalid_format": "无效的助手格式:缺少必填字段", "url_required": "请输入 URL" }, @@ -486,11 +487,14 @@ }, "manage": { "batch_delete": { - "button": "批量删除", + "button": "删除", "confirm": "确定要删除选中的 {{count}} 个助手吗?" }, + "batch_export": { + "button": "导出" + }, "mode": { - "delete": "删除", + "manage": "管理", "sort": "排序" }, "title": "管理助手" @@ -1248,11 +1252,13 @@ } }, "stop": "停止", + "subscribe": "订阅", "success": "成功", "swap": "交换", "topics": "话题", "unknown": "未知", "unnamed": "未命名", + "unsubscribe": "退订", "update_success": "更新成功", "upload_files": "上传文件", "warning": "警告", @@ -1747,7 +1753,7 @@ "import": { "error": "导入失败" }, - "imported": "导入成功" + "imported": "成功导入 {{count}} 个助手" }, "api": { "check": { diff --git a/src/renderer/src/i18n/locales/zh-tw.json b/src/renderer/src/i18n/locales/zh-tw.json index f150f5aef3..3feb287c1d 100644 --- a/src/renderer/src/i18n/locales/zh-tw.json +++ b/src/renderer/src/i18n/locales/zh-tw.json @@ -472,6 +472,7 @@ "button": "匯入", "error": { "fetch_failed": "從 URL 取得資料失敗", + "file_required": "請先選擇一個檔案", "invalid_format": "無效的助手格式:缺少必填欄位", "url_required": "請輸入 URL" }, @@ -489,8 +490,11 @@ "button": "批次刪除", "confirm": "確定要刪除所選的 {{count}} 個助手嗎?" }, + "batch_export": { + "button": "匯出" + }, "mode": { - "delete": "刪除", + "manage": "管理", "sort": "排序" }, "title": "管理助手" @@ -1248,11 +1252,13 @@ } }, "stop": "停止", + "subscribe": "訂閱", "success": "成功", "swap": "交換", "topics": "話題", "unknown": "未知", "unnamed": "未命名", + "unsubscribe": "取消訂閱", "update_success": "更新成功", "upload_files": "上傳檔案", "warning": "警告", diff --git a/src/renderer/src/i18n/translate/de-de.json b/src/renderer/src/i18n/translate/de-de.json index 074b53c4da..f535978606 100644 --- a/src/renderer/src/i18n/translate/de-de.json +++ b/src/renderer/src/i18n/translate/de-de.json @@ -472,6 +472,7 @@ "button": "Importieren", "error": { "fetch_failed": "Daten von URL abrufen fehlgeschlagen", + "file_required": "Bitte wählen Sie zuerst eine Datei aus", "invalid_format": "Ungültiges Assistentenformat: Pflichtfelder fehlen", "url_required": "Bitte geben Sie eine URL ein" }, @@ -489,8 +490,11 @@ "button": "Stapel löschen", "confirm": "Sind Sie sicher, dass Sie die ausgewählten {{count}} Assistenten löschen möchten?" }, + "batch_export": { + "button": "Exportieren" + }, "mode": { - "delete": "Löschen", + "manage": "Verwalten", "sort": "Sortieren" }, "title": "Assistenten verwalten" @@ -1248,11 +1252,13 @@ } }, "stop": "Stoppen", + "subscribe": "Abonnieren", "success": "Erfolgreich", "swap": "Tauschen", "topics": "Themen", "unknown": "Unbekannt", "unnamed": "Unbenannt", + "unsubscribe": "Abmelden", "update_success": "Erfolgreich aktualisiert", "upload_files": "Dateien hochladen", "warning": "Warnung", diff --git a/src/renderer/src/i18n/translate/el-gr.json b/src/renderer/src/i18n/translate/el-gr.json index 5f7b00f3be..99592e9adc 100644 --- a/src/renderer/src/i18n/translate/el-gr.json +++ b/src/renderer/src/i18n/translate/el-gr.json @@ -472,6 +472,7 @@ "button": "Εισαγωγή", "error": { "fetch_failed": "Αποτυχία λήψης δεδομένων από το URL", + "file_required": "Παρακαλώ επιλέξτε πρώτα ένα αρχείο", "invalid_format": "Μη έγκυρη μορφή βοηθού: λείπουν υποχρεωτικά πεδία", "url_required": "Παρακαλώ εισάγετε ένα URL" }, @@ -489,8 +490,11 @@ "button": "Μαζική Διαγραφή", "confirm": "Είστε βέβαιοι ότι θέλετε να διαγράψετε τους επιλεγμένους {{count}} βοηθούς;" }, + "batch_export": { + "button": "Εξαγωγή" + }, "mode": { - "delete": "Διαγραφή", + "manage": "Διαχειριστείτε", "sort": "Ταξινόμηση" }, "title": "Διαχείριση βοηθών" @@ -1248,11 +1252,13 @@ } }, "stop": "σταματήστε", + "subscribe": "Εγγραφείτε", "success": "Επιτυχία", "swap": "Εναλλαγή", "topics": "Θέματα", "unknown": "Άγνωστο", "unnamed": "Χωρίς όνομα", + "unsubscribe": "Απεγγραφή", "update_success": "Επιτυχής ενημέρωση", "upload_files": "Ανέβασμα αρχείου", "warning": "Προσοχή", diff --git a/src/renderer/src/i18n/translate/es-es.json b/src/renderer/src/i18n/translate/es-es.json index 66746875d9..31c7158587 100644 --- a/src/renderer/src/i18n/translate/es-es.json +++ b/src/renderer/src/i18n/translate/es-es.json @@ -472,6 +472,7 @@ "button": "Importar", "error": { "fetch_failed": "Error al obtener datos desde la URL", + "file_required": "Por favor, selecciona primero un archivo", "invalid_format": "Formato de asistente inválido: faltan campos obligatorios", "url_required": "Por favor introduce una URL" }, @@ -489,8 +490,11 @@ "button": "Eliminación por lotes", "confirm": "¿Estás seguro de que quieres eliminar los {{count}} asistentes seleccionados?" }, + "batch_export": { + "button": "Exportar" + }, "mode": { - "delete": "Eliminar", + "manage": "Gestionar", "sort": "Ordenar" }, "title": "Gestionar asistentes" @@ -1248,11 +1252,13 @@ } }, "stop": "Detener", + "subscribe": "Suscribirse", "success": "Éxito", "swap": "Intercambiar", "topics": "Temas", "unknown": "Desconocido", "unnamed": "Sin nombre", + "unsubscribe": "Cancelar suscripción", "update_success": "Actualización exitosa", "upload_files": "Subir archivo", "warning": "Advertencia", diff --git a/src/renderer/src/i18n/translate/fr-fr.json b/src/renderer/src/i18n/translate/fr-fr.json index 76efea8e3d..da1d297a7f 100644 --- a/src/renderer/src/i18n/translate/fr-fr.json +++ b/src/renderer/src/i18n/translate/fr-fr.json @@ -472,6 +472,7 @@ "button": "Importer", "error": { "fetch_failed": "Échec de la récupération des données depuis l'URL", + "file_required": "Veuillez d'abord sélectionner un fichier", "invalid_format": "Format d'assistant invalide : champs obligatoires manquants", "url_required": "Veuillez saisir une URL" }, @@ -489,8 +490,11 @@ "button": "Suppression par lot", "confirm": "Êtes-vous sûr de vouloir supprimer les {{count}} assistants sélectionnés ?" }, + "batch_export": { + "button": "Exporter" + }, "mode": { - "delete": "Supprimer", + "manage": "Gérer", "sort": "Trier" }, "title": "Gérer les assistants" @@ -1248,11 +1252,13 @@ } }, "stop": "Arrêter", + "subscribe": "S'abonner", "success": "Succès", "swap": "Échanger", "topics": "Sujets", "unknown": "Inconnu", "unnamed": "Sans nom", + "unsubscribe": "Se désabonner", "update_success": "Mise à jour réussie", "upload_files": "Uploader des fichiers", "warning": "Avertissement", diff --git a/src/renderer/src/i18n/translate/ja-jp.json b/src/renderer/src/i18n/translate/ja-jp.json index d9e62b6229..93bacf506c 100644 --- a/src/renderer/src/i18n/translate/ja-jp.json +++ b/src/renderer/src/i18n/translate/ja-jp.json @@ -472,6 +472,7 @@ "button": "インポート", "error": { "fetch_failed": "URLからのデータ取得に失敗しました", + "file_required": "まずファイルを選択してください", "invalid_format": "無効なアシスタント形式:必須フィールドが不足しています", "url_required": "URLを入力してください" }, @@ -489,8 +490,11 @@ "button": "バッチ削除", "confirm": "選択した{{count}}件のアシスタントを削除してもよろしいですか?" }, + "batch_export": { + "button": "エクスポート" + }, "mode": { - "delete": "削除", + "manage": "管理", "sort": "並べ替え" }, "title": "アシスタントを管理" @@ -1248,11 +1252,13 @@ } }, "stop": "停止", + "subscribe": "購読", "success": "成功", "swap": "交換", "topics": "トピック", "unknown": "Unknown", "unnamed": "無題", + "unsubscribe": "配信停止", "update_success": "更新成功", "upload_files": "ファイルをアップロードする", "warning": "警告", diff --git a/src/renderer/src/i18n/translate/pt-pt.json b/src/renderer/src/i18n/translate/pt-pt.json index 69c2ae2609..9bd6881673 100644 --- a/src/renderer/src/i18n/translate/pt-pt.json +++ b/src/renderer/src/i18n/translate/pt-pt.json @@ -472,6 +472,7 @@ "button": "Importar", "error": { "fetch_failed": "Falha ao obter dados do URL", + "file_required": "Por favor, selecione um arquivo primeiro", "invalid_format": "Formato de assistente inválido: campos obrigatórios em falta", "url_required": "Por favor insere um URL" }, @@ -489,8 +490,11 @@ "button": "Exclusão em Lote", "confirm": "Tem certeza de que deseja excluir os {{count}} assistentes selecionados?" }, + "batch_export": { + "button": "Exportar" + }, "mode": { - "delete": "Excluir", + "manage": "Gerenciar", "sort": "Ordenar" }, "title": "Gerir assistentes" @@ -1248,11 +1252,13 @@ } }, "stop": "Parar", + "subscribe": "Subscrever", "success": "Sucesso", "swap": "Trocar", "topics": "Tópicos", "unknown": "Desconhecido", "unnamed": "Sem nome", + "unsubscribe": "Cancelar inscrição", "update_success": "Atualização bem-sucedida", "upload_files": "Carregar arquivo", "warning": "Aviso", diff --git a/src/renderer/src/i18n/translate/ru-ru.json b/src/renderer/src/i18n/translate/ru-ru.json index 8ef955addd..7665115d5c 100644 --- a/src/renderer/src/i18n/translate/ru-ru.json +++ b/src/renderer/src/i18n/translate/ru-ru.json @@ -472,6 +472,7 @@ "button": "Импортировать", "error": { "fetch_failed": "Ошибка получения данных с URL", + "file_required": "Сначала выберите файл", "invalid_format": "Неверный формат помощника: отсутствуют обязательные поля", "url_required": "Пожалуйста, введите URL" }, @@ -489,8 +490,11 @@ "button": "Массовое удаление", "confirm": "Вы уверены, что хотите удалить выбранных {{count}} ассистентов?" }, + "batch_export": { + "button": "Экспорт" + }, "mode": { - "delete": "Удалить", + "manage": "Управлять", "sort": "Сортировать" }, "title": "Управление помощниками" @@ -1248,11 +1252,13 @@ } }, "stop": "остановить", + "subscribe": "Подписаться", "success": "Успешно", "swap": "Поменять местами", "topics": "Топики", "unknown": "Неизвестно", "unnamed": "Без имени", + "unsubscribe": "Отписаться", "update_success": "Обновление выполнено успешно", "upload_files": "Загрузить файл", "warning": "Предупреждение", diff --git a/src/renderer/src/pages/store/assistants/presets/AssistantPresetsPage.tsx b/src/renderer/src/pages/store/assistants/presets/AssistantPresetsPage.tsx index 79d1275b10..a56c04b152 100644 --- a/src/renderer/src/pages/store/assistants/presets/AssistantPresetsPage.tsx +++ b/src/renderer/src/pages/store/assistants/presets/AssistantPresetsPage.tsx @@ -1,7 +1,6 @@ import { Navbar, NavbarCenter } from '@renderer/components/app/Navbar' import { HStack } from '@renderer/components/Layout' import ListItem from '@renderer/components/ListItem' -import GeneralPopup from '@renderer/components/Popups/GeneralPopup' import Scrollbar from '@renderer/components/Scrollbar' import CustomTag from '@renderer/components/Tags/CustomTag' import { useAssistantPresets } from '@renderer/hooks/useAssistantPresets' @@ -11,7 +10,7 @@ import type { AssistantPreset } from '@renderer/types' import { uuid } from '@renderer/utils' import { Button, Empty, Flex, Input } from 'antd' import { omit } from 'lodash' -import { Import, Plus, Rss, Search, Settings2 } from 'lucide-react' +import { Import, Plus, Search, Settings2 } from 'lucide-react' import type { FC } from 'react' import { useCallback, useEffect, useMemo, useState } from 'react' import { useTranslation } from 'react-i18next' @@ -23,7 +22,6 @@ import { groupTranslations } from './assistantPresetGroupTranslations' import AddAssistantPresetPopup from './components/AddAssistantPresetPopup' import AssistantPresetCard from './components/AssistantPresetCard' import { AssistantPresetGroupIcon } from './components/AssistantPresetGroupIcon' -import AssistantsSubscribeUrlSettings from './components/AssistantsSubscribeUrlSettings' import ImportAssistantPresetPopup from './components/ImportAssistantPresetPopup' import ManageAssistantPresetsPopup from './components/ManageAssistantPresetsPopup' @@ -177,15 +175,6 @@ const AssistantPresetsPage: FC = () => { } } - const handleSubscribeSettings = () => { - GeneralPopup.show({ - title: t('assistants.presets.settings.title'), - content: , - footer: null, - width: 600 - }) - } - const handleManageAgents = () => { ManageAssistantPresetsPopup.show() } @@ -292,9 +281,6 @@ const AssistantPresetsPage: FC = () => { - diff --git a/src/renderer/src/pages/store/assistants/presets/components/AssistantsSubscribeUrlSettings.tsx b/src/renderer/src/pages/store/assistants/presets/components/AssistantsSubscribeUrlSettings.tsx deleted file mode 100755 index 8ea3b92fde..0000000000 --- a/src/renderer/src/pages/store/assistants/presets/components/AssistantsSubscribeUrlSettings.tsx +++ /dev/null @@ -1,58 +0,0 @@ -import { HStack } from '@renderer/components/Layout' -import { useTheme } from '@renderer/context/ThemeProvider' -import { useSettings } from '@renderer/hooks/useSettings' -import { SettingDivider, SettingGroup, SettingRow, SettingRowTitle, SettingTitle } from '@renderer/pages/settings' -import { useAppDispatch } from '@renderer/store' -import { setAgentssubscribeUrl } from '@renderer/store/settings' -import Input from 'antd/es/input/Input' -import { HelpCircle } from 'lucide-react' -import type { FC } from 'react' -import { useTranslation } from 'react-i18next' - -const AssistantsSubscribeUrlSettings: FC = () => { - const { t } = useTranslation() - const { theme } = useTheme() - const dispatch = useAppDispatch() - - const { agentssubscribeUrl } = useSettings() - - const handleAgentChange = (e: React.ChangeEvent) => { - dispatch(setAgentssubscribeUrl(e.target.value)) - } - - const handleHelpClick = () => { - window.open('https://docs.cherry-ai.com/data-settings/assistants-subscribe', '_blank') - } - - return ( - - - - {t('assistants.presets.tag.agent')} - {t('settings.tool.websearch.subscribe_add')} - - - - - - {t('settings.tool.websearch.subscribe_url')} - - - - - - ) -} - -export default AssistantsSubscribeUrlSettings diff --git a/src/renderer/src/pages/store/assistants/presets/components/ImportAssistantPresetPopup.tsx b/src/renderer/src/pages/store/assistants/presets/components/ImportAssistantPresetPopup.tsx index 9a76d1d02e..37b7938248 100644 --- a/src/renderer/src/pages/store/assistants/presets/components/ImportAssistantPresetPopup.tsx +++ b/src/renderer/src/pages/store/assistants/presets/components/ImportAssistantPresetPopup.tsx @@ -1,11 +1,15 @@ import { TopView } from '@renderer/components/TopView' import { useAssistantPresets } from '@renderer/hooks/useAssistantPresets' +import { useSettings } from '@renderer/hooks/useSettings' import { useTimer } from '@renderer/hooks/useTimer' import { getDefaultModel } from '@renderer/services/AssistantService' import { EVENT_NAMES, EventEmitter } from '@renderer/services/EventService' +import { useAppDispatch } from '@renderer/store' +import { setAgentssubscribeUrl } from '@renderer/store/settings' import type { AssistantPreset } from '@renderer/types' import { uuid } from '@renderer/utils' -import { Button, Flex, Form, Input, Modal, Radio } from 'antd' +import { Button, Divider, Flex, Form, Input, Modal, Radio, Typography } from 'antd' +import { HelpCircle } from 'lucide-react' import { useState } from 'react' import { useTranslation } from 'react-i18next' @@ -20,35 +24,53 @@ const PopupContainer: React.FC = ({ resolve }) => { const { addAssistantPreset } = useAssistantPresets() const [importType, setImportType] = useState<'url' | 'file'>('url') const [loading, setLoading] = useState(false) + const [subscribeLoading, setSubscribeLoading] = useState(false) const { setTimeoutTimer } = useTimer() + const dispatch = useAppDispatch() + const { agentssubscribeUrl } = useSettings() + const [subscribeUrl, setSubscribeUrl] = useState(agentssubscribeUrl || '') + const [selectedFile, setSelectedFile] = useState<{ name: string; content: Uint8Array } | null>(null) + const [urlValue, setUrlValue] = useState('') + + const isImportDisabled = importType === 'url' ? !urlValue.trim() : !selectedFile + const isSubscribed = !!agentssubscribeUrl + + const handleSelectFile = async () => { + const result = await window.api.file.open({ + filters: [{ name: t('assistants.presets.import.file_filter'), extensions: ['json'] }] + }) + + if (result) { + setSelectedFile({ name: result.fileName, content: result.content }) + } + } + + const onFinish = async () => { + // Validate before setting loading + if (importType === 'url' && !urlValue.trim()) { + window.toast.error(t('assistants.presets.import.error.url_required')) + return + } + if (importType === 'file' && !selectedFile) { + window.toast.error(t('assistants.presets.import.error.file_required')) + return + } - const onFinish = async (values: { url?: string }) => { setLoading(true) try { let presets: AssistantPreset[] = [] if (importType === 'url') { - if (!values.url) { - throw new Error(t('assistants.presets.import.error.url_required')) - } - const response = await fetch(values.url) + const response = await fetch(urlValue.trim()) if (!response.ok) { throw new Error(t('assistants.presets.import.error.fetch_failed')) } const data = await response.json() presets = Array.isArray(data) ? data : [data] } else { - const result = await window.api.file.open({ - filters: [{ name: t('assistants.presets.import.file_filter'), extensions: ['json'] }] - }) - - if (result) { - presets = JSON.parse(new TextDecoder('utf-8').decode(result.content)) - if (!Array.isArray(presets)) { - presets = [presets] - } - } else { - return + presets = JSON.parse(new TextDecoder('utf-8').decode(selectedFile!.content)) + if (!Array.isArray(presets)) { + presets = [presets] } } @@ -74,7 +96,7 @@ const PopupContainer: React.FC = ({ resolve }) => { addAssistantPreset(newPreset) } - window.toast.success(t('message.agents.imported')) + window.toast.success(t('message.agents.imported', { count: presets.length })) setTimeoutTimer('onFinish', () => EventEmitter.emit(EVENT_NAMES.SHOW_ASSISTANTS), 0) setOpen(false) @@ -88,7 +110,42 @@ const PopupContainer: React.FC = ({ resolve }) => { const onCancel = () => { setOpen(false) - resolve(null) + } + + const handleSubscribeUrlChange = (e: React.ChangeEvent) => { + setSubscribeUrl(e.target.value) + } + + const handleSubscribe = async () => { + // If already subscribed, unsubscribe + if (isSubscribed) { + dispatch(setAgentssubscribeUrl('')) + setSubscribeUrl('') + window.location.reload() + return + } + + if (!subscribeUrl.trim()) { + return + } + + setSubscribeLoading(true) + try { + const response = await fetch(subscribeUrl) + if (!response.ok) { + throw new Error(t('assistants.presets.import.error.fetch_failed')) + } + dispatch(setAgentssubscribeUrl(subscribeUrl)) + window.location.reload() + } catch (error) { + window.toast.error(error instanceof Error ? error.message : t('message.agents.import.error')) + } finally { + setSubscribeLoading(false) + } + } + + const handleHelpClick = () => { + window.open('https://docs.cherry-ai.com/data-settings/assistants-subscribe', '_blank') } return ( @@ -96,39 +153,79 @@ const PopupContainer: React.FC = ({ resolve }) => { title={t('assistants.presets.import.title')} open={open} onCancel={onCancel} - maskClosable={false} - footer={ - - - - - } + afterClose={() => resolve(null)} + footer={null} transitionName="animation-move-down" + styles={{ body: { padding: '16px' } }} centered>
- - setImportType(e.target.value)}> - {t('assistants.presets.import.type.url')} - {t('assistants.presets.import.type.file')} - + + + setImportType(e.target.value)}> + {t('assistants.presets.import.type.url')} + {t('assistants.presets.import.type.file')} + + + {importType === 'url' && ( + + setUrlValue(e.target.value)} + /> + + )} + + {importType === 'file' && ( + <> + + {selectedFile && ( + + {selectedFile.name} + + )} +
+ + )} + + + - - {importType === 'url' && ( - - - - )} - - {importType === 'file' && ( - - - - )} + + + + + + {t('assistants.presets.tag.agent')} + {t('settings.tool.websearch.subscribe_add')} + + + + + + + + ) } diff --git a/src/renderer/src/pages/store/assistants/presets/components/ManageAssistantPresetsPopup.tsx b/src/renderer/src/pages/store/assistants/presets/components/ManageAssistantPresetsPopup.tsx index b75a569c5f..961ec24abe 100644 --- a/src/renderer/src/pages/store/assistants/presets/components/ManageAssistantPresetsPopup.tsx +++ b/src/renderer/src/pages/store/assistants/presets/components/ManageAssistantPresetsPopup.tsx @@ -1,4 +1,4 @@ -import { MenuOutlined } from '@ant-design/icons' +import { ExportOutlined, MenuOutlined } from '@ant-design/icons' import { DraggableList } from '@renderer/components/DraggableList' import { DeleteIcon } from '@renderer/components/Icons' import { Box, HStack } from '@renderer/components/Layout' @@ -10,13 +10,13 @@ import { useState } from 'react' import { useTranslation } from 'react-i18next' import styled from 'styled-components' -type Mode = 'sort' | 'delete' +type Mode = 'sort' | 'manage' const PopupContainer: React.FC = () => { const [open, setOpen] = useState(true) const { t } = useTranslation() const { presets, setAssistantPresets } = useAssistantPresets() - const [mode, setMode] = useState(() => (presets.length > 50 ? 'delete' : 'sort')) + const [mode, setMode] = useState('manage') const [selectedIds, setSelectedIds] = useState>(new Set()) const onCancel = () => { @@ -88,6 +88,23 @@ const PopupContainer: React.FC = () => { }) } + const handleBatchExport = async () => { + if (selectedIds.size === 0) return + + const selectedPresets = presets.filter((p) => selectedIds.has(p.id)) + const exportData = selectedPresets.map((p) => ({ + name: p.name, + emoji: p.emoji, + prompt: p.prompt, + description: p.description, + group: p.group + })) + + const fileName = selectedIds.size === 1 ? `${selectedPresets[0].name}.json` : `assistants_${selectedIds.size}.json` + + await window.api.file.save(fileName, JSON.stringify(exportData, null, 2)) + } + const isAllSelected = presets.length > 0 && selectedIds.size === presets.length const isIndeterminate = selectedIds.size > 0 && selectedIds.size < presets.length @@ -98,13 +115,14 @@ const PopupContainer: React.FC = () => { onCancel={onCancel} afterClose={onClose} footer={null} + width={600} transitionName="animation-move-down" centered> {presets.length > 0 && ( <> - {mode === 'delete' ? ( + {mode === 'manage' ? ( {t('common.select_all')} @@ -119,15 +137,24 @@ const PopupContainer: React.FC = () => {
)} - {mode === 'delete' && ( - + {mode === 'manage' && ( + <> + + + )} { onChange={(value) => handleModeChange(value as Mode)} options={[ { label: t('assistants.presets.manage.mode.sort'), value: 'sort' }, - { label: t('assistants.presets.manage.mode.delete'), value: 'delete' } + { label: t('assistants.presets.manage.mode.manage'), value: 'manage' } ]} />