From e5ded81d9bdd68e7abb44e012fa7342ec70417ba Mon Sep 17 00:00:00 2001 From: suyao Date: Thu, 22 May 2025 23:20:25 +0800 Subject: [PATCH 01/54] feat: gemini thinking summary support - Removed unnecessary content accumulation and streamlined chunk processing logic. - Introduced distinct handling for 'thinking' and 'text' parts, ensuring accurate onChunk calls for both types. - Enhanced timing tracking for reasoning content, improving overall responsiveness in streaming scenarios. --- .../providers/AiProvider/GeminiProvider.ts | 50 +++++++++++++------ 1 file changed, 35 insertions(+), 15 deletions(-) diff --git a/src/renderer/src/providers/AiProvider/GeminiProvider.ts b/src/renderer/src/providers/AiProvider/GeminiProvider.ts index c6a1689c03..35c8654d32 100644 --- a/src/renderer/src/providers/AiProvider/GeminiProvider.ts +++ b/src/renderer/src/providers/AiProvider/GeminiProvider.ts @@ -508,7 +508,6 @@ export default class GeminiProvider extends BaseProvider { let time_first_token_millsec = 0 if (stream instanceof GenerateContentResponse) { - let content = '' const time_completion_millsec = new Date().getTime() - start_time_millsec const toolResults: Awaited> = [] @@ -523,16 +522,18 @@ export default class GeminiProvider extends BaseProvider { if (part.functionCall) { functionCalls.push(part.functionCall) } - if (part.text) { - content += part.text - onChunk({ type: ChunkType.TEXT_DELTA, text: part.text }) + const text = part.text || '' + if (part.thought) { + onChunk({ type: ChunkType.THINKING_DELTA, text }) + onChunk({ type: ChunkType.THINKING_COMPLETE, text }) + } else if (part.text) { + onChunk({ type: ChunkType.TEXT_DELTA, text }) + onChunk({ type: ChunkType.TEXT_COMPLETE, text }) } }) } }) - if (content.length) { - onChunk({ type: ChunkType.TEXT_COMPLETE, text: content }) - } + if (functionCalls.length) { toolResults.push(...(await processToolCalls(functionCalls))) } @@ -565,16 +566,35 @@ export default class GeminiProvider extends BaseProvider { } as BlockCompleteChunk) } else { let content = '' + let thinkingContent = '' for await (const chunk of stream) { if (window.keyv.get(EVENT_NAMES.CHAT_COMPLETION_PAUSED)) break - if (time_first_token_millsec == 0) { - time_first_token_millsec = new Date().getTime() - } - - if (chunk.text !== undefined) { - content += chunk.text - onChunk({ type: ChunkType.TEXT_DELTA, text: chunk.text }) + if (chunk.candidates?.[0]?.content?.parts && chunk.candidates[0].content.parts.length > 0) { + const parts = chunk.candidates[0].content.parts + for (const part of parts) { + if (!part.text) { + continue + } else if (part.thought) { + if (time_first_token_millsec === 0) { + time_first_token_millsec = new Date().getTime() + } + thinkingContent += part.text + onChunk({ type: ChunkType.THINKING_DELTA, text: part.text || '' }) + } else { + if (time_first_token_millsec == 0) { + time_first_token_millsec = new Date().getTime() + } else { + onChunk({ + type: ChunkType.THINKING_COMPLETE, + text: thinkingContent, + thinking_millsec: new Date().getTime() - time_first_token_millsec + }) + } + content += part.text + onChunk({ type: ChunkType.TEXT_DELTA, text: part.text }) + } + } } if (chunk.candidates?.[0]?.finishReason) { @@ -643,6 +663,7 @@ export default class GeminiProvider extends BaseProvider { const start_time_millsec = new Date().getTime() if (!streamOutput) { + onChunk({ type: ChunkType.LLM_RESPONSE_CREATED }) const response = await chat.sendMessage({ message: messageContents as PartUnion, config: { @@ -650,7 +671,6 @@ export default class GeminiProvider extends BaseProvider { abortSignal: abortController.signal } }) - onChunk({ type: ChunkType.LLM_RESPONSE_CREATED }) return await processStream(response, 0).then(cleanup) } From 178d164ff93b94761f9b32b4f6047bd695eb3061 Mon Sep 17 00:00:00 2001 From: kangfenmao Date: Fri, 23 May 2025 11:17:12 +0800 Subject: [PATCH 02/54] chore(version): 1.3.10 --- electron-builder.yml | 13 ++++++------- package.json | 2 +- 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/electron-builder.yml b/electron-builder.yml index 294335c36a..a233a2f9a1 100644 --- a/electron-builder.yml +++ b/electron-builder.yml @@ -94,10 +94,9 @@ artifactBuildCompleted: scripts/artifact-build-completed.js releaseInfo: releaseNotes: | ⚠️ 注意:升级前请备份数据,否则将无法降级 - 增加消息通知功能 - 增加 Google 小程序 - MCP 支持运行 Python 代码 - 修复 MCP SSE 连接问题 - 修复消息编辑和消息多选相关问题 - 修复消息显示问题 - 修复话题提示词无效问题 + 增加 Claude 4 模型支持 + Grok 模型增加联网能力 + 修复无法搜索历史消息问题 + 修复 MCP 代理问题 + 修复精简备份恢复覆盖文件问题 + 修复@模型回复插入位置错误问题 diff --git a/package.json b/package.json index c71a8b5370..fe677754ab 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "CherryStudio", - "version": "1.3.9", + "version": "1.3.10", "private": true, "description": "A powerful AI assistant for producer.", "main": "./out/main/index.js", From ee042e11f1480399455c1300e40c7ab5cacb9ff7 Mon Sep 17 00:00:00 2001 From: karl Date: Fri, 23 May 2025 10:15:00 +0800 Subject: [PATCH 03/54] fix: editing user messages is not re-sent; it can only be saved#6327 --- src/renderer/src/pages/home/Messages/MessageEditor.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/renderer/src/pages/home/Messages/MessageEditor.tsx b/src/renderer/src/pages/home/Messages/MessageEditor.tsx index e2810e4aa2..827a078e61 100644 --- a/src/renderer/src/pages/home/Messages/MessageEditor.tsx +++ b/src/renderer/src/pages/home/Messages/MessageEditor.tsx @@ -255,7 +255,7 @@ const MessageBlockEditor: FC = ({ message, onSave, onResend, onCancel }) - {message.role === 'assistant' && ( + {message.role === 'user' && ( handleClick(true)}> From 631fa3a42a010f05d3f0d9cab04544e5c73a52c3 Mon Sep 17 00:00:00 2001 From: jtsang4 Date: Tue, 20 May 2025 07:36:50 +0800 Subject: [PATCH 04/54] feat: support pin topic to the top Signed-off-by: jtsang4 --- src/renderer/src/hooks/useSettings.ts | 4 ++++ src/renderer/src/i18n/locales/en-us.json | 1 + src/renderer/src/i18n/locales/ja-jp.json | 1 + src/renderer/src/i18n/locales/ru-ru.json | 1 + src/renderer/src/i18n/locales/zh-cn.json | 1 + src/renderer/src/i18n/locales/zh-tw.json | 1 + src/renderer/src/pages/home/Tabs/TopicsTab.tsx | 16 ++++++++++++++-- .../settings/DisplaySettings/DisplaySettings.tsx | 7 +++++++ src/renderer/src/store/settings.ts | 6 ++++++ 9 files changed, 36 insertions(+), 2 deletions(-) diff --git a/src/renderer/src/hooks/useSettings.ts b/src/renderer/src/hooks/useSettings.ts index ba6185aba9..5eea046d00 100644 --- a/src/renderer/src/hooks/useSettings.ts +++ b/src/renderer/src/hooks/useSettings.ts @@ -12,6 +12,7 @@ import { setTheme, SettingsState, setTopicPosition, + setPinTopicsToTop, setTray as _setTray, setTrayOnClose, setWindowStyle @@ -68,6 +69,9 @@ export function useSettings() { setTopicPosition(topicPosition: 'left' | 'right') { dispatch(setTopicPosition(topicPosition)) }, + setPinTopicsToTop(pinTopicsToTop: boolean) { + dispatch(setPinTopicsToTop(pinTopicsToTop)) + }, updateSidebarIcons(icons: { visible: SidebarIcon[]; disabled: SidebarIcon[] }) { dispatch(setSidebarIcons(icons)) }, diff --git a/src/renderer/src/i18n/locales/en-us.json b/src/renderer/src/i18n/locales/en-us.json index b6b742fd85..578032003c 100644 --- a/src/renderer/src/i18n/locales/en-us.json +++ b/src/renderer/src/i18n/locales/en-us.json @@ -1635,6 +1635,7 @@ "topic.position.left": "Left", "topic.position.right": "Right", "topic.show.time": "Show topic time", + "topic.pin_to_top": "Pin Topics to Top", "tray.onclose": "Minimize to Tray on Close", "tray.show": "Show Tray Icon", "tray.title": "Tray", diff --git a/src/renderer/src/i18n/locales/ja-jp.json b/src/renderer/src/i18n/locales/ja-jp.json index 42a7564d06..38abc69bc3 100644 --- a/src/renderer/src/i18n/locales/ja-jp.json +++ b/src/renderer/src/i18n/locales/ja-jp.json @@ -1626,6 +1626,7 @@ "topic.position.left": "左", "topic.position.right": "右", "topic.show.time": "トピックの時間を表示", + "topic.pin_to_top": "固定トピックを上部に表示", "tray.onclose": "閉じるときにトレイに最小化", "tray.show": "トレイアイコンを表示", "tray.title": "トレイ", diff --git a/src/renderer/src/i18n/locales/ru-ru.json b/src/renderer/src/i18n/locales/ru-ru.json index 28aacde678..2ca59e4007 100644 --- a/src/renderer/src/i18n/locales/ru-ru.json +++ b/src/renderer/src/i18n/locales/ru-ru.json @@ -1626,6 +1626,7 @@ "topic.position.left": "Слева", "topic.position.right": "Справа", "topic.show.time": "Показывать время топика", + "topic.pin_to_top": "Закрепленные топики сверху", "tray.onclose": "Свернуть в трей при закрытии", "tray.show": "Показать значок в трее", "tray.title": "Трей", diff --git a/src/renderer/src/i18n/locales/zh-cn.json b/src/renderer/src/i18n/locales/zh-cn.json index 89fac6e56e..759302c82d 100644 --- a/src/renderer/src/i18n/locales/zh-cn.json +++ b/src/renderer/src/i18n/locales/zh-cn.json @@ -1635,6 +1635,7 @@ "topic.position.left": "左侧", "topic.position.right": "右侧", "topic.show.time": "显示话题时间", + "topic.pin_to_top": "固定话题置顶", "tray.onclose": "关闭时最小化到托盘", "tray.show": "显示托盘图标", "tray.title": "托盘", diff --git a/src/renderer/src/i18n/locales/zh-tw.json b/src/renderer/src/i18n/locales/zh-tw.json index 3b5650f5a2..ceaf666452 100644 --- a/src/renderer/src/i18n/locales/zh-tw.json +++ b/src/renderer/src/i18n/locales/zh-tw.json @@ -1629,6 +1629,7 @@ "topic.position.left": "左側", "topic.position.right": "右側", "topic.show.time": "顯示話題時間", + "topic.pin_to_top": "固定話題置頂", "tray.onclose": "關閉時最小化到系统匣", "tray.show": "顯示系统匣圖示", "tray.title": "系统匣", diff --git a/src/renderer/src/pages/home/Tabs/TopicsTab.tsx b/src/renderer/src/pages/home/Tabs/TopicsTab.tsx index 2df5fb5db8..f53989be0c 100644 --- a/src/renderer/src/pages/home/Tabs/TopicsTab.tsx +++ b/src/renderer/src/pages/home/Tabs/TopicsTab.tsx @@ -54,7 +54,7 @@ const Topics: FC = ({ assistant: _assistant, activeTopic, setActiveTopic const { assistants } = useAssistants() const { assistant, removeTopic, moveTopic, updateTopic, updateTopics } = useAssistant(_assistant.id) const { t } = useTranslation() - const { showTopicTime, topicPosition } = useSettings() + const { showTopicTime, topicPosition, pinTopicsToTop } = useSettings() const borderRadius = showTopicTime ? 12 : 'var(--list-item-border-radius)' @@ -380,10 +380,22 @@ const Topics: FC = ({ assistant: _assistant, activeTopic, setActiveTopic targetTopic ]) + // Sort topics based on pinned status if pinTopicsToTop is enabled + const sortedTopics = useMemo(() => { + if (pinTopicsToTop) { + return [...assistant.topics].sort((a, b) => { + if (a.pinned && !b.pinned) return -1 + if (!a.pinned && b.pinned) return 1 + return 0 + }) + } + return assistant.topics + }, [assistant.topics, pinTopicsToTop]) + return ( - + {(topic) => { const isActive = topic.id === activeTopic?.id const topicName = topic.name.replace('`', '') diff --git a/src/renderer/src/pages/settings/DisplaySettings/DisplaySettings.tsx b/src/renderer/src/pages/settings/DisplaySettings/DisplaySettings.tsx index d58cf0c01a..b48633abba 100644 --- a/src/renderer/src/pages/settings/DisplaySettings/DisplaySettings.tsx +++ b/src/renderer/src/pages/settings/DisplaySettings/DisplaySettings.tsx @@ -9,6 +9,7 @@ import { setAssistantIconType, setClickAssistantToShowTopic, setCustomCss, + setPinTopicsToTop, setShowTopicTime, setSidebarIcons } from '@renderer/store/settings' @@ -32,6 +33,7 @@ const DisplaySettings: FC = () => { setTopicPosition, clickAssistantToShowTopic, showTopicTime, + pinTopicsToTop, customCss, sidebarIcons, assistantIconType @@ -189,6 +191,11 @@ const DisplaySettings: FC = () => { {t('settings.topic.show.time')} dispatch(setShowTopicTime(checked))} /> + + + {t('settings.topic.pin_to_top')} + dispatch(setPinTopicsToTop(checked))} /> + {t('settings.display.assistant.title')} diff --git a/src/renderer/src/store/settings.ts b/src/renderer/src/store/settings.ts index cda007c7f8..a089a6a6f3 100644 --- a/src/renderer/src/store/settings.ts +++ b/src/renderer/src/store/settings.ts @@ -52,6 +52,7 @@ export interface SettingsState { fontSize: number topicPosition: 'left' | 'right' showTopicTime: boolean + pinTopicsToTop: boolean assistantIconType: AssistantIconType pasteLongTextAsFile: boolean pasteLongTextThreshold: number @@ -192,6 +193,7 @@ export const initialState: SettingsState = { fontSize: 14, topicPosition: 'left', showTopicTime: false, + pinTopicsToTop: false, assistantIconType: 'emoji', pasteLongTextAsFile: false, pasteLongTextThreshold: 1500, @@ -375,6 +377,9 @@ const settingsSlice = createSlice({ setShowTopicTime: (state, action: PayloadAction) => { state.showTopicTime = action.payload }, + setPinTopicsToTop: (state, action: PayloadAction) => { + state.pinTopicsToTop = action.payload + }, setAssistantIconType: (state, action: PayloadAction) => { state.assistantIconType = action.payload }, @@ -655,6 +660,7 @@ export const { setWindowStyle, setTopicPosition, setShowTopicTime, + setPinTopicsToTop, setAssistantIconType, setPasteLongTextAsFile, setAutoCheckUpdate, From e21e0d238e6785ecbf6ca44f807085fa18dc0241 Mon Sep 17 00:00:00 2001 From: kangfenmao Date: Fri, 23 May 2025 13:52:46 +0800 Subject: [PATCH 05/54] fix: minapp search error --- .../src/components/Popups/MinAppsPopover.tsx | 1 - src/renderer/src/i18n/locales/zh-cn.json | 52 ++-- src/renderer/src/i18n/locales/zh-tw.json | 68 ++--- src/renderer/src/pages/apps/App.tsx | 274 ++++-------------- src/renderer/src/pages/apps/AppsPage.tsx | 21 +- src/renderer/src/pages/apps/NewAppButton.tsx | 197 +++++++++++++ 6 files changed, 318 insertions(+), 295 deletions(-) create mode 100644 src/renderer/src/pages/apps/NewAppButton.tsx diff --git a/src/renderer/src/components/Popups/MinAppsPopover.tsx b/src/renderer/src/components/Popups/MinAppsPopover.tsx index effdf1189c..28a9621bc9 100644 --- a/src/renderer/src/components/Popups/MinAppsPopover.tsx +++ b/src/renderer/src/components/Popups/MinAppsPopover.tsx @@ -51,7 +51,6 @@ const MinAppsPopover: FC = ({ children }) => { )} - ) diff --git a/src/renderer/src/i18n/locales/zh-cn.json b/src/renderer/src/i18n/locales/zh-cn.json index 759302c82d..782c4943b4 100644 --- a/src/renderer/src/i18n/locales/zh-cn.json +++ b/src/renderer/src/i18n/locales/zh-cn.json @@ -100,7 +100,7 @@ "titleLabel": "标题", "titlePlaceholder": "输入标题", "contentLabel": "内容", - "contentPlaceholder": "请输入短语内容,支持使用变量,然后按Tab键可以快速定位到变量进行修改。比如:\n帮我规划从${from}到${to}的路线,然后发送到${email}。" + "contentPlaceholder": "请输入短语内容,支持使用变量,然后按Tab键可以快速定位到变量进行修改。比如:\n帮我规划从${from}到${to}的路线,然后发送到${email}" } }, "auth": { @@ -113,7 +113,7 @@ "backup": { "confirm": "确定要备份数据吗?", "confirm.button": "选择备份位置", - "content": "备份全部数据,包括聊天记录、设置、知识库等所有数据。请注意,备份过程可能需要一些时间,感谢您的耐心等待。", + "content": "备份全部数据,包括聊天记录、设置、知识库等所有数据。请注意,备份过程可能需要一些时间,感谢您的耐心等待", "progress": { "completed": "备份完成", "compressing": "压缩文件...", @@ -144,7 +144,7 @@ "artifacts.preview.openExternal.error.content": "外部浏览器打开出错", "assistant.search.placeholder": "搜索", "deeply_thought": "已深度思考(用时 {{seconds}} 秒)", - "default.description": "你好,我是默认助手。你可以立刻开始跟我聊天。", + "default.description": "你好,我是默认助手。你可以立刻开始跟我聊天", "default.name": "默认助手", "default.topic.name": "默认话题", "history": { @@ -238,7 +238,7 @@ "settings.code_cacheable": "代码块缓存", "settings.code_cacheable.tip": "缓存代码块可以减少长代码块的渲染时间,但会增加内存占用", "settings.code_cache_max_size": "缓存上限", - "settings.code_cache_max_size.tip": "允许缓存的字符数上限(千字符),按照高亮后的代码计算。高亮后的代码长度相比于纯文本会长很多。", + "settings.code_cache_max_size.tip": "允许缓存的字符数上限(千字符),按照高亮后的代码计算。高亮后的代码长度相比于纯文本会长很多", "settings.code_cache_ttl": "缓存期限", "settings.code_cache_ttl.tip": "缓存过期时间(分钟)", "settings.code_cache_threshold": "缓存阈值", @@ -942,7 +942,7 @@ "restore": { "confirm": "确定要恢复数据吗?", "confirm.button": "选择备份文件", - "content": "恢复操作将使用备份数据覆盖当前所有应用数据。请注意,恢复过程可能需要一些时间,感谢您的耐心等待。", + "content": "恢复操作将使用备份数据覆盖当前所有应用数据。请注意,恢复过程可能需要一些时间,感谢您的耐心等待", "progress": { "completed": "恢复完成", "copying_files": "复制文件... {{progress}}%", @@ -995,7 +995,7 @@ "app_knowledge.remove_all_success": "文件删除成功", "app_logs": "应用日志", "backup.skip_file_data_title": "精简备份", - "backup.skip_file_data_help": "备份时跳过备份图片、知识库等数据文件,仅备份聊天记录和设置。减少空间占用, 加快备份速度。", + "backup.skip_file_data_help": "备份时跳过备份图片、知识库等数据文件,仅备份聊天记录和设置。减少空间占用, 加快备份速度", "clear_cache": { "button": "清除缓存", "confirm": "清除缓存将删除应用缓存的数据,包括小程序数据。此操作不可恢复,是否继续?", @@ -1037,7 +1037,7 @@ "url": "Joplin 剪裁服务监听 URL", "url_placeholder": "http://127.0.0.1:41184/" }, - "markdown_export.force_dollar_math.help": "开启后,导出Markdown时会将强制使用$$来标记LaTeX公式。注意:该项也会影响所有通过Markdown导出的方式,如Notion、语雀等。", + "markdown_export.force_dollar_math.help": "开启后,导出Markdown时会将强制使用$$来标记LaTeX公式。注意:该项也会影响所有通过Markdown导出的方式,如Notion、语雀等", "markdown_export.force_dollar_math.title": "强制使用$$来标记LaTeX公式", "markdown_export.help": "若填入,则每次导出时将自动保存到该路径;否则,将弹出保存对话框", "markdown_export.path": "默认导出路径", @@ -1045,7 +1045,7 @@ "markdown_export.select": "选择", "markdown_export.title": "Markdown 导出", "message_title.use_topic_naming.title": "使用话题命名模型为导出的消息创建标题", - "message_title.use_topic_naming.help": "开启后,使用话题命名模型为导出的消息创建标题。该项也会影响所有通过Markdown导出的方式。", + "message_title.use_topic_naming.help": "开启后,使用话题命名模型为导出的消息创建标题。该项也会影响所有通过Markdown导出的方式", "minute_interval_one": "{{count}} 分钟", "minute_interval_other": "{{count}} 分钟", "notion.api_key": "Notion 密钥", @@ -1084,8 +1084,8 @@ "backup.manager.restore.success": "恢复成功,应用将在几秒后刷新", "backup.manager.restore.error": "恢复失败", "backup.manager.delete.confirm.title": "确认删除", - "backup.manager.delete.confirm.single": "确定要删除备份文件 \"{{fileName}}\" 吗?此操作不可恢复。", - "backup.manager.delete.confirm.multiple": "确定要删除选中的 {{count}} 个备份文件吗?此操作不可恢复。", + "backup.manager.delete.confirm.single": "确定要删除备份文件 \"{{fileName}}\" 吗?此操作不可恢复", + "backup.manager.delete.confirm.multiple": "确定要删除选中的 {{count}} 个备份文件吗?此操作不可恢复", "backup.manager.delete.success.single": "删除成功", "backup.manager.delete.success.multiple": "成功删除 {{count}} 个备份文件", "backup.manager.delete.error": "删除失败", @@ -1215,20 +1215,20 @@ "custom": { "title": "自定义", "edit_title": "编辑自定义小程序", - "save_success": "自定义小程序保存成功。", - "save_error": "自定义小程序保存失败。", - "remove_success": "自定义小程序删除成功。", - "remove_error": "自定义小程序删除失败。", - "logo_upload_success": "Logo 上传成功。", - "logo_upload_error": "Logo 上传失败。", + "save_success": "自定义小程序保存成功", + "save_error": "自定义小程序保存失败", + "remove_success": "自定义小程序删除成功", + "remove_error": "自定义小程序删除失败", + "logo_upload_success": "Logo 上传成功", + "logo_upload_error": "Logo 上传失败", "id": "ID", - "id_error": "ID 是必填项。", + "id_error": "ID 是必填项", "id_placeholder": "请输入 ID", "name": "名称", - "name_error": "名称是必填项。", + "name_error": "名称是必填项", "name_placeholder": "请输入名称", "url": "URL", - "url_error": "URL 是必填项。", + "url_error": "URL 是必填项", "url_placeholder": "请输入 URL", "logo": "Logo", "logo_url": "Logo URL", @@ -1238,7 +1238,7 @@ "logo_upload_label": "上传 Logo", "logo_upload_button": "上传", "save": "保存", - "edit_description": "在这里编辑自定义小应用的配置。每个应用需要包含 id、name、url 和 logo 字段。", + "edit_description": "在这里编辑自定义小应用的配置。每个应用需要包含 id、name、url 和 logo 字段", "placeholder": "请输入自定义小程序配置(JSON格式)", "duplicate_ids": "发现重复的ID: {{ids}}", "conflicting_ids": "与默认应用ID冲突: {{ids}}" @@ -1286,7 +1286,7 @@ "addServer": "添加服务器", "addServer.create": "快速创建", "addServer.importFrom": "从 JSON 导入", - "addServer.importFrom.tooltip": "请从 MCP Servers 的介绍页面复制配置JSON(优先使用\n NPX或 UVX 配置),并粘贴到输入框中。", + "addServer.importFrom.tooltip": "请从 MCP Servers 的介绍页面复制配置JSON(优先使用\n NPX或 UVX 配置),并粘贴到输入框中", "addServer.importFrom.placeholder": "粘贴 MCP 服务器 JSON 配置", "addServer.importFrom.invalid": "无效输入,请检查 JSON 格式", "addServer.importFrom.nameExists": "服务器已存在:{{name}}", @@ -1321,7 +1321,7 @@ "installError": "安装依赖项失败", "installSuccess": "依赖项安装成功", "jsonFormatError": "JSON格式化错误", - "jsonModeHint": "编辑MCP服务器配置的JSON表示。保存前请确保格式正确。", + "jsonModeHint": "编辑MCP服务器配置的JSON表示。保存前请确保格式正确", "jsonSaveError": "保存JSON配置失败", "jsonSaveSuccess": "JSON配置已保存", "missingDependencies": "缺失,请安装它以继续", @@ -1388,7 +1388,7 @@ "deleteServer": "删除服务器", "deleteServerConfirm": "确定要删除此服务器吗?", "registry": "包管理源", - "registryTooltip": "选择用于安装包的源,以解决默认源的网络问题。", + "registryTooltip": "选择用于安装包的源,以解决默认源的网络问题", "registryDefault": "默认", "not_support": "模型不支持", "user": "用户", @@ -1472,7 +1472,7 @@ "models.check.model_status_partial": "其中 {{count}} 个模型用某些密钥无法访问", "models.check.model_status_passed": "{{count}} 个模型通过健康检测", "models.check.model_status_summary": "{{provider}}: {{summary}}", - "models.check.no_api_keys": "未找到API密钥,请先添加API密钥。", + "models.check.no_api_keys": "未找到API密钥,请先添加API密钥", "models.check.passed": "通过", "models.check.select_api_key": "选择要使用的API密钥:", "models.check.single": "单个", @@ -1520,7 +1520,7 @@ "api_key.tip": "多个密钥使用逗号分隔", "api_version": "API 版本", "basic_auth": "HTTP 认证", - "basic_auth.tip": "适用于通过服务器部署的实例(参见文档)。目前仅支持 Basic 方案(RFC7617)。", + "basic_auth.tip": "适用于通过服务器部署的实例(参见文档)。目前仅支持 Basic 方案(RFC7617)", "basic_auth.user_name": "用户名", "basic_auth.user_name.tip": "留空以禁用", "basic_auth.password": "密码", @@ -1682,7 +1682,7 @@ "titleLabel": "标题", "contentLabel": "内容", "titlePlaceholder": "请输入短语标题", - "contentPlaceholder": "请输入短语内容,支持使用变量,然后按Tab键可以快速定位到变量进行修改。比如:\n帮我规划从${from}到${to}的路线,然后发送到${email}。", + "contentPlaceholder": "请输入短语内容,支持使用变量,然后按Tab键可以快速定位到变量进行修改。比如:\n帮我规划从${from}到${to}的路线,然后发送到${email}", "delete": "删除短语", "deleteConfirm": "删除短语后将无法恢复,是否继续?", "locationLabel": "添加位置", diff --git a/src/renderer/src/i18n/locales/zh-tw.json b/src/renderer/src/i18n/locales/zh-tw.json index ceaf666452..9f1a30675e 100644 --- a/src/renderer/src/i18n/locales/zh-tw.json +++ b/src/renderer/src/i18n/locales/zh-tw.json @@ -93,7 +93,7 @@ "titleLabel": "標題", "titlePlaceholder": "輸入標題", "contentLabel": "內容", - "contentPlaceholder": "請輸入短語內容,支持使用變量,然後按Tab鍵可以快速定位到變量進行修改。比如:\n幫我規劃從${from}到${to}的行程,然後發送到${email}。" + "contentPlaceholder": "請輸入短語內容,支持使用變量,然後按Tab鍵可以快速定位到變量進行修改。比如:\n幫我規劃從${from}到${to}的行程,然後發送到${email}" }, "settings.knowledge_base.recognition.tip": "智慧代理人將調用大語言模型的意圖識別能力,判斷是否需要調用知識庫進行回答,該功能將依賴模型的能力", "settings.knowledge_base.recognition": "調用知識庫", @@ -113,7 +113,7 @@ "backup": { "confirm": "確定要備份資料嗎?", "confirm.button": "選擇備份位置", - "content": "備份全部資料,包括聊天記錄、設定、知識庫等全部資料。請注意,備份過程可能需要一些時間,感謝您的耐心等待。", + "content": "備份全部資料,包括聊天記錄、設定、知識庫等全部資料。請注意,備份過程可能需要一些時間,感謝您的耐心等待", "progress": { "completed": "備份完成", "compressing": "壓縮檔案...", @@ -144,7 +144,7 @@ "artifacts.preview.openExternal.error.content": "外部瀏覽器開啟出錯", "assistant.search.placeholder": "搜尋", "deeply_thought": "已深度思考(用時 {{seconds}} 秒)", - "default.description": "你好,我是預設助手。你可以立即開始與我聊天。", + "default.description": "你好,我是預設助手。你可以立即開始與我聊天", "default.name": "預設助手", "default.topic.name": "預設話題", "history": { @@ -224,18 +224,18 @@ "settings.code_cacheable": "程式碼區塊快取", "settings.code_cacheable.tip": "快取程式碼區塊可以減少長程式碼區塊的渲染時間,但會增加記憶體使用量", "settings.code_cache_max_size": "快取上限", - "settings.code_cache_max_size.tip": "允許快取的字元數上限(千字符),按照高亮後的程式碼計算。高亮後的程式碼長度相比純文字會長很多。", + "settings.code_cache_max_size.tip": "允許快取的字元數上限(千字符),按照高亮後的程式碼計算。高亮後的程式碼長度相比純文字會長很多", "settings.code_cache_ttl": "快取期限", "settings.code_cache_ttl.tip": "快取的存活時間(分鐘)", "settings.code_cache_threshold": "快取門檻", "settings.code_cache_threshold.tip": "允許快取的最小程式碼長度(千字符),超過門檻的程式碼區塊才會被快取", "settings.context_count": "上下文", - "settings.context_count.tip": "在上下文中保留的前幾則訊息。", + "settings.context_count.tip": "在上下文中保留的前幾則訊息", "settings.max": "最大", "settings.max_tokens": "最大 Token 數", "settings.max_tokens.confirm": "設置最大 Token 數", - "settings.max_tokens.confirm_content": "設置單次交互所用的最大 Token 數,會影響返回結果的長度。要根據模型上下文限制來設定,否則會發生錯誤。", - "settings.max_tokens.tip": "模型可以生成的最大 Token 數。要根據模型上下文限制來設定,否則會發生錯誤。", + "settings.max_tokens.confirm_content": "設置單次交互所用的最大 Token 數,會影響返回結果的長度。要根據模型上下文限制來設定,否則會發生錯誤", + "settings.max_tokens.tip": "模型可以生成的最大 Token 數。要根據模型上下文限制來設定,否則會發生錯誤", "settings.reset": "重設", "settings.set_as_default": "設為預設助手", "settings.show_line_numbers": "程式碼顯示行號", @@ -467,7 +467,7 @@ "type": "類型" }, "gpustack": { - "keep_alive_time.description": "模型在記憶體中保持的時間(預設為 5 分鐘)。", + "keep_alive_time.description": "模型在記憶體中保持的時間(預設為 5 分鐘)", "keep_alive_time.placeholder": "分鐘", "keep_alive_time.title": "保持活躍時間", "title": "GPUStack" @@ -568,7 +568,7 @@ "spanish": "西班牙文" }, "lmstudio": { - "keep_alive_time.description": "對話後模型在記憶體中保持的時間(預設為 5 分鐘)。", + "keep_alive_time.description": "對話後模型在記憶體中保持的時間(預設為 5 分鐘)", "keep_alive_time.placeholder": "分鐘", "keep_alive_time.title": "保持活躍時間", "title": "LM Studio" @@ -788,7 +788,7 @@ "knowledge.error": "無法將 {{type}} 加入知識庫: {{error}}" }, "ollama": { - "keep_alive_time.description": "對話後模型在記憶體中保持的時間(預設為 5 分鐘)。", + "keep_alive_time.description": "對話後模型在記憶體中保持的時間(預設為 5 分鐘)", "keep_alive_time.placeholder": "分鐘", "keep_alive_time.title": "保持活躍時間", "title": "Ollama" @@ -943,7 +943,7 @@ "restore": { "confirm": "確定要復原資料嗎?", "confirm.button": "選擇備份檔案", - "content": "復原操作將使用備份資料覆蓋目前所有應用程式資料。請注意,復原過程可能需要一些時間,感謝您的耐心等待。", + "content": "復原操作將使用備份資料覆蓋目前所有應用程式資料。請注意,復原過程可能需要一些時間,感謝您的耐心等待", "progress": { "completed": "復原完成", "copying_files": "複製檔案... {{progress}}%", @@ -996,7 +996,7 @@ "app_knowledge.remove_all_success": "檔案刪除成功", "app_logs": "應用程式日誌", "backup.skip_file_data_title": "精簡備份", - "backup.skip_file_data_help": "備份時跳過備份圖片、知識庫等數據文件,僅備份聊天記錄和設置。減少空間佔用, 加快備份速度。", + "backup.skip_file_data_help": "備份時跳過備份圖片、知識庫等數據文件,僅備份聊天記錄和設置。減少空間佔用, 加快備份速度", "clear_cache": { "button": "清除快取", "confirm": "清除快取將刪除應用快取資料,包括小工具資料。此操作不可恢復,是否繼續?", @@ -1038,9 +1038,9 @@ "url": "Joplin 剪輯服務 URL", "url_placeholder": "http://127.0.0.1:41184/" }, - "markdown_export.force_dollar_math.help": "開啟後,匯出Markdown時會強制使用$$來標記LaTeX公式。注意:該項也會影響所有透過Markdown匯出的方式,如Notion、語雀等。", + "markdown_export.force_dollar_math.help": "開啟後,匯出Markdown時會強制使用$$來標記LaTeX公式。注意:該項也會影響所有透過Markdown匯出的方式,如Notion、語雀等", "markdown_export.force_dollar_math.title": "LaTeX公式強制使用$$", - "markdown_export.help": "若填入,每次匯出時將自動儲存至該路徑;否則,將彈出儲存對話框。", + "markdown_export.help": "若填入,每次匯出時將自動儲存至該路徑;否則,將彈出儲存對話框", "markdown_export.path": "預設匯出路徑", "markdown_export.path_placeholder": "匯出路徑", "markdown_export.select": "選擇", @@ -1083,8 +1083,8 @@ "backup.manager.restore.success": "恢復成功,應用將在幾秒後刷新", "backup.manager.restore.error": "恢復失敗", "backup.manager.delete.confirm.title": "確認刪除", - "backup.manager.delete.confirm.single": "確定要刪除備份文件 \"{{fileName}}\" 嗎?此操作不可恢復。", - "backup.manager.delete.confirm.multiple": "確定要刪除選中的 {{count}} 個備份文件嗎?此操作不可恢復。", + "backup.manager.delete.confirm.single": "確定要刪除備份文件 \"{{fileName}}\" 嗎?此操作不可恢復", + "backup.manager.delete.confirm.multiple": "確定要刪除選中的 {{count}} 個備份文件嗎?此操作不可恢復", "backup.manager.delete.success.single": "刪除成功", "backup.manager.delete.success.multiple": "成功刪除 {{count}} 個備份文件", "backup.manager.delete.error": "刪除失敗", @@ -1187,7 +1187,7 @@ "new_folder.button": "新建文件夾" }, "message_title.use_topic_naming.title": "使用話題命名模型為導出的消息創建標題", - "message_title.use_topic_naming.help": "此設定會影響所有通過Markdown導出的方式,如Notion、語雀等。" + "message_title.use_topic_naming.help": "此設定會影響所有通過Markdown導出的方式,如Notion、語雀等" }, "display.assistant.title": "助手設定", "display.custom.css": "自訂 CSS", @@ -1218,20 +1218,20 @@ "conflicting_ids": "與預設應用ID衝突: {{ids}}", "title": "自定義", "edit_title": "編輯自定義小程序", - "save_success": "自定義小程序保存成功。", - "save_error": "自定義小程序保存失敗。", - "remove_success": "自定義小程序刪除成功。", - "remove_error": "自定義小程序刪除失敗。", - "logo_upload_success": "Logo 上傳成功。", - "logo_upload_error": "Logo 上傳失敗。", + "save_success": "自定義小程序保存成功", + "save_error": "自定義小程序保存失敗", + "remove_success": "自定義小程序刪除成功", + "remove_error": "自定義小程序刪除失敗", + "logo_upload_success": "Logo 上傳成功", + "logo_upload_error": "Logo 上傳失敗", "id": "ID", - "id_error": "ID 是必填項。", + "id_error": "ID 是必填項", "id_placeholder": "請輸入 ID", "name": "名稱", - "name_error": "名稱是必填項。", + "name_error": "名稱是必填項", "name_placeholder": "請輸入名稱", "url": "URL", - "url_error": "URL 是必填項。", + "url_error": "URL 是必填項", "url_placeholder": "請輸入 URL", "logo": "Logo", "logo_url": "Logo URL", @@ -1286,7 +1286,7 @@ "addServer": "新增伺服器", "addServer.create": "快速創建", "addServer.importFrom": "從 JSON 導入", - "addServer.importFrom.tooltip": "請從 MCP Servers 的介紹頁面複製配置JSON(優先使用\n NPX或 UVX 配置),並粘貼到輸入框中。", + "addServer.importFrom.tooltip": "請從 MCP Servers 的介紹頁面複製配置JSON(優先使用\n NPX或 UVX 配置),並粘貼到輸入框中", "addServer.importFrom.placeholder": "貼上 MCP 伺服器 JSON 設定", "addServer.importFrom.invalid": "無效的輸入,請檢查 JSON 格式", "addServer.importFrom.nameExists": "伺服器已存在:{{name}}", @@ -1321,7 +1321,7 @@ "installError": "安裝相依套件失敗", "installSuccess": "相依套件安裝成功", "jsonFormatError": "JSON格式錯誤", - "jsonModeHint": "編輯MCP伺服器配置的JSON表示。保存前請確保格式正確。", + "jsonModeHint": "編輯MCP伺服器配置的JSON表示。保存前請確保格式正確", "jsonSaveError": "保存JSON配置失敗", "jsonSaveSuccess": "JSON配置已儲存", "missingDependencies": "缺失,請安裝它以繼續", @@ -1388,7 +1388,7 @@ "deleteServer": "刪除伺服器", "deleteServerConfirm": "確定要刪除此伺服器嗎?", "registry": "套件管理源", - "registryTooltip": "選擇用於安裝套件的源,以解決預設源的網路問題。", + "registryTooltip": "選擇用於安裝套件的源,以解決預設源的網路問題", "registryDefault": "預設", "not_support": "不支援此模型", "user": "用戶", @@ -1472,7 +1472,7 @@ "models.check.model_status_partial": "其中 {{count}} 個模型用某些密鑰無法訪問", "models.check.model_status_passed": "{{count}} 個模型通過健康檢查", "models.check.model_status_summary": "{{provider}}: {{summary}}", - "models.check.no_api_keys": "未找到API密鑰,請先添加API密鑰。", + "models.check.no_api_keys": "未找到API密鑰,請先添加API密鑰", "models.check.passed": "通過", "models.check.select_api_key": "選擇要使用的API密鑰:", "models.check.single": "單個", @@ -1514,7 +1514,7 @@ "api_key.tip": "多個金鑰使用逗號分隔", "api_version": "API 版本", "basic_auth": "HTTP 認證", - "basic_auth.tip": "適用於透過伺服器部署的實例(請參閱文檔)。目前僅支援 Basic 方案(RFC7617)。", + "basic_auth.tip": "適用於透過伺服器部署的實例(請參閱文檔)。目前僅支援 Basic 方案(RFC7617)", "basic_auth.user_name": "用戶", "basic_auth.user_name.tip": "留空以停用", "basic_auth.password": "密碼", @@ -1572,7 +1572,7 @@ "markdown_editor_default_value": "預覽區域" }, "openai": { - "alert": "OpenAI Provider 不再支援舊的呼叫方法。如果使用第三方 API,請建立新的服務供應商。" + "alert": "OpenAI Provider 不再支援舊的呼叫方法。如果使用第三方 API,請建立新的服務供應商" } }, "proxy": { @@ -1667,7 +1667,7 @@ "apikey": "API 金鑰", "free": "免費", "content_limit": "內容長度限制", - "content_limit_tooltip": "限制搜尋結果的內容長度,超過限制的內容將被截斷。" + "content_limit_tooltip": "限制搜尋結果的內容長度,超過限制的內容將被截斷" }, "general.auto_check_update.title": "啟用自動更新", "quickPhrase": { @@ -1677,7 +1677,7 @@ "titleLabel": "標題", "contentLabel": "內容", "titlePlaceholder": "請輸入短語標題", - "contentPlaceholder": "請輸入短語內容,支持使用變量,然後按Tab鍵可以快速定位到變量進行修改。比如:\n幫我規劃從${from}到${to}的行程,然後發送到${email}。", + "contentPlaceholder": "請輸入短語內容,支持使用變量,然後按Tab鍵可以快速定位到變量進行修改。比如:\n幫我規劃從${from}到${to}的行程,然後發送到${email}", "delete": "刪除短語", "deleteConfirm": "刪除後無法復原,是否繼續?", "locationLabel": "添加位置", diff --git a/src/renderer/src/pages/apps/App.tsx b/src/renderer/src/pages/apps/App.tsx index 41c069f12d..d8e751dee7 100644 --- a/src/renderer/src/pages/apps/App.tsx +++ b/src/renderer/src/pages/apps/App.tsx @@ -1,13 +1,11 @@ -import { PlusOutlined, UploadOutlined } from '@ant-design/icons' import MinAppIcon from '@renderer/components/Icons/MinAppIcon' import { loadCustomMiniApp, ORIGIN_DEFAULT_MIN_APPS, updateDefaultMinApps } from '@renderer/config/minapps' import { useMinappPopup } from '@renderer/hooks/useMinappPopup' import { useMinapps } from '@renderer/hooks/useMinapps' import { MinAppType } from '@renderer/types' import type { MenuProps } from 'antd' -import { Button, Dropdown, Form, Input, message, Modal, Radio, Upload } from 'antd' -import type { UploadFile } from 'antd/es/upload/interface' -import { FC, useState } from 'react' +import { Dropdown, message } from 'antd' +import { FC } from 'react' import { useTranslation } from 'react-i18next' import styled from 'styled-components' @@ -24,215 +22,72 @@ const App: FC = ({ app, onClick, size = 60, isLast }) => { const { minapps, pinned, disabled, updateMinapps, updateDisabledMinapps, updatePinnedMinapps } = useMinapps() const isPinned = pinned.some((p) => p.id === app.id) const isVisible = minapps.some((m) => m.id === app.id) - const [isModalVisible, setIsModalVisible] = useState(false) - const [form] = Form.useForm() - const [logoType, setLogoType] = useState<'url' | 'file'>('url') - const [fileList, setFileList] = useState([]) const handleClick = () => { - if (isLast) { - setIsModalVisible(true) - return - } openMinappKeepAlive(app) onClick?.() } - const handleAddCustomApp = async (values: any) => { - try { - const content = await window.api.file.read('custom-minapps.json') - const customApps = JSON.parse(content) - - // Check for duplicate ID - if (customApps.some((app: MinAppType) => app.id === values.id)) { - message.error(t('settings.miniapps.custom.duplicate_ids', { ids: values.id })) - return + const menuItems: MenuProps['items'] = [ + { + key: 'togglePin', + label: isPinned ? t('minapp.sidebar.remove.title') : t('minapp.sidebar.add.title'), + onClick: () => { + const newPinned = isPinned ? pinned.filter((item) => item.id !== app.id) : [...(pinned || []), app] + updatePinnedMinapps(newPinned) } - if (ORIGIN_DEFAULT_MIN_APPS.some((app: MinAppType) => app.id === values.id)) { - message.error(t('settings.miniapps.custom.conflicting_ids', { ids: values.id })) - return + }, + { + key: 'hide', + label: t('minapp.sidebar.hide.title'), + onClick: () => { + const newMinapps = minapps.filter((item) => item.id !== app.id) + updateMinapps(newMinapps) + const newDisabled = [...(disabled || []), app] + updateDisabledMinapps(newDisabled) + const newPinned = pinned.filter((item) => item.id !== app.id) + updatePinnedMinapps(newPinned) } - - const newApp: MinAppType = { - id: values.id, - name: values.name, - url: values.url, - logo: form.getFieldValue('logo') || '', - type: 'Custom', - addTime: new Date().toISOString() - } - customApps.push(newApp) - await window.api.file.writeWithId('custom-minapps.json', JSON.stringify(customApps, null, 2)) - message.success(t('settings.miniapps.custom.save_success')) - setIsModalVisible(false) - form.resetFields() - setFileList([]) - // 重新加载应用列表 - const reloadedApps = [...ORIGIN_DEFAULT_MIN_APPS, ...(await loadCustomMiniApp())] - updateDefaultMinApps(reloadedApps) - updateMinapps([...minapps, newApp]) - } catch (error) { - message.error(t('settings.miniapps.custom.save_error')) - console.error('Failed to save custom mini app:', error) - } - } - - const handleLogoTypeChange = (e: any) => { - setLogoType(e.target.value) - form.setFieldValue('logo', '') - setFileList([]) - } - - const handleFileChange = async (info: any) => { - const file = info.fileList[info.fileList.length - 1]?.originFileObj - setFileList(info.fileList.slice(-1)) - - if (file) { - try { - const reader = new FileReader() - reader.onload = (event) => { - const base64Data = event.target?.result - if (typeof base64Data === 'string') { - message.success(t('settings.miniapps.custom.logo_upload_success')) - form.setFieldValue('logo', base64Data) - } - } - reader.readAsDataURL(file) - } catch (error) { - console.error('Failed to read file:', error) - message.error(t('settings.miniapps.custom.logo_upload_error')) - } - } - } - - const menuItems: MenuProps['items'] = isLast - ? [] - : [ - { - key: 'togglePin', - label: isPinned ? t('minapp.sidebar.remove.title') : t('minapp.sidebar.add.title'), - onClick: () => { - const newPinned = isPinned ? pinned.filter((item) => item.id !== app.id) : [...(pinned || []), app] - updatePinnedMinapps(newPinned) - } - }, - { - key: 'hide', - label: t('minapp.sidebar.hide.title'), - onClick: () => { - const newMinapps = minapps.filter((item) => item.id !== app.id) - updateMinapps(newMinapps) - const newDisabled = [...(disabled || []), app] - updateDisabledMinapps(newDisabled) - const newPinned = pinned.filter((item) => item.id !== app.id) - updatePinnedMinapps(newPinned) - } - }, - ...(app.type === 'Custom' - ? [ - { - key: 'removeCustom', - label: t('minapp.sidebar.remove_custom.title'), - danger: true, - onClick: async () => { - try { - const content = await window.api.file.read('custom-minapps.json') - const customApps = JSON.parse(content) - const updatedApps = customApps.filter((customApp: MinAppType) => customApp.id !== app.id) - await window.api.file.writeWithId('custom-minapps.json', JSON.stringify(updatedApps, null, 2)) - message.success(t('settings.miniapps.custom.remove_success')) - const reloadedApps = [...ORIGIN_DEFAULT_MIN_APPS, ...(await loadCustomMiniApp())] - updateDefaultMinApps(reloadedApps) - updateMinapps(minapps.filter((item) => item.id !== app.id)) - updatePinnedMinapps(pinned.filter((item) => item.id !== app.id)) - updateDisabledMinapps(disabled.filter((item) => item.id !== app.id)) - } catch (error) { - message.error(t('settings.miniapps.custom.remove_error')) - console.error('Failed to remove custom mini app:', error) - } - } + }, + ...(app.type === 'Custom' + ? [ + { + key: 'removeCustom', + label: t('minapp.sidebar.remove_custom.title'), + danger: true, + onClick: async () => { + try { + const content = await window.api.file.read('custom-minapps.json') + const customApps = JSON.parse(content) + const updatedApps = customApps.filter((customApp: MinAppType) => customApp.id !== app.id) + await window.api.file.writeWithId('custom-minapps.json', JSON.stringify(updatedApps, null, 2)) + message.success(t('settings.miniapps.custom.remove_success')) + const reloadedApps = [...ORIGIN_DEFAULT_MIN_APPS, ...(await loadCustomMiniApp())] + updateDefaultMinApps(reloadedApps) + updateMinapps(minapps.filter((item) => item.id !== app.id)) + updatePinnedMinapps(pinned.filter((item) => item.id !== app.id)) + updateDisabledMinapps(disabled.filter((item) => item.id !== app.id)) + } catch (error) { + message.error(t('settings.miniapps.custom.remove_error')) + console.error('Failed to remove custom mini app:', error) } - ] - : []) - ] + } + } + ] + : []) + ] - if (!isVisible && !isLast) { + if (!isVisible) { return null } return ( - <> - - - {isLast ? ( - - - - ) : ( - - )} - {isLast ? t('settings.miniapps.custom.title') : app.name} - - - { - setIsModalVisible(false) - setFileList([]) - }} - footer={null} - transitionName="animation-move-down" - centered> -
- - - - - - - - - - - - {t('settings.miniapps.custom.logo_url')} - {t('settings.miniapps.custom.logo_file')} - - - {logoType === 'url' ? ( - - - - ) : ( - - false}> - - - - )} - - - -
-
- + + + + {isLast ? t('settings.miniapps.custom.title') : app.name} + + ) } @@ -254,25 +109,4 @@ const AppTitle = styled.div` white-space: nowrap; ` -const AddButton = styled.div<{ size?: number }>` - width: ${({ size }) => size || 60}px; - height: ${({ size }) => size || 60}px; - border-radius: 12px; - display: flex; - align-items: center; - justify-content: center; - background: var(--color-background-soft); - border: 1px dashed var(--color-border); - color: var(--color-text-soft); - font-size: 24px; - cursor: pointer; - transition: all 0.2s; - - &:hover { - background: var(--color-background); - border-color: var(--color-primary); - color: var(--color-primary); - } -` - export default App diff --git a/src/renderer/src/pages/apps/AppsPage.tsx b/src/renderer/src/pages/apps/AppsPage.tsx index 099649fade..5348ac4367 100644 --- a/src/renderer/src/pages/apps/AppsPage.tsx +++ b/src/renderer/src/pages/apps/AppsPage.tsx @@ -1,14 +1,13 @@ import { Navbar, NavbarCenter } from '@renderer/components/app/Navbar' -import { Center } from '@renderer/components/Layout' import { useMinapps } from '@renderer/hooks/useMinapps' import { Input } from 'antd' -import { isEmpty } from 'lodash' import { Search } from 'lucide-react' import React, { FC, useState } from 'react' import { useTranslation } from 'react-i18next' import styled from 'styled-components' import App from './App' +import NewAppButton from './NewAppButton' const AppsPage: FC = () => { const { t } = useTranslation() @@ -51,18 +50,12 @@ const AppsPage: FC = () => { - {isEmpty(filteredApps) ? ( -
- -
- ) : ( - - {filteredApps.map((app) => ( - - ))} - - - )} + + {filteredApps.map((app) => ( + + ))} + +
) diff --git a/src/renderer/src/pages/apps/NewAppButton.tsx b/src/renderer/src/pages/apps/NewAppButton.tsx new file mode 100644 index 0000000000..a09f4c86f3 --- /dev/null +++ b/src/renderer/src/pages/apps/NewAppButton.tsx @@ -0,0 +1,197 @@ +import { PlusOutlined, UploadOutlined } from '@ant-design/icons' +import { loadCustomMiniApp, ORIGIN_DEFAULT_MIN_APPS, updateDefaultMinApps } from '@renderer/config/minapps' +import { useMinapps } from '@renderer/hooks/useMinapps' +import { MinAppType } from '@renderer/types' +import { Button, Form, Input, message, Modal, Radio, Upload } from 'antd' +import type { UploadFile } from 'antd/es/upload/interface' +import { FC, useState } from 'react' +import { useTranslation } from 'react-i18next' +import styled from 'styled-components' + +interface Props { + size?: number +} + +const NewAppButton: FC = ({ size = 60 }) => { + const { t } = useTranslation() + const [isModalVisible, setIsModalVisible] = useState(false) + const [fileList, setFileList] = useState([]) + const [logoType, setLogoType] = useState<'url' | 'file'>('url') + const [form] = Form.useForm() + const { minapps, updateMinapps } = useMinapps() + + const handleLogoTypeChange = (e: any) => { + setLogoType(e.target.value) + form.setFieldValue('logo', '') + setFileList([]) + } + + const handleAddCustomApp = async (values: any) => { + try { + const content = await window.api.file.read('custom-minapps.json') + const customApps = JSON.parse(content) + + // Check for duplicate ID + if (customApps.some((app: MinAppType) => app.id === values.id)) { + message.error(t('settings.miniapps.custom.duplicate_ids', { ids: values.id })) + return + } + if (ORIGIN_DEFAULT_MIN_APPS.some((app: MinAppType) => app.id === values.id)) { + message.error(t('settings.miniapps.custom.conflicting_ids', { ids: values.id })) + return + } + + const newApp: MinAppType = { + id: values.id, + name: values.name, + url: values.url, + logo: form.getFieldValue('logo') || '', + type: 'Custom', + addTime: new Date().toISOString() + } + customApps.push(newApp) + await window.api.file.writeWithId('custom-minapps.json', JSON.stringify(customApps, null, 2)) + message.success(t('settings.miniapps.custom.save_success')) + setIsModalVisible(false) + form.resetFields() + setFileList([]) + const reloadedApps = [...ORIGIN_DEFAULT_MIN_APPS, ...(await loadCustomMiniApp())] + updateDefaultMinApps(reloadedApps) + updateMinapps([...minapps, newApp]) + } catch (error) { + message.error(t('settings.miniapps.custom.save_error')) + console.error('Failed to save custom mini app:', error) + } + } + + const handleFileChange = async (info: any) => { + const file = info.fileList[info.fileList.length - 1]?.originFileObj + setFileList(info.fileList.slice(-1)) + + if (file) { + try { + const reader = new FileReader() + reader.onload = (event) => { + const base64Data = event.target?.result + if (typeof base64Data === 'string') { + message.success(t('settings.miniapps.custom.logo_upload_success')) + form.setFieldValue('logo', base64Data) + } + } + reader.readAsDataURL(file) + } catch (error) { + console.error('Failed to read file:', error) + message.error(t('settings.miniapps.custom.logo_upload_error')) + } + } + } + + return ( + <> + setIsModalVisible(true)}> + + + + {t('settings.miniapps.custom.title')} + + { + setIsModalVisible(false) + setFileList([]) + }} + footer={null} + transitionName="animation-move-down" + centered> +
+ + + + + + + + + + + + {t('settings.miniapps.custom.logo_url')} + {t('settings.miniapps.custom.logo_file')} + + + {logoType === 'url' ? ( + + + + ) : ( + + false}> + + + + )} + + + +
+
+ + ) +} + +const Container = styled.div` + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + cursor: pointer; +` + +const AddButton = styled.div<{ size?: number }>` + width: ${({ size }) => size || 60}px; + height: ${({ size }) => size || 60}px; + border-radius: 12px; + display: flex; + align-items: center; + justify-content: center; + background: var(--color-background-soft); + border: 1px dashed var(--color-border); + color: var(--color-text-soft); + font-size: 24px; + cursor: pointer; + transition: all 0.2s; + + &:hover { + background: var(--color-background); + border-color: var(--color-primary); + color: var(--color-primary); + } +` + +const AppTitle = styled.div` + font-size: 12px; + margin-top: 5px; + color: var(--color-text-soft); + text-align: center; + user-select: none; + white-space: nowrap; +` + +export default NewAppButton From 03ef52c6a7c8715f361cc066ef32c02bf2af079c Mon Sep 17 00:00:00 2001 From: kangfenmao Date: Fri, 23 May 2025 15:25:01 +0800 Subject: [PATCH 06/54] fix: ensure correct PATH assignment in shell environment - Updated the environment variable initialization to use a consistent type. - Added logic to set the PATH variable correctly, ensuring it falls back to existing values if necessary. --- src/main/services/mcp/shell-env.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/main/services/mcp/shell-env.ts b/src/main/services/mcp/shell-env.ts index 9901417024..a4128b3651 100644 --- a/src/main/services/mcp/shell-env.ts +++ b/src/main/services/mcp/shell-env.ts @@ -85,7 +85,7 @@ function getLoginShellEnvironment(): Promise> { Logger.warn(`Shell process stderr output (even with exit code 0):\n${errorOutput.trim()}`) } - const env = {} + const env: Record = {} const lines = output.split('\n') lines.forEach((line) => { @@ -110,6 +110,8 @@ function getLoginShellEnvironment(): Promise> { Logger.warn('Raw output from shell:\n', output) } + env.PATH = env.Path || env.PATH || '' + resolve(env) }) }) From 7c119e2747e46f717f4275bcec413977ce9697e5 Mon Sep 17 00:00:00 2001 From: beyondkmp Date: Fri, 23 May 2025 15:29:13 +0800 Subject: [PATCH 07/54] feat: add navigation buttons for webview history in MinApp popup (#6342) * feat: add navigation buttons for webview history in MinApp popup - Implemented 'Go Back' and 'Go Forward' functionality in the MinApp popup. - Added corresponding translations for English, Japanese, Russian, and Chinese locales. - Included icons for navigation buttons to enhance user experience. * fix: update Russian and Traditional Chinese translations for UI elements - Revised translations for "rightclick_copyurl", "close", and "minimize" to improve clarity and consistency in the Russian and Traditional Chinese locales. - Ensured that the translations align better with user expectations and common usage. * fix: update Russian translations for MinApp popup UI elements - Revised translations for "close" and "minimize" to specify their context within the MinApp, enhancing clarity for users. - Ensured consistency with existing translations and improved user understanding of the interface. --------- Co-authored-by: beyondkmp --- .../MinApp/MinappPopupContainer.tsx | 28 +++++++++++++++++++ src/renderer/src/i18n/locales/en-us.json | 2 ++ src/renderer/src/i18n/locales/ja-jp.json | 2 ++ src/renderer/src/i18n/locales/ru-ru.json | 2 ++ src/renderer/src/i18n/locales/zh-cn.json | 2 ++ src/renderer/src/i18n/locales/zh-tw.json | 2 ++ 6 files changed, 38 insertions(+) diff --git a/src/renderer/src/components/MinApp/MinappPopupContainer.tsx b/src/renderer/src/components/MinApp/MinappPopupContainer.tsx index a2dd31cfab..bd360c8a30 100644 --- a/src/renderer/src/components/MinApp/MinappPopupContainer.tsx +++ b/src/renderer/src/components/MinApp/MinappPopupContainer.tsx @@ -1,4 +1,6 @@ import { + ArrowLeftOutlined, + ArrowRightOutlined, CloseOutlined, CodeOutlined, CopyOutlined, @@ -241,6 +243,22 @@ const MinappPopupContainer: React.FC = () => { dispatch(setMinappsOpenLinkExternal(!minappsOpenLinkExternal)) } + /** navigate back in webview history */ + const handleGoBack = (appid: string) => { + const webview = webviewRefs.current.get(appid) + if (webview && webview.canGoBack()) { + webview.goBack() + } + } + + /** navigate forward in webview history */ + const handleGoForward = (appid: string) => { + const webview = webviewRefs.current.get(appid) + if (webview && webview.canGoForward()) { + webview.goForward() + } + } + /** Title bar of the popup */ const Title = ({ appInfo, url }: { appInfo: AppInfo | null; url: string | null }) => { if (!appInfo) return null @@ -286,6 +304,16 @@ const MinappPopupContainer: React.FC = () => { )} + + + + + + + + )} + + + + + {t('common.provider')} + + {t('paintings.paint_course')} + + + + + {t('common.model')} + onInputSeed(e)} + suffix={ + updatePaintingState({ seed: Math.floor(Math.random() * 1000000).toString() })} + style={{ cursor: 'pointer', color: 'var(--color-text-2)' }} + /> + } + /> + + {t('paintings.style_type')} + + + {STYLE_TYPE_OPTIONS.map((ele) => ( + onSelectStyleType(ele.label)}> + {ele.label} + + ))} + + + + + {painting?.urls?.length > 0 || DMXAPIPaintings?.length > 1 ? ( + + ) : ( + + + + )} + + +