From cf7584bb63a3aecec873bc0af6d9eea4e29056ee Mon Sep 17 00:00:00 2001 From: Phantom <59059173+EurFelux@users.noreply.github.com> Date: Wed, 10 Sep 2025 15:16:53 +0800 Subject: [PATCH] refactor(toast): migrate message to toast (#10023) * feat(toast): add toast utility functions to global interface Expose error, success, warning, and info toast functions globally for consistent notification handling * refactor(toast): use ToastPropsColored type to enforce color consistency Create ToastPropsColored type to explicitly omit color property from ToastProps * refactor(toast): simplify toast functions using factory pattern Create a factory function to generate toast functions instead of repeating similar code. This improves maintainability and reduces code duplication. * fix: replace window.message with window.toast for copy notifications * refactor(toast): update type definition to use Parameters utility Use Parameters utility type to derive ToastPropsColored from addToast parameters for better type safety * feat(types): add RequireSome utility type for making specific properties required * feat(toast): add loading toast functionality Add loading toast type to support promises in toast notifications. This enables showing loading states for async operations. * chore: add claude script to package.json * build(eslint): add packages dist folder to ignore patterns * refactor: migrate message to toast * refactor: update toast import path from @heroui/react to @heroui/toast * docs(toast): add JSDoc comments for toast functions * fix(toast): set default timeout for loading toasts Make loading toasts disappear immediately by default when timeout is not specified * fix(translate): replace window.message with window.toast for consistency Use window.toast consistently across the translation page for displaying notifications to maintain a uniform user experience and simplify the codebase. * refactor: remove deprecated message interface from window The MessageInstance interface from antd was marked as deprecated and is no longer needed in the window object. * refactor(toast): consolidate toast utilities into single export Move all toast-related functions into a single utility export to reduce code duplication and improve maintainability. Update all imports to use the new utility function. * docs(useOcr): remove redundant comment in ocr function * docs: update comments from Chinese to English Update error log messages in CodeToolsPage to use English instead of Chinese for better consistency and maintainability * feat(toast): add no-drag style and adjust toast placement add custom CSS class to disable drag on toast elements move toast placement to top-center and adjust timeout settings --- eslint.config.mjs | 3 +- package.json | 3 +- src/renderer/src/assets/styles/tailwind.css | 5 + .../__tests__/useImageTools.test.tsx | 16 +- .../ActionTools/hooks/useImageTools.tsx | 8 +- .../CodeBlockView/HtmlArtifactsCard.tsx | 2 +- .../CodeBlockView/HtmlArtifactsPopup.tsx | 2 +- .../src/components/CodeBlockView/view.tsx | 2 +- .../src/components/ContextMenu/index.tsx | 4 +- src/renderer/src/components/CopyButton.tsx | 4 +- src/renderer/src/components/ImageViewer.tsx | 6 +- .../components/InputEmbeddingDimension.tsx | 6 +- .../src/components/LocalBackupManager.tsx | 10 +- src/renderer/src/components/MinApp/MinApp.tsx | 4 +- .../MinApp/MinappPopupContainer.tsx | 4 +- .../src/components/OAuth/OAuthButton.tsx | 2 +- .../src/components/ObsidianExportDialog.tsx | 2 +- .../components/Popups/ApiKeyListPopup/hook.ts | 8 +- .../Popups/ApiKeyListPopup/item.tsx | 5 +- .../Popups/ApiKeyListPopup/types.ts | 13 +- .../Popups/SaveToKnowledgePopup.tsx | 2 +- .../src/components/Popups/TextEditPopup.tsx | 5 +- .../src/components/Popups/UserPopup.tsx | 6 +- .../src/components/S3BackupManager.tsx | 26 ++- src/renderer/src/components/S3Modals.tsx | 23 +-- src/renderer/src/components/ToastPortal.tsx | 9 +- src/renderer/src/components/TopView/index.tsx | 17 +- src/renderer/src/components/TopView/toast.ts | 72 ++++++++ .../src/components/TranslateButton.tsx | 5 +- .../src/components/WebdavBackupManager.tsx | 22 +-- .../components/__tests__/CopyButton.test.tsx | 14 +- .../InputEmbeddingDimension.test.tsx | 8 +- src/renderer/src/env.d.ts | 12 +- src/renderer/src/hooks/useAssistant.ts | 2 +- src/renderer/src/hooks/useChatContext.ts | 10 +- src/renderer/src/hooks/useFiles.ts | 7 +- src/renderer/src/hooks/useFullScreenNotice.ts | 7 +- .../src/hooks/useKnowledgeBaseForm.ts | 2 +- src/renderer/src/hooks/useOcr.ts | 31 ++-- src/renderer/src/hooks/useOcrProvider.tsx | 10 +- src/renderer/src/hooks/useRuntime.ts | 2 +- src/renderer/src/hooks/useUpdateHandler.ts | 2 +- src/renderer/src/pages/agents/AgentsPage.tsx | 5 +- .../agents/components/ImportAgentPopup.tsx | 10 +- src/renderer/src/pages/code/CodeToolsPage.tsx | 22 +-- .../src/pages/history/HistoryPage.tsx | 2 +- .../pages/home/Inputbar/AttachmentButton.tsx | 7 +- .../src/pages/home/Inputbar/Inputbar.tsx | 7 +- .../pages/home/Inputbar/MCPToolsButton.tsx | 4 +- .../pages/home/Inputbar/UrlContextbutton.tsx | 2 +- .../pages/home/Inputbar/WebSearchButton.tsx | 4 +- .../src/pages/home/Markdown/Table.tsx | 2 +- .../pages/home/Messages/ChatNavigation.tsx | 12 +- .../src/pages/home/Messages/CitationsList.tsx | 2 +- .../src/pages/home/Messages/MessageEditor.tsx | 5 +- .../src/pages/home/Messages/MessageImage.tsx | 8 +- .../pages/home/Messages/MessageMenubar.tsx | 12 +- .../src/pages/home/Messages/Messages.tsx | 10 +- .../home/Messages/Tools/MessageMcpTool.tsx | 7 +- .../src/pages/home/Tabs/TopicsTab.tsx | 8 +- .../home/Tabs/components/AssistantItem.tsx | 5 +- .../components/AddKnowledgeBasePopup.tsx | 6 +- .../components/EditKnowledgeBasePopup.tsx | 4 +- .../components/KnowledgeSearchItem/hooks.ts | 2 +- .../knowledge/components/MigrationInfoTag.tsx | 2 +- .../pages/knowledge/items/KnowledgeUrls.tsx | 4 +- .../src/pages/minapps/NewAppButton.tsx | 12 +- src/renderer/src/pages/notes/HeaderNavbar.tsx | 6 +- src/renderer/src/pages/notes/NotesPage.tsx | 14 +- src/renderer/src/pages/notes/NotesSidebar.tsx | 6 +- .../src/pages/paintings/AihubmixPage.tsx | 10 +- .../src/pages/paintings/DmxapiPage.tsx | 15 +- .../src/pages/paintings/NewApiPage.tsx | 12 +- .../src/pages/paintings/SiliconPage.tsx | 10 +- .../src/pages/paintings/ZhipuPage.tsx | 10 +- .../components/DynamicFormRender.tsx | 2 +- .../pages/paintings/config/DmxapiConfig.ts | 4 +- .../pages/paintings/utils/TokenFluxService.ts | 5 +- .../src/pages/settings/AboutSettings.tsx | 4 +- .../AssistantPromptSettings.tsx | 2 +- .../settings/DataSettings/DataSettings.tsx | 65 ++++--- .../settings/DataSettings/JoplinSettings.tsx | 10 +- .../DataSettings/LocalBackupSettings.tsx | 6 +- .../settings/DataSettings/NotionSettings.tsx | 10 +- .../DataSettings/NutstoreSettings.tsx | 8 +- .../settings/DataSettings/SiyuanSettings.tsx | 10 +- .../settings/DataSettings/YuqueSettings.tsx | 10 +- .../DocProcessSettings/OcrImageSettings.tsx | 2 +- .../src/pages/settings/GeneralSettings.tsx | 8 +- .../MCPSettings/AddMcpServerModal.tsx | 25 +-- .../MCPSettings/BuiltinMCPServerList.tsx | 2 +- .../settings/MCPSettings/EditMcpJsonPopup.tsx | 6 +- .../settings/MCPSettings/InstallNpxUv.tsx | 7 +- .../settings/MCPSettings/McpServersList.tsx | 11 +- .../settings/MCPSettings/McpSettings.tsx | 11 +- .../pages/settings/MCPSettings/NpxSearch.tsx | 13 +- .../settings/MCPSettings/SyncServersPopup.tsx | 8 +- .../MemorySettings/MemorySettings.tsx | 34 ++-- .../src/pages/settings/NotesSettings.tsx | 8 +- .../ProviderSettings/AddProviderPopup.tsx | 6 +- .../ProviderSettings/AnthropicSettings.tsx | 10 +- .../ProviderSettings/CustomHeaderPopup.tsx | 4 +- .../GithubCopilotSettings.tsx | 16 +- .../ModelList/AddModelPopup.tsx | 2 +- .../ModelList/NewApiAddModelPopup.tsx | 2 +- .../ModelList/useHealthCheck.ts | 16 +- .../ProviderSettings/ProviderList.tsx | 10 +- .../ProviderSettings/ProviderSetting.tsx | 26 ++- .../SelectProviderModelPopup.tsx | 2 +- .../pages/settings/QuickAssistantSettings.tsx | 9 +- .../CustomLanguageModal.tsx | 8 +- .../CustomLanguageSettings.tsx | 4 +- .../WebSearchSettings/AddSubscribePopup.tsx | 4 +- .../WebSearchSettings/BlacklistSettings.tsx | 39 +++-- .../WebSearchProviderSetting.tsx | 25 ++- .../src/pages/translate/TranslateHistory.tsx | 2 +- .../src/pages/translate/TranslatePage.tsx | 159 ++++++++---------- .../src/pages/translate/TranslateSettings.tsx | 10 +- src/renderer/src/services/ApiService.ts | 9 +- src/renderer/src/services/AssistantService.ts | 5 +- src/renderer/src/services/BackupService.ts | 26 +-- src/renderer/src/services/MessagesService.ts | 23 +-- src/renderer/src/services/NutstoreService.ts | 10 +- src/renderer/src/services/PasteService.ts | 12 +- src/renderer/src/services/WebSearchService.ts | 7 +- src/renderer/src/store/thunk/messageThunk.ts | 10 +- src/renderer/src/types/index.ts | 15 ++ src/renderer/src/utils/__tests__/copy.test.ts | 22 +-- .../src/utils/__tests__/download.test.ts | 12 +- .../src/utils/__tests__/export.test.ts | 4 +- .../src/utils/__tests__/image.test.ts | 6 +- src/renderer/src/utils/copy.ts | 6 +- src/renderer/src/utils/download.ts | 4 +- src/renderer/src/utils/export.ts | 125 +++++--------- src/renderer/src/utils/image.ts | 13 +- src/renderer/src/utils/mcp-tools.ts | 16 +- src/renderer/src/utils/oauth.ts | 4 +- .../src/windows/mini/MiniWindowApp.tsx | 36 ++-- .../src/windows/mini/home/HomeWindow.tsx | 2 +- .../mini/translate/TranslateWindow.tsx | 2 +- .../action/components/WindowFooter.tsx | 4 +- .../windows/selection/action/entryPoint.tsx | 33 ++-- 142 files changed, 774 insertions(+), 907 deletions(-) create mode 100644 src/renderer/src/components/TopView/toast.ts diff --git a/eslint.config.mjs b/eslint.config.mjs index 6863613c1..f3a96ca22 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -125,7 +125,8 @@ export default defineConfig([ 'src/main/integration/nutstore/sso/lib/**', 'src/main/integration/cherryin/index.js', 'src/main/integration/nutstore/sso/lib/**', - 'src/renderer/src/ui/**' + 'src/renderer/src/ui/**', + 'packages/**/dist' ] } ]) diff --git a/package.json b/package.json index dc67e318f..ca0cc7e17 100644 --- a/package.json +++ b/package.json @@ -68,7 +68,8 @@ "format": "prettier --write .", "format:check": "prettier --check .", "lint": "eslint . --ext .js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix && yarn typecheck && yarn check:i18n", - "prepare": "git config blame.ignoreRevsFile .git-blame-ignore-revs && husky" + "prepare": "git config blame.ignoreRevsFile .git-blame-ignore-revs && husky", + "claude": "dotenv -e .env -- claude" }, "dependencies": { "@libsql/client": "0.14.0", diff --git a/src/renderer/src/assets/styles/tailwind.css b/src/renderer/src/assets/styles/tailwind.css index aa9e68910..4d9e0d4f0 100644 --- a/src/renderer/src/assets/styles/tailwind.css +++ b/src/renderer/src/assets/styles/tailwind.css @@ -153,6 +153,11 @@ body { @apply bg-background text-foreground; } + + /* To disable drag title bar on toast. tailwind css doesn't provide such class name. */ + .hero-toast { + -webkit-app-region: no-drag; + } } :root { diff --git a/src/renderer/src/components/ActionTools/__tests__/useImageTools.test.tsx b/src/renderer/src/components/ActionTools/__tests__/useImageTools.test.tsx index 6083a508d..b59bf8aae 100644 --- a/src/renderer/src/components/ActionTools/__tests__/useImageTools.test.tsx +++ b/src/renderer/src/components/ActionTools/__tests__/useImageTools.test.tsx @@ -43,8 +43,8 @@ vi.mock('@renderer/context/ThemeProvider', () => ({ // Mock navigator.clipboard const mockWrite = vi.fn() -// Mock window.message -const mockMessage = { +// Mock window.toast +const mockedToast = { success: vi.fn(), error: vi.fn() } @@ -68,8 +68,8 @@ describe('useImageTools', () => { writable: true }) - Object.defineProperty(global.window, 'message', { - value: mockMessage, + Object.defineProperty(global.window, 'toast', { + value: mockedToast, writable: true }) @@ -284,7 +284,7 @@ describe('useImageTools', () => { expect(mocks.svgToPngBlob).toHaveBeenCalledWith(mockSvg) expect(mockWrite).toHaveBeenCalled() - expect(mockMessage.success).toHaveBeenCalledWith('message.copy.success') + expect(mockedToast.success).toHaveBeenCalledWith('message.copy.success') }) it('should download image as PNG and SVG', async () => { @@ -364,13 +364,13 @@ describe('useImageTools', () => { await act(async () => { await result.current.copy() }) - expect(mockMessage.error).toHaveBeenCalledWith('message.copy.failed') + expect(mockedToast.error).toHaveBeenCalledWith('message.copy.failed') // 下载失败 await act(async () => { await result.current.download('png') }) - expect(mockMessage.error).toHaveBeenCalledWith('message.download.failed') + expect(mockedToast.error).toHaveBeenCalledWith('message.download.failed') }) }) @@ -420,7 +420,7 @@ describe('useImageTools', () => { await result.current.dialog() }) - expect(mockMessage.error).toHaveBeenCalledWith('message.dialog.failed') + expect(mockedToast.error).toHaveBeenCalledWith('message.dialog.failed') }) it('should do nothing when no element is found', async () => { diff --git a/src/renderer/src/components/ActionTools/hooks/useImageTools.tsx b/src/renderer/src/components/ActionTools/hooks/useImageTools.tsx index e02d8846a..3481b9279 100644 --- a/src/renderer/src/components/ActionTools/hooks/useImageTools.tsx +++ b/src/renderer/src/components/ActionTools/hooks/useImageTools.tsx @@ -210,10 +210,10 @@ export const useImageTools = ( const blob = await svgToPngBlob(imgElement) await navigator.clipboard.write([new ClipboardItem({ 'image/png': blob })]) - window.message.success(t('message.copy.success')) + window.toast.success(t('message.copy.success')) } catch (error) { logger.error('Copy failed:', error as Error) - window.message.error(t('message.copy.failed')) + window.toast.error(t('message.copy.failed')) } }, [getCleanImgElement, t]) @@ -243,7 +243,7 @@ export const useImageTools = ( } } catch (error) { logger.error('Download failed:', error as Error) - window.message.error(t('message.download.failed')) + window.toast.error(t('message.download.failed')) } }, [getCleanImgElement, prefix, t] @@ -262,7 +262,7 @@ export const useImageTools = ( await ImagePreviewService.show(imgElement, { format: 'svg' }) } catch (error) { logger.error('Dialog preview failed:', error as Error) - window.message.error(t('message.dialog.failed')) + window.toast.error(t('message.dialog.failed')) } }, [getCleanImgElement, t]) diff --git a/src/renderer/src/components/CodeBlockView/HtmlArtifactsCard.tsx b/src/renderer/src/components/CodeBlockView/HtmlArtifactsCard.tsx index 13d13c55a..9f5ab5998 100644 --- a/src/renderer/src/components/CodeBlockView/HtmlArtifactsCard.tsx +++ b/src/renderer/src/components/CodeBlockView/HtmlArtifactsCard.tsx @@ -50,7 +50,7 @@ const HtmlArtifactsCard: FC = ({ html, onSave, isStreaming = false }) => const handleDownload = async () => { const fileName = `${getFileNameFromHtmlTitle(title) || 'html-artifact'}.html` await window.api.file.save(fileName, htmlContent) - window.message.success({ content: t('message.download.success'), key: 'download' }) + window.toast.success(t('message.download.success')) } return ( diff --git a/src/renderer/src/components/CodeBlockView/HtmlArtifactsPopup.tsx b/src/renderer/src/components/CodeBlockView/HtmlArtifactsPopup.tsx index dc3825c2f..9453866f2 100644 --- a/src/renderer/src/components/CodeBlockView/HtmlArtifactsPopup.tsx +++ b/src/renderer/src/components/CodeBlockView/HtmlArtifactsPopup.tsx @@ -62,7 +62,7 @@ const HtmlArtifactsPopup: React.FC = ({ open, title, ht await captureScrollableIframeAsBlob(previewFrameRef, async (blob) => { if (blob) { await navigator.clipboard.write([new ClipboardItem({ 'image/png': blob })]) - window.message.success(t('message.copy.success')) + window.toast.success(t('message.copy.success')) } }) } diff --git a/src/renderer/src/components/CodeBlockView/view.tsx b/src/renderer/src/components/CodeBlockView/view.tsx index 5addfa077..a89e545bc 100644 --- a/src/renderer/src/components/CodeBlockView/view.tsx +++ b/src/renderer/src/components/CodeBlockView/view.tsx @@ -128,7 +128,7 @@ export const CodeBlockView: React.FC = memo(({ children, language, onSave const handleCopySource = useCallback(() => { navigator.clipboard.writeText(children) - window.message.success({ content: t('code_block.copy.success'), key: 'copy-code' }) + window.toast.success(t('code_block.copy.success')) }, [children, t]) const handleDownloadSource = useCallback(() => { diff --git a/src/renderer/src/components/ContextMenu/index.tsx b/src/renderer/src/components/ContextMenu/index.tsx index 610afa695..3af2fa2e3 100644 --- a/src/renderer/src/components/ContextMenu/index.tsx +++ b/src/renderer/src/components/ContextMenu/index.tsx @@ -22,10 +22,10 @@ const ContextMenu: React.FC = ({ children }) => { navigator.clipboard .writeText(selectedText) .then(() => { - window.message.success({ content: t('message.copied'), key: 'copy-message' }) + window.toast.success(t('message.copied')) }) .catch(() => { - window.message.error({ content: t('message.copy.failed'), key: 'copy-message-failed' }) + window.toast.error(t('message.copy.failed')) }) } } diff --git a/src/renderer/src/components/CopyButton.tsx b/src/renderer/src/components/CopyButton.tsx index bdc34a067..cfa80a02c 100644 --- a/src/renderer/src/components/CopyButton.tsx +++ b/src/renderer/src/components/CopyButton.tsx @@ -32,10 +32,10 @@ const CopyButton: FC = ({ navigator.clipboard .writeText(textToCopy) .then(() => { - window.message?.success(t('message.copy.success')) + window.toast?.success(t('message.copy.success')) }) .catch(() => { - window.message?.error(t('message.copy.failed')) + window.toast?.error(t('message.copy.failed')) }) } diff --git a/src/renderer/src/components/ImageViewer.tsx b/src/renderer/src/components/ImageViewer.tsx index 1f1cbccb4..179babeaf 100644 --- a/src/renderer/src/components/ImageViewer.tsx +++ b/src/renderer/src/components/ImageViewer.tsx @@ -62,10 +62,10 @@ const ImageViewer: React.FC = ({ src, style, ...props }) => { ]) } - window.message.success(t('message.copy.success')) + window.toast.success(t('message.copy.success')) } catch (error) { logger.error('Failed to copy image:', error as Error) - window.message.error(t('message.copy.failed')) + window.toast.error(t('message.copy.failed')) } } @@ -77,7 +77,7 @@ const ImageViewer: React.FC = ({ src, style, ...props }) => { icon: , onClick: () => { navigator.clipboard.writeText(src) - window.message.success(t('message.copy.success')) + window.toast.success(t('message.copy.success')) } }, { diff --git a/src/renderer/src/components/InputEmbeddingDimension.tsx b/src/renderer/src/components/InputEmbeddingDimension.tsx index 7d7f452d0..056ebaea5 100644 --- a/src/renderer/src/components/InputEmbeddingDimension.tsx +++ b/src/renderer/src/components/InputEmbeddingDimension.tsx @@ -35,13 +35,13 @@ const InputEmbeddingDimension = ({ const handleFetchDimension = useCallback(async () => { if (!model) { logger.warn('Failed to get embedding dimensions: no model') - window.message.error(t('knowledge.embedding_model_required')) + window.toast.error(t('knowledge.embedding_model_required')) return } if (!provider) { logger.warn('Failed to get embedding dimensions: no provider') - window.message.error(t('knowledge.provider_not_found')) + window.toast.error(t('knowledge.provider_not_found')) return } @@ -56,7 +56,7 @@ const InputEmbeddingDimension = ({ onChange?.(dimension) } catch (error) { logger.error(t('message.error.get_embedding_dimensions'), error as Error) - window.message.error(t('message.error.get_embedding_dimensions') + '\n' + getErrorMessage(error)) + window.toast.error(t('message.error.get_embedding_dimensions') + '\n' + getErrorMessage(error)) } finally { setLoading(false) } diff --git a/src/renderer/src/components/LocalBackupManager.tsx b/src/renderer/src/components/LocalBackupManager.tsx index c7b86c94b..fdc64222d 100644 --- a/src/renderer/src/components/LocalBackupManager.tsx +++ b/src/renderer/src/components/LocalBackupManager.tsx @@ -45,7 +45,7 @@ export function LocalBackupManager({ visible, onClose, localBackupDir, restoreMe total: files.length })) } catch (error: any) { - window.message.error(`${t('settings.data.local.backup.manager.fetch.error')}: ${error.message}`) + window.toast.error(`${t('settings.data.local.backup.manager.fetch.error')}: ${error.message}`) } finally { setLoading(false) } @@ -90,13 +90,13 @@ export function LocalBackupManager({ visible, onClose, localBackupDir, restoreMe for (const key of selectedRowKeys) { await window.api.backup.deleteLocalBackupFile(key.toString(), localBackupDir) } - window.message.success( + window.toast.success( t('settings.data.local.backup.manager.delete.success.multiple', { count: selectedRowKeys.length }) ) setSelectedRowKeys([]) await fetchBackupFiles() } catch (error: any) { - window.message.error(`${t('settings.data.local.backup.manager.delete.error')}: ${error.message}`) + window.toast.error(`${t('settings.data.local.backup.manager.delete.error')}: ${error.message}`) } finally { setDeleting(false) } @@ -123,7 +123,7 @@ export function LocalBackupManager({ visible, onClose, localBackupDir, restoreMe message.success(t('settings.data.local.backup.manager.delete.success.single')) await fetchBackupFiles() } catch (error: any) { - window.message.error(`${t('settings.data.local.backup.manager.delete.error')}: ${error.message}`) + window.toast.error(`${t('settings.data.local.backup.manager.delete.error')}: ${error.message}`) } finally { setDeleting(false) } @@ -150,7 +150,7 @@ export function LocalBackupManager({ visible, onClose, localBackupDir, restoreMe message.success(t('settings.data.local.backup.manager.restore.success')) onClose() // Close the modal } catch (error: any) { - window.message.error(`${t('settings.data.local.backup.manager.restore.error')}: ${error.message}`) + window.toast.error(`${t('settings.data.local.backup.manager.restore.error')}: ${error.message}`) } finally { setRestoring(false) } diff --git a/src/renderer/src/components/MinApp/MinApp.tsx b/src/renderer/src/components/MinApp/MinApp.tsx index c14346de2..853d37d10 100644 --- a/src/renderer/src/components/MinApp/MinApp.tsx +++ b/src/renderer/src/components/MinApp/MinApp.tsx @@ -91,14 +91,14 @@ const MinApp: FC = ({ app, onClick, size = 60, isLast }) => { 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)) - window.message.success(t('settings.miniapps.custom.remove_success')) + window.toast.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) { - window.message.error(t('settings.miniapps.custom.remove_error')) + window.toast.error(t('settings.miniapps.custom.remove_error')) logger.error('Failed to remove custom mini app:', error as Error) } } diff --git a/src/renderer/src/components/MinApp/MinappPopupContainer.tsx b/src/renderer/src/components/MinApp/MinappPopupContainer.tsx index fcee851ee..841677440 100644 --- a/src/renderer/src/components/MinApp/MinappPopupContainer.tsx +++ b/src/renderer/src/components/MinApp/MinappPopupContainer.tsx @@ -394,10 +394,10 @@ const MinappPopupContainer: React.FC = () => { navigator.clipboard .writeText(url) .then(() => { - window.toast.addToast({ title: 'URL ' + t('message.copy.success') }) + window.toast.success('URL ' + t('message.copy.success')) }) .catch(() => { - window.toast.addToast({ title: 'URL ' + t('message.copy.failed') }) + window.toast.error('URL ' + t('message.copy.failed')) }) } diff --git a/src/renderer/src/components/OAuth/OAuthButton.tsx b/src/renderer/src/components/OAuth/OAuthButton.tsx index 16faefe42..fec3aae61 100644 --- a/src/renderer/src/components/OAuth/OAuthButton.tsx +++ b/src/renderer/src/components/OAuth/OAuthButton.tsx @@ -23,7 +23,7 @@ const OAuthButton: FC = ({ provider, onSuccess, ...buttonProps }) => { const handleSuccess = (key: string) => { if (key.trim()) { onSuccess?.(key) - window.message.success({ content: t('auth.get_key_success'), key: 'auth-success' }) + window.toast.success(t('auth.get_key_success')) } } diff --git a/src/renderer/src/components/ObsidianExportDialog.tsx b/src/renderer/src/components/ObsidianExportDialog.tsx index fba858763..0c3d1c003 100644 --- a/src/renderer/src/components/ObsidianExportDialog.tsx +++ b/src/renderer/src/components/ObsidianExportDialog.tsx @@ -245,7 +245,7 @@ const PopupContainer: React.FC = ({ content = `---\ntitle: ${state.title}\ncreated: ${state.createdAt}\nsource: ${state.source}\ntags: ${state.tags}\n---\n${markdown}` } if (content === '') { - window.message.error(i18n.t('chat.topics.export.obsidian_export_failed')) + window.toast.error(i18n.t('chat.topics.export.obsidian_export_failed')) return } await navigator.clipboard.writeText(content) diff --git a/src/renderer/src/components/Popups/ApiKeyListPopup/hook.ts b/src/renderer/src/components/Popups/ApiKeyListPopup/hook.ts index c4c9459ed..e69341a86 100644 --- a/src/renderer/src/components/Popups/ApiKeyListPopup/hook.ts +++ b/src/renderer/src/components/Popups/ApiKeyListPopup/hook.ts @@ -302,11 +302,9 @@ async function getModelForCheck(provider: Provider, t: TFunction): Promise !isEmbeddingModel(model) && !isRerankModel(model)) if (isEmpty(modelsToCheck)) { - window.message.error({ - key: 'no-models', - style: { marginTop: '3vh' }, - duration: 5, - content: t('settings.provider.no_models_for_check') + window.toast.error({ + title: t('settings.provider.no_models_for_check'), + timeout: 5000 }) return null } diff --git a/src/renderer/src/components/Popups/ApiKeyListPopup/item.tsx b/src/renderer/src/components/Popups/ApiKeyListPopup/item.tsx index 75d32b8bc..83c938993 100644 --- a/src/renderer/src/components/Popups/ApiKeyListPopup/item.tsx +++ b/src/renderer/src/components/Popups/ApiKeyListPopup/item.tsx @@ -61,10 +61,7 @@ const ApiKeyItem: FC = ({ const handleSave = () => { const result = onUpdate(editValue) if (!result.isValid) { - window.message.warning({ - key: 'api-key-error', - content: result.error - }) + window.toast.warning(result.error) return } diff --git a/src/renderer/src/components/Popups/ApiKeyListPopup/types.ts b/src/renderer/src/components/Popups/ApiKeyListPopup/types.ts index bc230c577..ad713b40f 100644 --- a/src/renderer/src/components/Popups/ApiKeyListPopup/types.ts +++ b/src/renderer/src/components/Popups/ApiKeyListPopup/types.ts @@ -3,10 +3,15 @@ import { PreprocessProvider, Provider, WebSearchProvider } from '@renderer/types /** * API key 格式有效性 */ -export type ApiKeyValidity = { - isValid: boolean - error?: string -} +export type ApiKeyValidity = + | { + isValid: true + error?: never + } + | { + isValid: false + error: string + } export type ApiProvider = Provider | WebSearchProvider | PreprocessProvider diff --git a/src/renderer/src/components/Popups/SaveToKnowledgePopup.tsx b/src/renderer/src/components/Popups/SaveToKnowledgePopup.tsx index f8789735e..9a7d7ee93 100644 --- a/src/renderer/src/components/Popups/SaveToKnowledgePopup.tsx +++ b/src/renderer/src/components/Popups/SaveToKnowledgePopup.tsx @@ -281,7 +281,7 @@ const PopupContainer: React.FC = ({ source, title, resolve }) => { resolve({ success: true, savedCount }) } catch (error) { logger.error('save failed:', error as Error) - window.message.error( + window.toast.error( t(isTopicMode ? 'chat.save.topic.knowledge.error.save_failed' : 'chat.save.knowledge.error.save_failed') ) setLoading(false) diff --git a/src/renderer/src/components/Popups/TextEditPopup.tsx b/src/renderer/src/components/Popups/TextEditPopup.tsx index 5fec6f3a1..49dca0254 100644 --- a/src/renderer/src/components/Popups/TextEditPopup.tsx +++ b/src/renderer/src/components/Popups/TextEditPopup.tsx @@ -112,10 +112,7 @@ const PopupContainer: React.FC = ({ } } catch (error) { logger.error('Translation failed:', error as Error) - window.message.error({ - content: t('translate.error.failed'), - key: 'translate-message' - }) + window.toast.error(t('translate.error.failed')) } finally { if (isMounted.current) { setIsTranslating(false) diff --git a/src/renderer/src/components/Popups/UserPopup.tsx b/src/renderer/src/components/Popups/UserPopup.tsx index ac4b9eca9..562f7e681 100644 --- a/src/renderer/src/components/Popups/UserPopup.tsx +++ b/src/renderer/src/components/Popups/UserPopup.tsx @@ -49,7 +49,7 @@ const PopupContainer: React.FC = ({ resolve }) => { dispatch(setAvatar(emoji)) setEmojiPickerOpen(false) } catch (error: any) { - window.message.error(error.message) + window.toast.error(error.message) } } const handleReset = async () => { @@ -58,7 +58,7 @@ const PopupContainer: React.FC = ({ resolve }) => { dispatch(setAvatar(DefaultAvatar)) setDropdownOpen(false) } catch (error: any) { - window.message.error(error.message) + window.toast.error(error.message) } } const items = [ @@ -83,7 +83,7 @@ const PopupContainer: React.FC = ({ resolve }) => { dispatch(setAvatar(await ImageStorage.get('avatar'))) setDropdownOpen(false) } catch (error: any) { - window.message.error(error.message) + window.toast.error(error.message) } }}> {t('settings.general.image_upload')} diff --git a/src/renderer/src/components/S3BackupManager.tsx b/src/renderer/src/components/S3BackupManager.tsx index e31b988ae..4327a6c21 100644 --- a/src/renderer/src/components/S3BackupManager.tsx +++ b/src/renderer/src/components/S3BackupManager.tsx @@ -37,7 +37,7 @@ export function S3BackupManager({ visible, onClose, s3Config, restoreMethod }: S const fetchBackupFiles = useCallback(async () => { if (!endpoint || !region || !bucket || !accessKeyId || !secretAccessKey) { - window.message.error(t('settings.data.s3.manager.config.incomplete')) + window.toast.error(t('settings.data.s3.manager.config.incomplete')) return } @@ -61,7 +61,7 @@ export function S3BackupManager({ visible, onClose, s3Config, restoreMethod }: S total: files.length })) } catch (error: any) { - window.message.error(t('settings.data.s3.manager.files.fetch.error', { message: error.message })) + window.toast.error(t('settings.data.s3.manager.files.fetch.error', { message: error.message })) } finally { setLoading(false) } @@ -84,12 +84,12 @@ export function S3BackupManager({ visible, onClose, s3Config, restoreMethod }: S const handleDeleteSelected = async () => { if (selectedRowKeys.length === 0) { - window.message.warning(t('settings.data.s3.manager.select.warning')) + window.toast.warning(t('settings.data.s3.manager.select.warning')) return } if (!endpoint || !region || !bucket || !accessKeyId || !secretAccessKey) { - window.message.error(t('settings.data.s3.manager.config.incomplete')) + window.toast.error(t('settings.data.s3.manager.config.incomplete')) return } @@ -118,13 +118,11 @@ export function S3BackupManager({ visible, onClose, s3Config, restoreMethod }: S maxBackups: 0 }) } - window.message.success( - t('settings.data.s3.manager.delete.success.multiple', { count: selectedRowKeys.length }) - ) + window.toast.success(t('settings.data.s3.manager.delete.success.multiple', { count: selectedRowKeys.length })) setSelectedRowKeys([]) await fetchBackupFiles() } catch (error: any) { - window.message.error(t('settings.data.s3.manager.delete.error', { message: error.message })) + window.toast.error(t('settings.data.s3.manager.delete.error', { message: error.message })) } finally { setDeleting(false) } @@ -134,7 +132,7 @@ export function S3BackupManager({ visible, onClose, s3Config, restoreMethod }: S const handleDeleteSingle = async (fileName: string) => { if (!endpoint || !region || !bucket || !accessKeyId || !secretAccessKey) { - window.message.error(t('settings.data.s3.manager.config.incomplete')) + window.toast.error(t('settings.data.s3.manager.config.incomplete')) return } @@ -160,10 +158,10 @@ export function S3BackupManager({ visible, onClose, s3Config, restoreMethod }: S syncInterval: 0, maxBackups: 0 }) - window.message.success(t('settings.data.s3.manager.delete.success.single')) + window.toast.success(t('settings.data.s3.manager.delete.success.single')) await fetchBackupFiles() } catch (error: any) { - window.message.error(t('settings.data.s3.manager.delete.error', { message: error.message })) + window.toast.error(t('settings.data.s3.manager.delete.error', { message: error.message })) } finally { setDeleting(false) } @@ -173,7 +171,7 @@ export function S3BackupManager({ visible, onClose, s3Config, restoreMethod }: S const handleRestore = async (fileName: string) => { if (!endpoint || !region || !bucket || !accessKeyId || !secretAccessKey) { - window.message.error(t('settings.data.s3.manager.config.incomplete')) + window.toast.error(t('settings.data.s3.manager.config.incomplete')) return } @@ -188,10 +186,10 @@ export function S3BackupManager({ visible, onClose, s3Config, restoreMethod }: S setRestoring(true) try { await (restoreMethod || restoreFromS3)(fileName) - window.message.success(t('settings.data.s3.restore.success')) + window.toast.success(t('settings.data.s3.restore.success')) onClose() // 关闭模态框 } catch (error: any) { - window.message.error(t('settings.data.s3.restore.error', { message: error.message })) + window.toast.error(t('settings.data.s3.restore.error', { message: error.message })) } finally { setRestoring(false) } diff --git a/src/renderer/src/components/S3Modals.tsx b/src/renderer/src/components/S3Modals.tsx index 75c8b31b3..96d72406f 100644 --- a/src/renderer/src/components/S3Modals.tsx +++ b/src/renderer/src/components/S3Modals.tsx @@ -114,7 +114,7 @@ export function useS3RestoreModal({ const showRestoreModal = useCallback(async () => { if (!endpoint || !region || !bucket || !accessKeyId || !secretAccessKey) { - window.message.error({ content: t('settings.data.s3.manager.config.incomplete'), key: 's3-error' }) + window.toast.error(t('settings.data.s3.manager.config.incomplete')) return } @@ -135,10 +135,7 @@ export function useS3RestoreModal({ }) setBackupFiles(files) } catch (error: any) { - window.message.error({ - content: t('settings.data.s3.manager.files.fetch.error', { message: error.message }), - key: 'list-files-error' - }) + window.toast.error(t('settings.data.s3.manager.files.fetch.error', { message: error.message })) } finally { setLoadingFiles(false) } @@ -146,12 +143,9 @@ export function useS3RestoreModal({ const handleRestore = useCallback(async () => { if (!selectedFile || !endpoint || !region || !bucket || !accessKeyId || !secretAccessKey) { - window.message.error({ - content: !selectedFile - ? t('settings.data.s3.restore.file.required') - : t('settings.data.s3.restore.config.incomplete'), - key: 'restore-error' - }) + window.toast.error( + !selectedFile ? t('settings.data.s3.restore.file.required') : t('settings.data.s3.restore.config.incomplete') + ) return } @@ -177,13 +171,10 @@ export function useS3RestoreModal({ maxBackups: 0, skipBackupFile: false }) - window.message.success({ content: t('message.restore.success'), key: 's3-restore' }) + window.toast.success(t('message.restore.success')) setIsRestoreModalVisible(false) } catch (error: any) { - window.message.error({ - content: t('settings.data.s3.restore.error', { message: error.message }), - key: 'restore-error' - }) + window.toast.error(t('settings.data.s3.restore.error', { message: error.message })) } finally { setRestoring(false) } diff --git a/src/renderer/src/components/ToastPortal.tsx b/src/renderer/src/components/ToastPortal.tsx index 655dafccf..b149ae6fd 100644 --- a/src/renderer/src/components/ToastPortal.tsx +++ b/src/renderer/src/components/ToastPortal.tsx @@ -14,12 +14,17 @@ export const ToastPortal = () => { return createPortal( , document.body diff --git a/src/renderer/src/components/TopView/index.tsx b/src/renderer/src/components/TopView/index.tsx index a3e6da2bb..d37f9d507 100644 --- a/src/renderer/src/components/TopView/index.tsx +++ b/src/renderer/src/components/TopView/index.tsx @@ -1,12 +1,12 @@ // import { loggerService } from '@logger' -import { addToast, closeAll, closeToast, getToastQueue, isToastClosing } from '@heroui/toast' import TopViewMinappContainer from '@renderer/components/MinApp/TopViewMinappContainer' import { useAppInit } from '@renderer/hooks/useAppInit' import { useShortcuts } from '@renderer/hooks/useShortcuts' -import { message, Modal } from 'antd' +import { Modal } from 'antd' import React, { PropsWithChildren, useCallback, useEffect, useRef, useState } from 'react' import { Box } from '../Layout' +import { getToastUtilities } from './toast' let onPop = () => {} let onShow = ({ element, id }: { element: React.FC | React.ReactNode; id: string }) => { @@ -34,7 +34,6 @@ const TopViewContainer: React.FC = ({ children }) => { const elementsRef = useRef([]) elementsRef.current = elements - const [messageApi, messageContextHolder] = message.useMessage() const [modal, modalContextHolder] = Modal.useModal() const { shortcuts } = useShortcuts() const enableQuitFullScreen = shortcuts.find((item) => item.key === 'exit_fullscreen')?.enabled @@ -42,16 +41,9 @@ const TopViewContainer: React.FC = ({ children }) => { useAppInit() useEffect(() => { - window.message = messageApi window.modal = modal - window.toast = { - getToastQueue: getToastQueue, - addToast: addToast, - closeToast: closeToast, - closeAll: closeAll, - isToastClosing: isToastClosing - } - }, [messageApi, modal]) + window.toast = getToastUtilities() + }, [modal]) onPop = () => { const views = [...elementsRef.current] @@ -104,7 +96,6 @@ const TopViewContainer: React.FC = ({ children }) => { return ( <> {children} - {messageContextHolder} {modalContextHolder} {elements.map(({ element: Element, id }) => ( diff --git a/src/renderer/src/components/TopView/toast.ts b/src/renderer/src/components/TopView/toast.ts new file mode 100644 index 000000000..b5108315f --- /dev/null +++ b/src/renderer/src/components/TopView/toast.ts @@ -0,0 +1,72 @@ +import { addToast, closeAll, closeToast, getToastQueue, isToastClosing } from '@heroui/toast' +import { RequireSome } from '@renderer/types' + +type AddToastProps = Parameters[0] +type ToastPropsColored = Omit + +const createToast = (color: 'danger' | 'success' | 'warning' | 'default') => { + return (arg: ToastPropsColored | string): string | null => { + if (typeof arg === 'string') { + return addToast({ color, title: arg }) + } else { + return addToast({ color, ...arg }) + } + } +} + +// syntatic sugar, oh yeah + +/** + * Display an error toast notification with red color + * @param arg - Toast content (string) or toast options object + * @returns Toast ID or null + */ +export const error = createToast('danger') + +/** + * Display a success toast notification with green color + * @param arg - Toast content (string) or toast options object + * @returns Toast ID or null + */ +export const success = createToast('success') + +/** + * Display a warning toast notification with yellow color + * @param arg - Toast content (string) or toast options object + * @returns Toast ID or null + */ +export const warning = createToast('warning') + +/** + * Display an info toast notification with default color + * @param arg - Toast content (string) or toast options object + * @returns Toast ID or null + */ +export const info = createToast('default') + +/** + * Display a loading toast notification that resolves with a promise + * @param args - Toast options object containing a promise to resolve + * @returns Toast ID or null + */ +export const loading = (args: RequireSome) => { + // Disappear immediately by default + if (args.timeout === undefined) { + args.timeout = 1 + } + return addToast(args) +} + +export const getToastUtilities = () => + ({ + getToastQueue, + addToast, + closeToast, + closeAll, + isToastClosing, + error, + success, + warning, + info, + loading + }) as const diff --git a/src/renderer/src/components/TranslateButton.tsx b/src/renderer/src/components/TranslateButton.tsx index 6f074a079..f8231d348 100644 --- a/src/renderer/src/components/TranslateButton.tsx +++ b/src/renderer/src/components/TranslateButton.tsx @@ -52,10 +52,7 @@ const TranslateButton: FC = ({ text, onTranslated, disabled, style, isLoa onTranslated(translatedText) } catch (error) { logger.error('Translation failed:', error as Error) - window.message.error({ - content: t('translate.error.failed'), - key: 'translate-message' - }) + window.toast.error(t('translate.error.failed')) } finally { setIsTranslating(false) } diff --git a/src/renderer/src/components/WebdavBackupManager.tsx b/src/renderer/src/components/WebdavBackupManager.tsx index f04cbef04..6683cacfe 100644 --- a/src/renderer/src/components/WebdavBackupManager.tsx +++ b/src/renderer/src/components/WebdavBackupManager.tsx @@ -60,7 +60,7 @@ export function WebdavBackupManager({ const fetchBackupFiles = useCallback(async () => { if (!webdavHost) { - window.message.error(t('message.error.invalid.webdav')) + window.toast.error(t('message.error.invalid.webdav')) return } @@ -78,7 +78,7 @@ export function WebdavBackupManager({ total: files.length })) } catch (error: any) { - window.message.error(`${t('settings.data.webdav.backup.manager.fetch.error')}: ${error.message}`) + window.toast.error(`${t('settings.data.webdav.backup.manager.fetch.error')}: ${error.message}`) } finally { setLoading(false) } @@ -106,7 +106,7 @@ export function WebdavBackupManager({ } if (!webdavHost) { - window.message.error(t('message.error.invalid.webdav')) + window.toast.error(t('message.error.invalid.webdav')) return } @@ -129,13 +129,13 @@ export function WebdavBackupManager({ webdavPath } as WebdavConfig) } - window.message.success( + window.toast.success( t('settings.data.webdav.backup.manager.delete.success.multiple', { count: selectedRowKeys.length }) ) setSelectedRowKeys([]) await fetchBackupFiles() } catch (error: any) { - window.message.error(`${t('settings.data.webdav.backup.manager.delete.error')}: ${error.message}`) + window.toast.error(`${t('settings.data.webdav.backup.manager.delete.error')}: ${error.message}`) } finally { setDeleting(false) } @@ -145,7 +145,7 @@ export function WebdavBackupManager({ const handleDeleteSingle = async (fileName: string) => { if (!webdavHost) { - window.message.error(t('message.error.invalid.webdav')) + window.toast.error(t('message.error.invalid.webdav')) return } @@ -165,10 +165,10 @@ export function WebdavBackupManager({ webdavPass, webdavPath } as WebdavConfig) - window.message.success(t('settings.data.webdav.backup.manager.delete.success.single')) + window.toast.success(t('settings.data.webdav.backup.manager.delete.success.single')) await fetchBackupFiles() } catch (error: any) { - window.message.error(`${t('settings.data.webdav.backup.manager.delete.error')}: ${error.message}`) + window.toast.error(`${t('settings.data.webdav.backup.manager.delete.error')}: ${error.message}`) } finally { setDeleting(false) } @@ -178,7 +178,7 @@ export function WebdavBackupManager({ const handleRestore = async (fileName: string) => { if (!webdavHost) { - window.message.error(customLabels?.invalidConfigMessage || t('message.error.invalid.webdav')) + window.toast.error(customLabels?.invalidConfigMessage || t('message.error.invalid.webdav')) return } @@ -193,10 +193,10 @@ export function WebdavBackupManager({ setRestoring(true) try { await (restoreMethod || restoreFromWebdav)(fileName) - window.message.success(t('settings.data.webdav.backup.manager.restore.success')) + window.toast.success(t('settings.data.webdav.backup.manager.restore.success')) onClose() // 关闭模态框 } catch (error: any) { - window.message.error(`${t('settings.data.webdav.backup.manager.restore.error')}: ${error.message}`) + window.toast.error(`${t('settings.data.webdav.backup.manager.restore.error')}: ${error.message}`) } finally { setRestoring(false) } diff --git a/src/renderer/src/components/__tests__/CopyButton.test.tsx b/src/renderer/src/components/__tests__/CopyButton.test.tsx index dabe863d3..1087ee74f 100644 --- a/src/renderer/src/components/__tests__/CopyButton.test.tsx +++ b/src/renderer/src/components/__tests__/CopyButton.test.tsx @@ -10,8 +10,8 @@ const mockClipboard = { writeText: mockWriteText } -// Mock window.message -const mockMessage = { +// Mock window.toast +const mockedToast = { success: vi.fn(), error: vi.fn() } @@ -33,7 +33,7 @@ describe('CopyButton', () => { beforeEach(() => { // Setup mocks Object.assign(navigator, { clipboard: mockClipboard }) - Object.assign(window, { message: mockMessage }) + Object.assign(window, { toast: mockedToast }) // Clear all mocks vi.clearAllMocks() @@ -103,8 +103,8 @@ describe('CopyButton', () => { const clickableElement = copyIcon?.parentElement await userEvent.click(clickableElement!) - expect(mockMessage.success).toHaveBeenCalledWith('复制成功') - expect(mockMessage.error).not.toHaveBeenCalled() + expect(mockedToast.success).toHaveBeenCalledWith('复制成功') + expect(mockedToast.error).not.toHaveBeenCalled() }) it('should show error message when copy fails', async () => { @@ -116,8 +116,8 @@ describe('CopyButton', () => { const clickableElement = copyIcon?.parentElement await userEvent.click(clickableElement!) - expect(mockMessage.error).toHaveBeenCalledWith('复制失败') - expect(mockMessage.success).not.toHaveBeenCalled() + expect(mockedToast.error).toHaveBeenCalledWith('复制失败') + expect(mockedToast.success).not.toHaveBeenCalled() }) it('should apply custom size to icon and label', () => { diff --git a/src/renderer/src/components/__tests__/InputEmbeddingDimension.test.tsx b/src/renderer/src/components/__tests__/InputEmbeddingDimension.test.tsx index babc67e6e..946b08780 100644 --- a/src/renderer/src/components/__tests__/InputEmbeddingDimension.test.tsx +++ b/src/renderer/src/components/__tests__/InputEmbeddingDimension.test.tsx @@ -98,9 +98,9 @@ vi.mock('@renderer/components/Icons', () => ({ ) })) -// Mock window.message +// Mock window.toast Object.assign(window, { - message: { + toast: { error: vi.fn(), success: vi.fn() } @@ -195,7 +195,7 @@ describe('InputEmbeddingDimension', () => { // We can skip this check to be explicit. await userEvent.click(refreshButton, { pointerEventsCheck: 0 }) - expect(window.message.error).not.toHaveBeenCalled() + expect(window.toast.error).not.toHaveBeenCalled() }) it('should show error when API call fails', async () => { @@ -208,7 +208,7 @@ describe('InputEmbeddingDimension', () => { await user.click(refreshButton) await waitFor(() => { - expect(window.message.error).toHaveBeenCalledWith('获取嵌入维度失败\nAPI Error') + expect(window.toast.error).toHaveBeenCalledWith('获取嵌入维度失败\nAPI Error') }) }) diff --git a/src/renderer/src/env.d.ts b/src/renderer/src/env.d.ts index a306534e9..cf8f8a4cb 100644 --- a/src/renderer/src/env.d.ts +++ b/src/renderer/src/env.d.ts @@ -2,10 +2,11 @@ import { addToast, closeAll, closeToast, getToastQueue, isToastClosing } from '@heroui/toast' import type KeyvStorage from '@kangfenmao/keyv-storage' -import { MessageInstance } from 'antd/es/message/interface' import { HookAPI } from 'antd/es/modal/useModal' import { NavigateFunction } from 'react-router-dom' +import { error, info, loading, success, warning } from './components/TopView/toast' + interface ImportMetaEnv { VITE_RENDERER_INTEGRATED_MODEL: string } @@ -17,10 +18,6 @@ interface ImportMeta { declare global { interface Window { root: HTMLElement - /** - * @deprecated - */ - message: MessageInstance modal: HookAPI keyv: KeyvStorage store: any @@ -31,6 +28,11 @@ declare global { closeToast: typeof closeToast closeAll: typeof closeAll isToastClosing: typeof isToastClosing + error: typeof error + success: typeof success + warning: typeof warning + info: typeof info + loading: typeof loading } } } diff --git a/src/renderer/src/hooks/useAssistant.ts b/src/renderer/src/hooks/useAssistant.ts index bc4147d52..258048f0b 100644 --- a/src/renderer/src/hooks/useAssistant.ts +++ b/src/renderer/src/hooks/useAssistant.ts @@ -59,7 +59,7 @@ export function useAssistants() { dispatch(insertAssistant({ index: index + 1, assistant: _assistant })) } catch (e) { logger.error('Failed to insert assistant', e as Error) - window.message.error(t('message.error.copy')) + window.toast.error(t('message.error.copy')) } } return _assistant diff --git a/src/renderer/src/hooks/useChatContext.ts b/src/renderer/src/hooks/useChatContext.ts index 613a75c15..415ab0c61 100644 --- a/src/renderer/src/hooks/useChatContext.ts +++ b/src/renderer/src/hooks/useChatContext.ts @@ -97,7 +97,7 @@ export const useChatContext = (activeTopic: Topic) => { const handleMultiSelectAction = useCallback( async (actionType: string, messageIds: string[]) => { if (messageIds.length === 0) { - window.message.warning(t('chat.multiple.select.empty')) + window.toast.warning(t('chat.multiple.select.empty')) return } @@ -115,11 +115,11 @@ export const useChatContext = (activeTopic: Topic) => { onOk: async () => { try { await Promise.all(messageIds.map((messageId) => deleteMessage(messageId))) - window.message.success(t('message.delete.success')) + window.toast.success(t('message.delete.success')) handleToggleMultiSelectMode(false) } catch (error) { logger.error('Failed to delete messages:', error as Error) - window.message.error(t('message.delete.failed')) + window.toast.error(t('message.delete.failed')) } } }) @@ -142,7 +142,7 @@ export const useChatContext = (activeTopic: Topic) => { .join('\n\n---\n\n') const fileName = `chat_export_${new Date().toISOString().slice(0, 19).replace(/[T:]/g, '-')}.md` await window.api.file.save(fileName, contentToSave) - window.message.success({ content: t('message.save.success.title'), key: 'save-messages' }) + window.toast.success(t('message.save.success.title')) handleToggleMultiSelectMode(false) } else { // 这个分支不会进入 因为 messageIds.length === 0 已提前返回,需要简化掉 @@ -165,7 +165,7 @@ export const useChatContext = (activeTopic: Topic) => { }) .join('\n\n---\n\n') navigator.clipboard.writeText(contentToCopy) - window.message.success({ content: t('message.copied'), key: 'copy-messages' }) + window.toast.success(t('message.copied')) handleToggleMultiSelectMode(false) } else { // 和上面一样 diff --git a/src/renderer/src/hooks/useFiles.ts b/src/renderer/src/hooks/useFiles.ts index 59a9c99cf..19e6d98e7 100644 --- a/src/renderer/src/hooks/useFiles.ts +++ b/src/renderer/src/hooks/useFiles.ts @@ -68,12 +68,11 @@ export const useFiles = (props?: Props) => { } if (supportedFiles.length !== _files.length) { - window.message.info({ - key: 'file_not_supported', - content: t('chat.input.file_not_supported_count', { + window.toast.info( + t('chat.input.file_not_supported_count', { count: _files.length - supportedFiles.length }) - }) + ) } return supportedFiles } else { diff --git a/src/renderer/src/hooks/useFullScreenNotice.ts b/src/renderer/src/hooks/useFullScreenNotice.ts index 69af2da4b..7a228eb72 100644 --- a/src/renderer/src/hooks/useFullScreenNotice.ts +++ b/src/renderer/src/hooks/useFullScreenNotice.ts @@ -9,10 +9,9 @@ export function useFullScreenNotice() { useEffect(() => { const cleanup = window.electron.ipcRenderer.on(IpcChannel.FullscreenStatusChanged, (_, isFullscreen) => { if (isWin && isFullscreen) { - window.message.info({ - content: t('common.fullscreen'), - duration: 3, - key: 'fullscreen-notification' + window.toast.info({ + title: t('common.fullscreen'), + timeout: 3000 }) } }) diff --git a/src/renderer/src/hooks/useKnowledgeBaseForm.ts b/src/renderer/src/hooks/useKnowledgeBaseForm.ts index 59b75852c..f641a4fa5 100644 --- a/src/renderer/src/hooks/useKnowledgeBaseForm.ts +++ b/src/renderer/src/hooks/useKnowledgeBaseForm.ts @@ -126,7 +126,7 @@ export const useKnowledgeBaseForm = (base?: KnowledgeBase) => { if (!value || (newBase.chunkSize && newBase.chunkSize > value)) { setNewBase((prev) => ({ ...prev, chunkOverlap: value || undefined })) } else { - window.message.error(t('message.error.chunk_overlap_too_large')) + window.toast.error(t('message.error.chunk_overlap_too_large')) } }, [newBase.chunkSize, t] diff --git a/src/renderer/src/hooks/useOcr.ts b/src/renderer/src/hooks/useOcr.ts index 93350fda1..284b31695 100644 --- a/src/renderer/src/hooks/useOcr.ts +++ b/src/renderer/src/hooks/useOcr.ts @@ -1,7 +1,6 @@ import { loggerService } from '@logger' import * as OcrService from '@renderer/services/ocr/OcrService' import { ImageFileMetadata, isImageFileMetadata, SupportedOcrFile } from '@renderer/types' -import { uuid } from '@renderer/utils' import { formatErrorMessage } from '@renderer/utils/error' import { useCallback } from 'react' import { useTranslation } from 'react-i18next' @@ -35,23 +34,23 @@ export const useOcr = () => { * @throws 当文件类型不支持或OCR失败时抛出错误 */ const ocr = async (file: SupportedOcrFile) => { - const key = uuid() - window.message.loading({ content: t('ocr.processing'), key, duration: 0 }) - // await to keep show loading message - try { - if (isImageFileMetadata(file)) { - return await ocrImage(file) - } else { - // @ts-expect-error all types should be covered - throw new Error(t('ocr.file.not_supported', { type: file.type })) + const _ocr = async () => { + try { + if (isImageFileMetadata(file)) { + return ocrImage(file) + } else { + // @ts-expect-error all types should be covered + throw new Error(t('ocr.file.not_supported', { type: file.type })) + } + } catch (e) { + logger.error('Failed to ocr.', e as Error) + window.toast.error(t('ocr.error.unknown') + ': ' + formatErrorMessage(e)) + throw e } - } catch (e) { - logger.error('Failed to ocr.', e as Error) - window.message.error(t('ocr.error.unknown') + ': ' + formatErrorMessage(e)) - throw e - } finally { - window.message.destroy(key) } + const promise = _ocr() + window.toast.loading({ title: t('ocr.processing'), promise }) + return promise } return { diff --git a/src/renderer/src/hooks/useOcrProvider.tsx b/src/renderer/src/hooks/useOcrProvider.tsx index 38afaf81b..0a40f047f 100644 --- a/src/renderer/src/hooks/useOcrProvider.tsx +++ b/src/renderer/src/hooks/useOcrProvider.tsx @@ -39,7 +39,7 @@ export const useOcrProviders = () => { if (providers.some((p) => p.id === provider.id)) { const msg = `Provider with id ${provider.id} already exists` logger.error(msg) - window.message.error(t('ocr.error.provider.existing')) + window.toast.error(t('ocr.error.provider.existing')) throw new Error(msg) } dispatch(addOcrProvider(provider)) @@ -56,7 +56,7 @@ export const useOcrProviders = () => { if (isBuiltinOcrProviderId(id)) { const msg = `Cannot remove builtin provider ${id}` logger.error(msg) - window.message.error(t('ocr.error.provider.cannot_remove_builtin')) + window.toast.error(t('ocr.error.provider.cannot_remove_builtin')) throw new Error(msg) } @@ -123,19 +123,19 @@ export const useOcrProvider = (id: string) => { // safely fallback if (!provider) { logger.error(`Ocr Provider ${id} not found`) - window.message.error(t('ocr.error.provider.not_found')) + window.toast.error(t('ocr.error.provider.not_found')) if (isBuiltinOcrProviderId(id)) { try { addProvider(BUILTIN_OCR_PROVIDERS_MAP[id]) } catch (e) { logger.warn(`Add ${BUILTIN_OCR_PROVIDERS_MAP[id].name} failed. Just use temp provider from config.`) - window.message.warning(t('ocr.warning.provider.fallback', { name: BUILTIN_OCR_PROVIDERS_MAP[id].name })) + window.toast.warning(t('ocr.warning.provider.fallback', { name: BUILTIN_OCR_PROVIDERS_MAP[id].name })) } finally { provider = BUILTIN_OCR_PROVIDERS_MAP[id] } } else { logger.warn(`Fallback to tesseract`) - window.message.warning(t('ocr.warning.provider.fallback', { name: 'Tesseract' })) + window.toast.warning(t('ocr.warning.provider.fallback', { name: 'Tesseract' })) provider = BUILTIN_OCR_PROVIDERS_MAP.tesseract } } diff --git a/src/renderer/src/hooks/useRuntime.ts b/src/renderer/src/hooks/useRuntime.ts index c82d81521..5dc0924ef 100644 --- a/src/renderer/src/hooks/useRuntime.ts +++ b/src/renderer/src/hooks/useRuntime.ts @@ -9,7 +9,7 @@ export function modelGenerating() { const generating = store.getState().runtime.generating if (generating) { - window.message.warning({ content: i18n.t('message.switch.disabled'), key: 'model-generating' }) + window.toast.warning(i18n.t('message.switch.disabled')) return Promise.reject() } diff --git a/src/renderer/src/hooks/useUpdateHandler.ts b/src/renderer/src/hooks/useUpdateHandler.ts index 4724ddd68..0b5282f0c 100644 --- a/src/renderer/src/hooks/useUpdateHandler.ts +++ b/src/renderer/src/hooks/useUpdateHandler.ts @@ -21,7 +21,7 @@ export default function useUpdateHandler() { ipcRenderer.on(IpcChannel.UpdateNotAvailable, () => { dispatch(setUpdateState({ checking: false })) if (window.location.hash.includes('settings/about')) { - window.message.success(t('settings.about.updateNotAvailable')) + window.toast.success(t('settings.about.updateNotAvailable')) } }), ipcRenderer.on(IpcChannel.UpdateAvailable, (_, releaseInfo: UpdateInfo) => { diff --git a/src/renderer/src/pages/agents/AgentsPage.tsx b/src/renderer/src/pages/agents/AgentsPage.tsx index c0c9093e7..ccf519715 100644 --- a/src/renderer/src/pages/agents/AgentsPage.tsx +++ b/src/renderer/src/pages/agents/AgentsPage.tsx @@ -170,10 +170,7 @@ const AgentsPage: FC = () => { try { await ImportAgentPopup.show() } catch (error) { - window.message.error({ - content: error instanceof Error ? error.message : t('message.agents.import.error'), - key: 'agents-import-error' - }) + window.toast.error(error instanceof Error ? error.message : t('message.agents.import.error')) } } diff --git a/src/renderer/src/pages/agents/components/ImportAgentPopup.tsx b/src/renderer/src/pages/agents/components/ImportAgentPopup.tsx index f638545ac..c2ec14051 100644 --- a/src/renderer/src/pages/agents/components/ImportAgentPopup.tsx +++ b/src/renderer/src/pages/agents/components/ImportAgentPopup.tsx @@ -74,19 +74,13 @@ const PopupContainer: React.FC = ({ resolve }) => { addAgent(newAgent) } - window.message.success({ - content: t('message.agents.imported'), - key: 'agents-imported' - }) + window.toast.success(t('message.agents.imported')) setTimeoutTimer('onFinish', () => EventEmitter.emit(EVENT_NAMES.SHOW_ASSISTANTS), 0) setOpen(false) resolve(agents) } catch (error) { - window.message.error({ - content: error instanceof Error ? error.message : t('message.agents.import.error'), - key: 'agents-import-error' - }) + window.toast.error(error instanceof Error ? error.message : t('message.agents.import.error')) } finally { setLoading(false) } diff --git a/src/renderer/src/pages/code/CodeToolsPage.tsx b/src/renderer/src/pages/code/CodeToolsPage.tsx index f4cf60bb9..bee260efa 100644 --- a/src/renderer/src/pages/code/CodeToolsPage.tsx +++ b/src/renderer/src/pages/code/CodeToolsPage.tsx @@ -109,7 +109,7 @@ const CodeToolsPage: FC = () => { const bunExists = await window.api.isBinaryExist('bun') dispatch(setIsBunInstalled(bunExists)) } catch (error) { - logger.error('检查 bun 安装状态失败:', error as Error) + logger.error('Failed to check bun installation status:', error as Error) dispatch(setIsBunInstalled(false)) } }, [dispatch]) @@ -120,16 +120,10 @@ const CodeToolsPage: FC = () => { setIsInstallingBun(true) await window.api.installBunBinary() dispatch(setIsBunInstalled(true)) - window.message.success({ - content: t('settings.mcp.installSuccess'), - key: 'bun-install-message' - }) + window.toast.success(t('settings.mcp.installSuccess')) } catch (error: any) { - logger.error('安装 bun 失败:', error as Error) - window.message.error({ - content: `${t('settings.mcp.installError')}: ${error.message}`, - key: 'bun-install-message' - }) + logger.error('Failed to install bun:', error as Error) + window.toast.error(`${t('settings.mcp.installError')}: ${error.message}`) } finally { setIsInstallingBun(false) // 重新检查安装状态 @@ -180,7 +174,7 @@ const CodeToolsPage: FC = () => { // 执行启动操作 const executeLaunch = async (env: Record) => { window.api.codeTools.run(selectedCliTool, selectedModel?.id!, currentDirectory, env, { autoUpdateToLatest }) - window.message.success({ content: t('code.launch.success'), key: 'code-launch-message' }) + window.toast.success(t('code.launch.success')) } // 处理启动 @@ -188,7 +182,7 @@ const CodeToolsPage: FC = () => { const validation = validateLaunch() if (!validation.isValid) { - window.message.warning({ content: validation.message, key: 'code-launch-message' }) + window.toast.warning(validation.message || t('code.launch.validation_error')) return } @@ -197,14 +191,14 @@ const CodeToolsPage: FC = () => { try { const env = await prepareLaunchEnvironment() if (!env) { - window.message.error({ content: t('code.model_required'), key: 'code-launch-message' }) + window.toast.error(t('code.model_required')) return } await executeLaunch(env) } catch (error) { logger.error('启动失败:', error as Error) - window.message.error({ content: t('code.launch.error'), key: 'code-launch-message' }) + window.toast.error(t('code.launch.error')) } finally { setIsLaunching(false) } diff --git a/src/renderer/src/pages/history/HistoryPage.tsx b/src/renderer/src/pages/history/HistoryPage.tsx index 7d0857f2e..c8c493740 100644 --- a/src/renderer/src/pages/history/HistoryPage.tsx +++ b/src/renderer/src/pages/history/HistoryPage.tsx @@ -55,7 +55,7 @@ const HistoryPage: FC = () => { // topic 不包含 messages,用到的时候才会获取 const onTopicClick = (topic: Topic | null | undefined) => { if (!topic) { - window.message.error(t('history.error.topic_not_found')) + window.toast.error(t('history.error.topic_not_found')) return } setStack((prev) => [...prev, 'topic']) diff --git a/src/renderer/src/pages/home/Inputbar/AttachmentButton.tsx b/src/renderer/src/pages/home/Inputbar/AttachmentButton.tsx index a67382a88..dd7cd8b30 100644 --- a/src/renderer/src/pages/home/Inputbar/AttachmentButton.tsx +++ b/src/renderer/src/pages/home/Inputbar/AttachmentButton.tsx @@ -61,12 +61,11 @@ const AttachmentButton: FC = ({ } if (supportedFiles.length !== _files.length) { - window.message.info({ - key: 'file_not_supported', - content: t('chat.input.file_not_supported_count', { + window.toast.info( + t('chat.input.file_not_supported_count', { count: _files.length - supportedFiles.length }) - }) + ) } } }, [extensions, files, selecting, setFiles, t]) diff --git a/src/renderer/src/pages/home/Inputbar/Inputbar.tsx b/src/renderer/src/pages/home/Inputbar/Inputbar.tsx index 4012b2a9c..9551c9575 100644 --- a/src/renderer/src/pages/home/Inputbar/Inputbar.tsx +++ b/src/renderer/src/pages/home/Inputbar/Inputbar.tsx @@ -601,12 +601,11 @@ const Inputbar: FC = ({ assistant: _assistant, setActiveTopic, topic }) = const supportedFiles = await filterSupportedFiles(droppedFiles, supportedExts) supportedFiles.length > 0 && setFiles((prevFiles) => [...prevFiles, ...supportedFiles]) if (droppedFiles.length > 0 && supportedFiles.length !== droppedFiles.length) { - window.message.info({ - key: 'file_not_supported', - content: t('chat.input.file_not_supported_count', { + window.toast.info( + t('chat.input.file_not_supported_count', { count: droppedFiles.length - supportedFiles.length }) - }) + ) } } }, diff --git a/src/renderer/src/pages/home/Inputbar/MCPToolsButton.tsx b/src/renderer/src/pages/home/Inputbar/MCPToolsButton.tsx index 7d0b125b9..d6ba841e6 100644 --- a/src/renderer/src/pages/home/Inputbar/MCPToolsButton.tsx +++ b/src/renderer/src/pages/home/Inputbar/MCPToolsButton.tsx @@ -151,7 +151,7 @@ const MCPToolsButton: FC = ({ ref, setInputValue, resizeTextArea, Toolbar if (update.mcpServers.length > 0 && isGeminiModel(model) && isToolUseModeFunction(assistant)) { const provider = getProviderByModel(model) if (isSupportUrlContextProvider(provider) && assistant.enableUrlContext) { - window.message.warning(t('chat.mcp.warning.url_context')) + window.toast.warning(t('chat.mcp.warning.url_context')) update.enableUrlContext = false } if ( @@ -160,7 +160,7 @@ const MCPToolsButton: FC = ({ ref, setInputValue, resizeTextArea, Toolbar isGeminiWebSearchProvider(provider) && assistant.enableWebSearch ) { - window.message.warning(t('chat.mcp.warning.gemini_web_search')) + window.toast.warning(t('chat.mcp.warning.gemini_web_search')) update.enableWebSearch = false } } diff --git a/src/renderer/src/pages/home/Inputbar/UrlContextbutton.tsx b/src/renderer/src/pages/home/Inputbar/UrlContextbutton.tsx index ff92fe116..e5e4c939b 100644 --- a/src/renderer/src/pages/home/Inputbar/UrlContextbutton.tsx +++ b/src/renderer/src/pages/home/Inputbar/UrlContextbutton.tsx @@ -36,7 +36,7 @@ const UrlContextButton: FC = ({ assistant, ToolbarButton }) => { isToolUseModeFunction(assistant) ) { update.enableUrlContext = false - window.message.warning(t('chat.mcp.warning.url_context')) + window.toast.warning(t('chat.mcp.warning.url_context')) } else { update.enableUrlContext = urlContentNewState } diff --git a/src/renderer/src/pages/home/Inputbar/WebSearchButton.tsx b/src/renderer/src/pages/home/Inputbar/WebSearchButton.tsx index 07a9e2910..343b5dbe4 100644 --- a/src/renderer/src/pages/home/Inputbar/WebSearchButton.tsx +++ b/src/renderer/src/pages/home/Inputbar/WebSearchButton.tsx @@ -101,7 +101,7 @@ const WebSearchButton: FC = ({ ref, assistant, ToolbarButton }) => { const provider = getProviderByModel(model) if (!model) { logger.error('Model does not exist.') - window.message.error(t('error.model.not_exists')) + window.toast.error(t('error.model.not_exists')) return } if ( @@ -113,7 +113,7 @@ const WebSearchButton: FC = ({ ref, assistant, ToolbarButton }) => { assistant.mcpServers.length > 0 ) { update.enableWebSearch = false - window.message.warning(t('chat.mcp.warning.gemini_web_search')) + window.toast.warning(t('chat.mcp.warning.gemini_web_search')) } setTimeoutTimer('updateSelectedWebSearchBuiltin', () => updateAssistant(update), 200) }, [assistant, setTimeoutTimer, t, updateAssistant]) diff --git a/src/renderer/src/pages/home/Markdown/Table.tsx b/src/renderer/src/pages/home/Markdown/Table.tsx index 4b3cdaa76..01f325bd4 100644 --- a/src/renderer/src/pages/home/Markdown/Table.tsx +++ b/src/renderer/src/pages/home/Markdown/Table.tsx @@ -32,7 +32,7 @@ const Table: React.FC = ({ children, node, blockId }) => { setCopied(true) }) .catch((error) => { - window.message?.error({ content: `${t('message.copy.failed')}: ${error}`, key: 'copy-table-error' }) + window.toast?.error(`${t('message.copy.failed')}: ${error}`) }) }, [blockId, node?.position, setCopied, t]) diff --git a/src/renderer/src/pages/home/Messages/ChatNavigation.tsx b/src/renderer/src/pages/home/Messages/ChatNavigation.tsx index 7694942b4..8b9e85835 100644 --- a/src/renderer/src/pages/home/Messages/ChatNavigation.tsx +++ b/src/renderer/src/pages/home/Messages/ChatNavigation.tsx @@ -193,21 +193,21 @@ const ChatNavigation: FC = ({ containerId }) => { const assistantMessages = findAssistantMessages() if (userMessages.length === 0 && assistantMessages.length === 0) { - // window.message.info({ content: t('chat.navigation.last'), key: 'navigation-info' }) + // window.toast.info(t('chat.navigation.last')) return scrollToBottom() } const visibleIndex = getCurrentVisibleIndex('down') if (visibleIndex === -1) { - // window.message.info({ content: t('chat.navigation.last'), key: 'navigation-info' }) + // window.toast.info(t('chat.navigation.last')) return scrollToBottom() } const targetIndex = visibleIndex - 1 if (targetIndex < 0) { - // window.message.info({ content: t('chat.navigation.last'), key: 'navigation-info' }) + // window.toast.info(t('chat.navigation.last')) return scrollToBottom() } @@ -219,21 +219,21 @@ const ChatNavigation: FC = ({ containerId }) => { const userMessages = findUserMessages() const assistantMessages = findAssistantMessages() if (userMessages.length === 0 && assistantMessages.length === 0) { - // window.message.info({ content: t('chat.navigation.first'), key: 'navigation-info' }) + // window.toast.info(t('chat.navigation.first')) return scrollToTop() } const visibleIndex = getCurrentVisibleIndex('up') if (visibleIndex === -1) { - // window.message.info({ content: t('chat.navigation.first'), key: 'navigation-info' }) + // window.toast.info(t('chat.navigation.first')) return scrollToTop() } const targetIndex = visibleIndex + 1 if (targetIndex >= userMessages.length) { - // window.message.info({ content: t('chat.navigation.first'), key: 'navigation-info' }) + // window.toast.info(t('chat.navigation.first')) return scrollToTop() } diff --git a/src/renderer/src/pages/home/Messages/CitationsList.tsx b/src/renderer/src/pages/home/Messages/CitationsList.tsx index 279ab1464..5ad6f55d4 100644 --- a/src/renderer/src/pages/home/Messages/CitationsList.tsx +++ b/src/renderer/src/pages/home/Messages/CitationsList.tsx @@ -126,7 +126,7 @@ const CopyButton: React.FC<{ content: string }> = ({ content }) => { .writeText(content) .then(() => { setCopied(true) - window.message.success(t('common.copied')) + window.toast.success(t('common.copied')) }) .catch(() => { message.error(t('message.copy.failed')) diff --git a/src/renderer/src/pages/home/Messages/MessageEditor.tsx b/src/renderer/src/pages/home/Messages/MessageEditor.tsx index 37fd52cd7..3d079fb24 100644 --- a/src/renderer/src/pages/home/Messages/MessageEditor.tsx +++ b/src/renderer/src/pages/home/Messages/MessageEditor.tsx @@ -183,10 +183,7 @@ const MessageBlockEditor: FC = ({ message, topicId, onSave, onResend, onC // 如果有文件,但都不支持 if (files.length > 0 && supportedFiles === 0) { - window.message.info({ - key: 'file_not_supported', - content: t('chat.input.file_not_supported') - }) + window.toast.info(t('chat.input.file_not_supported')) } } } diff --git a/src/renderer/src/pages/home/Messages/MessageImage.tsx b/src/renderer/src/pages/home/Messages/MessageImage.tsx index a0faf46d0..52bb4a227 100644 --- a/src/renderer/src/pages/home/Messages/MessageImage.tsx +++ b/src/renderer/src/pages/home/Messages/MessageImage.tsx @@ -32,10 +32,10 @@ const MessageImage: FC = ({ block }) => { document.body.appendChild(link) link.click() document.body.removeChild(link) - window.message.success(t('message.download.success')) + window.toast.success(t('message.download.success')) } catch (error) { logger.error('下载图片失败:', error as Error) - window.message.error(t('message.download.failed')) + window.toast.error(t('message.download.failed')) } } @@ -84,10 +84,10 @@ const MessageImage: FC = ({ block }) => { break } - window.message.success(t('message.copy.success')) + window.toast.success(t('message.copy.success')) } catch (error) { logger.error('复制图片失败:', error as Error) - window.message.error(t('message.copy.failed')) + window.toast.error(t('message.copy.failed')) } } diff --git a/src/renderer/src/pages/home/Messages/MessageMenubar.tsx b/src/renderer/src/pages/home/Messages/MessageMenubar.tsx index 0b0c623d2..e757bae34 100644 --- a/src/renderer/src/pages/home/Messages/MessageMenubar.tsx +++ b/src/renderer/src/pages/home/Messages/MessageMenubar.tsx @@ -151,7 +151,7 @@ const MessageMenubar: FC = (props) => { navigator.clipboard.writeText(removeTrailingDoubleSpaces(contentToCopy.trimStart())) - window.message.success({ content: t('message.copied'), key: 'copy-message' }) + window.toast.success(t('message.copied')) setCopied(true) }, [message, setCopied, t] // message is needed for message.id and as a fallback. t is for translation. @@ -159,7 +159,7 @@ const MessageMenubar: FC = (props) => { const onNewBranch = useCallback(async () => { EventEmitter.emit(EVENT_NAMES.NEW_BRANCH, index) - window.message.success({ content: t('chat.message.new.branch.created'), key: 'new-branch' }) + window.toast.success(t('chat.message.new.branch.created')) }, [index, t]) const handleResendUserMessage = useCallback( @@ -186,7 +186,7 @@ const MessageMenubar: FC = (props) => { try { await translateText(mainTextContent, language, translationUpdater) } catch (error) { - window.message.error({ content: t('translate.error.failed'), key: 'translate-message' }) + window.toast.error(t('translate.error.failed')) // 理应只有一个 const translationBlocks = findTranslationBlocksById(message.id) logger.silly(`there are ${translationBlocks.length} translation blocks`) @@ -568,9 +568,9 @@ const MessageMenubar: FC = (props) => { if (translationContent) { navigator.clipboard.writeText(translationContent) - window.message.success({ content: t('translate.copied'), key: 'translate-copy' }) + window.toast.success(t('translate.copied')) } else { - window.message.warning({ content: t('translate.empty'), key: 'translate-copy' }) + window.toast.warning(t('translate.empty')) } } } @@ -588,7 +588,7 @@ const MessageMenubar: FC = (props) => { translationBlocks.forEach((blockId) => { if (blockId) removeMessageBlock(message.id, blockId) }) - window.message.success({ content: t('translate.closed'), key: 'translate-close' }) + window.toast.success(t('translate.closed')) } } } diff --git a/src/renderer/src/pages/home/Messages/Messages.tsx b/src/renderer/src/pages/home/Messages/Messages.tsx index 0a8bd5dc9..340e984a2 100644 --- a/src/renderer/src/pages/home/Messages/Messages.tsx +++ b/src/renderer/src/pages/home/Messages/Messages.tsx @@ -194,7 +194,7 @@ const Messages: React.FC = ({ assistant, topic, setActiveTopic, o // You might want to remove the added topic if cloning fails // removeTopic(newTopic.id); // Assuming you have a removeTopic function logger.error(`[NEW_BRANCH] Failed to create topic branch for topic ${newTopic.id}`) - window.message.error(t('message.branch.error')) // Example error message + window.toast.error(t('message.branch.error')) // Example error message } }), EventEmitter.on( @@ -217,19 +217,19 @@ const Messages: React.FC = ({ assistant, topic, setActiveTopic, o dispatch(updateOneBlock({ id: msgBlockId, changes: { content: updatedRaw } })) await dispatch(updateMessageAndBlocksThunk(topic.id, null, [updatedBlock])) - window.message.success({ content: t('code_block.edit.save.success'), key: 'save-code' }) + window.toast.success(t('code_block.edit.save.success')) } catch (error) { logger.error( `Failed to save code block ${codeBlockId} content to message block ${msgBlockId}:`, error as Error ) - window.message.error({ content: t('code_block.edit.save.failed.label'), key: 'save-code-failed' }) + window.toast.error(t('code_block.edit.save.failed.label')) } } else { logger.error( `Failed to save code block ${codeBlockId} content to message block ${msgBlockId}: no such message block or the block doesn't have a content field` ) - window.message.error({ content: t('code_block.edit.save.failed.label'), key: 'save-code-failed' }) + window.toast.error(t('code_block.edit.save.failed.label')) } } ) @@ -270,7 +270,7 @@ const Messages: React.FC = ({ assistant, topic, setActiveTopic, o const lastMessage = last(messages) if (lastMessage) { navigator.clipboard.writeText(getMainTextContent(lastMessage)) - window.message.success(t('message.copy.success')) + window.toast.success(t('message.copy.success')) } }) diff --git a/src/renderer/src/pages/home/Messages/Tools/MessageMcpTool.tsx b/src/renderer/src/pages/home/Messages/Tools/MessageMcpTool.tsx index d3fddd0a1..be5b21104 100644 --- a/src/renderer/src/pages/home/Messages/Tools/MessageMcpTool.tsx +++ b/src/renderer/src/pages/home/Messages/Tools/MessageMcpTool.tsx @@ -176,7 +176,7 @@ const MessageMcpTool: FC = ({ block }) => { try { const success = await window.api.mcp.abortTool(toolResponse.id) if (success) { - window.message.success({ content: t('message.tools.aborted'), key: 'abort-tool' }) + window.toast.success(t('message.tools.aborted')) } else { message.error({ content: t('message.tools.abort_failed'), key: 'abort-tool' }) } @@ -215,10 +215,7 @@ const MessageMcpTool: FC = ({ block }) => { setIsConfirmed(true) confirmToolAction(id) - window.message.success({ - content: t('message.tools.autoApproveEnabled', 'Auto-approve enabled for this tool'), - key: 'auto-approve' - }) + window.toast.success(t('message.tools.autoApproveEnabled', 'Auto-approve enabled for this tool')) } const renderStatusIndicator = (status: string, hasError: boolean) => { diff --git a/src/renderer/src/pages/home/Tabs/TopicsTab.tsx b/src/renderer/src/pages/home/Tabs/TopicsTab.tsx index e34b8d380..f0cdda7e6 100644 --- a/src/renderer/src/pages/home/Tabs/TopicsTab.tsx +++ b/src/renderer/src/pages/home/Tabs/TopicsTab.tsx @@ -85,7 +85,7 @@ const Topics: FC = ({ assistant: _assistant, activeTopic, setActiveTopic, if (topic && name !== topic.name) { const updatedTopic = { ...topic, name, isNameManuallyEdited: true } updateTopic(updatedTopic) - window.message.success(t('common.saved')) + window.toast.success(t('common.saved')) } setEditingTopicId(null) }, @@ -213,7 +213,7 @@ const Topics: FC = ({ assistant: _assistant, activeTopic, setActiveTopic, const updatedTopic = { ...topic, name: summaryText, isNameManuallyEdited: false } updateTopic(updatedTopic) } else { - window.message?.error(t('message.error.fetchTopicName')) + window.toast?.error(t('message.error.fetchTopicName')) } } finally { finishTopicRenaming(topic.id) @@ -348,10 +348,10 @@ const Topics: FC = ({ assistant: _assistant, activeTopic, setActiveTopic, try { const result = await SaveToKnowledgePopup.showForTopic(topic) if (result?.success) { - window.message.success(t('chat.save.topic.knowledge.success', { count: result.savedCount })) + window.toast.success(t('chat.save.topic.knowledge.success', { count: result.savedCount })) } } catch { - window.message.error(t('chat.save.topic.knowledge.error.save_failed')) + window.toast.error(t('chat.save.topic.knowledge.error.save_failed')) } } } diff --git a/src/renderer/src/pages/home/Tabs/components/AssistantItem.tsx b/src/renderer/src/pages/home/Tabs/components/AssistantItem.tsx index 7951700aa..d20c69b05 100644 --- a/src/renderer/src/pages/home/Tabs/components/AssistantItem.tsx +++ b/src/renderer/src/pages/home/Tabs/components/AssistantItem.tsx @@ -301,10 +301,7 @@ function getMenuItems({ agent.id = uuid() agent.type = 'agent' addAgent(agent) - window.message.success({ - content: t('assistants.save.success'), - key: 'save-to-agent' - }) + window.toast.success(t('assistants.save.success')) } }, { diff --git a/src/renderer/src/pages/knowledge/components/AddKnowledgeBasePopup.tsx b/src/renderer/src/pages/knowledge/components/AddKnowledgeBasePopup.tsx index b5b33db51..32a13f8a6 100644 --- a/src/renderer/src/pages/knowledge/components/AddKnowledgeBasePopup.tsx +++ b/src/renderer/src/pages/knowledge/components/AddKnowledgeBasePopup.tsx @@ -38,12 +38,12 @@ const PopupContainer: React.FC = ({ title, resolve }) => { const onOk = async () => { if (!newBase.name?.trim()) { - window.message.error(t('knowledge.name_required')) + window.toast.error(t('knowledge.name_required')) return } if (!newBase.model) { - window.message.error(t('knowledge.embedding_model_required')) + window.toast.error(t('knowledge.embedding_model_required')) return } @@ -62,7 +62,7 @@ const PopupContainer: React.FC = ({ title, resolve }) => { resolve(_newBase) } catch (error) { logger.error('KnowledgeBase creation failed:', error as Error) - window.message.error(t('knowledge.error.failed_to_create') + formatErrorMessage(error)) + window.toast.error(t('knowledge.error.failed_to_create') + formatErrorMessage(error)) } } diff --git a/src/renderer/src/pages/knowledge/components/EditKnowledgeBasePopup.tsx b/src/renderer/src/pages/knowledge/components/EditKnowledgeBasePopup.tsx index 31b3b1ccb..0f1490238 100644 --- a/src/renderer/src/pages/knowledge/components/EditKnowledgeBasePopup.tsx +++ b/src/renderer/src/pages/knowledge/components/EditKnowledgeBasePopup.tsx @@ -53,7 +53,7 @@ const PopupContainer: React.FC = ({ base: _base, resolve }) resolve(migratedBase) } catch (error) { logger.error('KnowledgeBase migration failed:', error as Error) - window.message.error(t('knowledge.migrate.error.failed') + ': ' + formatErrorMessage(error)) + window.toast.error(t('knowledge.migrate.error.failed') + ': ' + formatErrorMessage(error)) } }, [newBase, migrateBase, resolve, t]) @@ -94,7 +94,7 @@ const PopupContainer: React.FC = ({ base: _base, resolve }) resolve(newBase) } catch (error) { logger.error('KnowledgeBase edit failed:', error as Error) - window.message.error(t('knowledge.error.failed_to_edit') + formatErrorMessage(error)) + window.toast.error(t('knowledge.error.failed_to_edit') + formatErrorMessage(error)) } } } diff --git a/src/renderer/src/pages/knowledge/components/KnowledgeSearchItem/hooks.ts b/src/renderer/src/pages/knowledge/components/KnowledgeSearchItem/hooks.ts index 23fbc7fdd..40b651332 100644 --- a/src/renderer/src/pages/knowledge/components/KnowledgeSearchItem/hooks.ts +++ b/src/renderer/src/pages/knowledge/components/KnowledgeSearchItem/hooks.ts @@ -37,7 +37,7 @@ export const useCopyText = () => { message.success(t('message.copied')) } catch (error) { logger.error('Failed to copy text:', error as Error) - window.message.error(t('message.error.copy') || 'Failed to copy text') + window.toast.error(t('message.error.copy') || 'Failed to copy text') } } diff --git a/src/renderer/src/pages/knowledge/components/MigrationInfoTag.tsx b/src/renderer/src/pages/knowledge/components/MigrationInfoTag.tsx index 3e351d85e..7af67f2f3 100644 --- a/src/renderer/src/pages/knowledge/components/MigrationInfoTag.tsx +++ b/src/renderer/src/pages/knowledge/components/MigrationInfoTag.tsx @@ -22,7 +22,7 @@ const MigrationInfoTag: FC<{ base: KnowledgeBase }> = ({ base: _base }) => { await migrateBase(migratedBase, MigrationModeEnum.MigrationToLangChain) } catch (error) { logger.error('KnowledgeBase migration failed:', error as Error) - window.message.error(t('knowledge.migrate.error.failed') + ': ' + formatErrorMessage(error)) + window.toast.error(t('knowledge.migrate.error.failed') + ': ' + formatErrorMessage(error)) } }, [newBase, migrateBase, t]) diff --git a/src/renderer/src/pages/knowledge/items/KnowledgeUrls.tsx b/src/renderer/src/pages/knowledge/items/KnowledgeUrls.tsx index de42c289a..a6b0b649e 100644 --- a/src/renderer/src/pages/knowledge/items/KnowledgeUrls.tsx +++ b/src/renderer/src/pages/knowledge/items/KnowledgeUrls.tsx @@ -76,7 +76,7 @@ const KnowledgeUrls: FC = ({ selectedBase }) => { if (!urlItems.find((item) => item.content === url.trim())) { addUrl(url.trim()) } else { - window.message.success(t('knowledge.url_added')) + window.toast.success(t('knowledge.url_added')) } } catch (e) { // Skip invalid URLs silently @@ -154,7 +154,7 @@ const KnowledgeUrls: FC = ({ selectedBase }) => { label: t('common.copy'), onClick: () => { navigator.clipboard.writeText(item.content as string) - window.message.success(t('message.copied')) + window.toast.success(t('message.copied')) } } ] diff --git a/src/renderer/src/pages/minapps/NewAppButton.tsx b/src/renderer/src/pages/minapps/NewAppButton.tsx index fcfb69f0e..9b2eb3068 100644 --- a/src/renderer/src/pages/minapps/NewAppButton.tsx +++ b/src/renderer/src/pages/minapps/NewAppButton.tsx @@ -36,11 +36,11 @@ const NewAppButton: FC = ({ size = 60 }) => { // Check for duplicate ID if (customApps.some((app: MinAppType) => app.id === values.id)) { - window.message.error(t('settings.miniapps.custom.duplicate_ids', { ids: values.id })) + window.toast.error(t('settings.miniapps.custom.duplicate_ids', { ids: values.id })) return } if (ORIGIN_DEFAULT_MIN_APPS.some((app: MinAppType) => app.id === values.id)) { - window.message.error(t('settings.miniapps.custom.conflicting_ids', { ids: values.id })) + window.toast.error(t('settings.miniapps.custom.conflicting_ids', { ids: values.id })) return } @@ -54,7 +54,7 @@ const NewAppButton: FC = ({ size = 60 }) => { } customApps.push(newApp) await window.api.file.writeWithId('custom-minapps.json', JSON.stringify(customApps, null, 2)) - window.message.success(t('settings.miniapps.custom.save_success')) + window.toast.success(t('settings.miniapps.custom.save_success')) setIsModalVisible(false) form.resetFields() setFileList([]) @@ -62,7 +62,7 @@ const NewAppButton: FC = ({ size = 60 }) => { updateDefaultMinApps(reloadedApps) updateMinapps([...minapps, newApp]) } catch (error) { - window.message.error(t('settings.miniapps.custom.save_error')) + window.toast.error(t('settings.miniapps.custom.save_error')) logger.error('Failed to save custom mini app:', error as Error) } } @@ -77,14 +77,14 @@ const NewAppButton: FC = ({ size = 60 }) => { reader.onload = (event) => { const base64Data = event.target?.result if (typeof base64Data === 'string') { - window.message.success(t('settings.miniapps.custom.logo_upload_success')) + window.toast.success(t('settings.miniapps.custom.logo_upload_success')) form.setFieldValue('logo', base64Data) } } reader.readAsDataURL(file) } catch (error) { logger.error('Failed to read file:', error as Error) - window.message.error(t('settings.miniapps.custom.logo_upload_error')) + window.toast.error(t('settings.miniapps.custom.logo_upload_error')) } } } diff --git a/src/renderer/src/pages/notes/HeaderNavbar.tsx b/src/renderer/src/pages/notes/HeaderNavbar.tsx index d1bdb2908..5f386f4b3 100644 --- a/src/renderer/src/pages/notes/HeaderNavbar.tsx +++ b/src/renderer/src/pages/notes/HeaderNavbar.tsx @@ -37,13 +37,13 @@ const HeaderNavbar = ({ notesTree, getCurrentNoteContent, onToggleStar }) => { const content = getCurrentNoteContent?.() if (content) { await navigator.clipboard.writeText(content) - window.message.success(t('common.copied')) + window.toast.success(t('common.copied')) } else { - window.message.warning(t('notes.no_content_to_copy')) + window.toast.warning(t('notes.no_content_to_copy')) } } catch (error) { logger.error('Failed to copy content:', error as Error) - window.message.error(t('common.copy_failed')) + window.toast.error(t('common.copy_failed')) } }, [getCurrentNoteContent]) diff --git a/src/renderer/src/pages/notes/NotesPage.tsx b/src/renderer/src/pages/notes/NotesPage.tsx index b65c779f4..e1976fcd2 100644 --- a/src/renderer/src/pages/notes/NotesPage.tsx +++ b/src/renderer/src/pages/notes/NotesPage.tsx @@ -507,7 +507,7 @@ const NotesPage: FC = () => { } await sortAllLevels(sortType) if (renamedNode.name !== newName) { - window.message.info(t('notes.rename_changed', { original: newName, final: renamedNode.name })) + window.toast.info(t('notes.rename_changed', { original: newName, final: renamedNode.name })) } } } catch (error) { @@ -528,16 +528,16 @@ const NotesPage: FC = () => { const fileToUpload = files[0] if (!fileToUpload) { - window.message.warning(t('notes.no_file_selected')) + window.toast.warning(t('notes.no_file_selected')) return } // 暂时这么处理 if (files.length > 1) { - window.message.warning(t('notes.only_one_file_allowed')) + window.toast.warning(t('notes.only_one_file_allowed')) } if (!fileToUpload.name.toLowerCase().endsWith('.md')) { - window.message.warning(t('notes.only_markdown')) + window.toast.warning(t('notes.only_markdown')) return } @@ -546,14 +546,14 @@ const NotesPage: FC = () => { throw new Error('No folder path selected') } await uploadNote(fileToUpload, notesPath) - window.message.success(t('notes.upload_success', { count: 1 })) + window.toast.success(t('notes.upload_success', { count: 1 })) } catch (error) { logger.error(`Failed to upload note file ${fileToUpload.name}:`, error as Error) - window.message.error(t('notes.upload_failed', { name: fileToUpload.name })) + window.toast.error(t('notes.upload_failed', { name: fileToUpload.name })) } } catch (error) { logger.error('Failed to handle file upload:', error as Error) - window.message.error(t('notes.upload_failed')) + window.toast.error(t('notes.upload_failed')) } }, [notesPath, t] diff --git a/src/renderer/src/pages/notes/NotesSidebar.tsx b/src/renderer/src/pages/notes/NotesSidebar.tsx index 0eee4e451..045697857 100644 --- a/src/renderer/src/pages/notes/NotesSidebar.tsx +++ b/src/renderer/src/pages/notes/NotesSidebar.tsx @@ -167,17 +167,17 @@ const NotesSidebar: FC = ({ async (note: NotesTreeNode) => { try { if (bases.length === 0) { - window.message.warning(t('chat.save.knowledge.empty.no_knowledge_base')) + window.toast.warning(t('chat.save.knowledge.empty.no_knowledge_base')) return } const result = await SaveToKnowledgePopup.showForNote(note) if (result?.success) { - window.message.success(t('notes.export_success', { count: result.savedCount })) + window.toast.success(t('notes.export_success', { count: result.savedCount })) } } catch (error) { - window.message.error(t('notes.export_failed')) + window.toast.error(t('notes.export_failed')) logger.error(`Failed to export note to knowledge base: ${error}`) } }, diff --git a/src/renderer/src/pages/paintings/AihubmixPage.tsx b/src/renderer/src/pages/paintings/AihubmixPage.tsx index e9bbac452..cf6a264a5 100644 --- a/src/renderer/src/pages/paintings/AihubmixPage.tsx +++ b/src/renderer/src/pages/paintings/AihubmixPage.tsx @@ -135,10 +135,7 @@ const AihubmixPage: FC<{ Options: string[] }> = ({ Options }) => { try { if (!url?.trim()) { logger.error('图像URL为空,可能是提示词违禁') - window.message.warning({ - content: t('message.empty_url'), - key: 'empty-url-warning' - }) + window.toast.warning(t('message.empty_url')) return null } return await window.api.file.download(url) @@ -148,10 +145,7 @@ const AihubmixPage: FC<{ Options: string[] }> = ({ Options }) => { error instanceof Error && (error.message.includes('Failed to parse URL') || error.message.includes('Invalid URL')) ) { - window.message.warning({ - content: t('message.empty_url'), - key: 'empty-url-warning' - }) + window.toast.warning(t('message.empty_url')) } return null } diff --git a/src/renderer/src/pages/paintings/DmxapiPage.tsx b/src/renderer/src/pages/paintings/DmxapiPage.tsx index 21a784397..2ffc9fd95 100644 --- a/src/renderer/src/pages/paintings/DmxapiPage.tsx +++ b/src/renderer/src/pages/paintings/DmxapiPage.tsx @@ -491,10 +491,7 @@ const DmxapiPage: FC<{ Options: string[] }> = ({ Options }) => { urls.map(async (url) => { try { if (!url || url.trim() === '') { - window.message.warning({ - content: t('message.empty_url'), - key: 'empty-url-warning' - }) + window.toast.warning(t('message.empty_url')) return null } return await window.api.file.download(url, true) @@ -503,10 +500,7 @@ const DmxapiPage: FC<{ Options: string[] }> = ({ Options }) => { error instanceof Error && (error.message.includes('Failed to parse URL') || error.message.includes('Invalid URL')) ) { - window.message.warning({ - content: t('message.empty_url'), - key: 'empty-url-warning' - }) + window.toast.warning(t('message.empty_url')) } return null } @@ -591,10 +585,7 @@ const DmxapiPage: FC<{ Options: string[] }> = ({ Options }) => { updatePaintingState({ files: validFiles, urls }) } } else { - window.message.warning({ - content: t('paintings.req_error_text'), - key: 'empty-url-warning' - }) + window.toast.warning(t('paintings.req_error_text')) } } } catch (error) { diff --git a/src/renderer/src/pages/paintings/NewApiPage.tsx b/src/renderer/src/pages/paintings/NewApiPage.tsx index 30bcad5f6..82852e2fe 100644 --- a/src/renderer/src/pages/paintings/NewApiPage.tsx +++ b/src/renderer/src/pages/paintings/NewApiPage.tsx @@ -198,10 +198,7 @@ const NewApiPage: FC<{ Options: string[] }> = ({ Options }) => { try { if (!url?.trim()) { logger.error('图像URL为空') - window.message.warning({ - content: t('message.empty_url'), - key: 'empty-url-warning' - }) + window.toast.warning(t('message.empty_url')) return null } return await window.api.file.download(url) @@ -211,10 +208,7 @@ const NewApiPage: FC<{ Options: string[] }> = ({ Options }) => { error instanceof Error && (error.message.includes('Failed to parse URL') || error.message.includes('Invalid URL')) ) { - window.message.warning({ - content: t('message.empty_url'), - key: 'empty-url-warning' - }) + window.toast.warning(t('message.empty_url')) } return null } @@ -283,7 +277,7 @@ const NewApiPage: FC<{ Options: string[] }> = ({ Options }) => { } else if (mode === 'openai_image_edit') { // -------- Edit Mode -------- if (editImages.length === 0) { - window.message.warning({ content: t('paintings.image_file_required') }) + window.toast.warning(t('paintings.image_file_required')) return } diff --git a/src/renderer/src/pages/paintings/SiliconPage.tsx b/src/renderer/src/pages/paintings/SiliconPage.tsx index 4b4f2bb95..eebd3ccb9 100644 --- a/src/renderer/src/pages/paintings/SiliconPage.tsx +++ b/src/renderer/src/pages/paintings/SiliconPage.tsx @@ -223,10 +223,7 @@ const SiliconPage: FC<{ Options: string[] }> = ({ Options }) => { try { if (!url || url.trim() === '') { logger.error('图像URL为空,可能是提示词违禁') - window.message.warning({ - content: t('message.empty_url'), - key: 'empty-url-warning' - }) + window.toast.warning(t('message.empty_url')) return null } return await window.api.file.download(url) @@ -236,10 +233,7 @@ const SiliconPage: FC<{ Options: string[] }> = ({ Options }) => { error instanceof Error && (error.message.includes('Failed to parse URL') || error.message.includes('Invalid URL')) ) { - window.message.warning({ - content: t('message.empty_url'), - key: 'empty-url-warning' - }) + window.toast.warning(t('message.empty_url')) } return null } diff --git a/src/renderer/src/pages/paintings/ZhipuPage.tsx b/src/renderer/src/pages/paintings/ZhipuPage.tsx index 8b511264c..ea8d7cfce 100644 --- a/src/renderer/src/pages/paintings/ZhipuPage.tsx +++ b/src/renderer/src/pages/paintings/ZhipuPage.tsx @@ -187,10 +187,7 @@ const ZhipuPage: FC<{ Options: string[] }> = ({ Options }) => { imageUrls.map(async (url) => { try { if (!url || url.trim() === '') { - window.message.warning({ - content: t('message.empty_url'), - key: 'empty-url-warning' - }) + window.toast.warning(t('message.empty_url')) return null } return await window.api.file.download(url) @@ -199,10 +196,7 @@ const ZhipuPage: FC<{ Options: string[] }> = ({ Options }) => { error instanceof Error && (error.message.includes('Failed to parse URL') || error.message.includes('Invalid URL')) ) { - window.message.warning({ - content: t('message.empty_url'), - key: 'empty-url-warning' - }) + window.toast.warning(t('message.empty_url')) } return null } diff --git a/src/renderer/src/pages/paintings/components/DynamicFormRender.tsx b/src/renderer/src/pages/paintings/components/DynamicFormRender.tsx index 6c2feab30..7495ad1a5 100644 --- a/src/renderer/src/pages/paintings/components/DynamicFormRender.tsx +++ b/src/renderer/src/pages/paintings/components/DynamicFormRender.tsx @@ -34,7 +34,7 @@ export const DynamicFormRender: React.FC = ({ if (fileOrUrl.match(/^https?:\/\/.+\.(jpg|jpeg|png|gif|webp|svg)(\?.*)?$/i)) { onChange(propertyName, fileOrUrl) } else { - window.message?.error('Invalid image URL format') + window.toast?.error('Invalid image URL format') } } else { // Handle File case - convert to base64 diff --git a/src/renderer/src/pages/paintings/config/DmxapiConfig.ts b/src/renderer/src/pages/paintings/config/DmxapiConfig.ts index 43bdf33c2..090ae7885 100644 --- a/src/renderer/src/pages/paintings/config/DmxapiConfig.ts +++ b/src/renderer/src/pages/paintings/config/DmxapiConfig.ts @@ -95,9 +95,7 @@ export const GetModelGroup = async (): Promise => { } catch { /* empty */ } - window.message.error({ - content: t('paintings.req_error_model') - }) + window.toast.error(t('paintings.req_error_model')) return {} } diff --git a/src/renderer/src/pages/paintings/utils/TokenFluxService.ts b/src/renderer/src/pages/paintings/utils/TokenFluxService.ts index d770427f5..cbe7fa2ca 100644 --- a/src/renderer/src/pages/paintings/utils/TokenFluxService.ts +++ b/src/renderer/src/pages/paintings/utils/TokenFluxService.ts @@ -219,10 +219,7 @@ export class TokenFluxService { try { if (!url?.trim()) { logger.error('Image URL is empty') - window.message.warning({ - content: 'Image URL is empty', - key: 'empty-url-warning' - }) + window.toast.warning('Image URL is empty') return null } return await window.api.file.download(url) diff --git a/src/renderer/src/pages/settings/AboutSettings.tsx b/src/renderer/src/pages/settings/AboutSettings.tsx index 72a545f4b..b79f1cb09 100644 --- a/src/renderer/src/pages/settings/AboutSettings.tsx +++ b/src/renderer/src/pages/settings/AboutSettings.tsx @@ -51,7 +51,7 @@ const AboutSettings: FC = () => { try { await window.api.checkForUpdate() } catch (error) { - window.message.error(t('settings.about.updateError')) + window.toast.error(t('settings.about.updateError')) } dispatch(setUpdateState({ checking: false })) @@ -105,7 +105,7 @@ const AboutSettings: FC = () => { const handleTestChannelChange = async (value: UpgradeChannel) => { if (testPlan && currentChannelByVersion !== UpgradeChannel.LATEST && value !== currentChannelByVersion) { - window.message.warning(t('settings.general.test_plan.version_channel_not_match')) + window.toast.warning(t('settings.general.test_plan.version_channel_not_match')) } setTestChannel(value) // Clear update info when switching upgrade channel diff --git a/src/renderer/src/pages/settings/AssistantSettings/AssistantPromptSettings.tsx b/src/renderer/src/pages/settings/AssistantSettings/AssistantPromptSettings.tsx index f7829a2bb..1315f8d7b 100644 --- a/src/renderer/src/pages/settings/AssistantSettings/AssistantPromptSettings.tsx +++ b/src/renderer/src/pages/settings/AssistantSettings/AssistantPromptSettings.tsx @@ -50,7 +50,7 @@ const AssistantPromptSettings: React.FC = ({ assistant, updateAssistant } const onUpdate = () => { const _assistant = { ...assistant, name: name.trim(), emoji, prompt } updateAssistant(_assistant) - window.message.success(t('common.saved')) + window.toast.success(t('common.saved')) } const handleEmojiSelect = (selectedEmoji: string) => { diff --git a/src/renderer/src/pages/settings/DataSettings/DataSettings.tsx b/src/renderer/src/pages/settings/DataSettings/DataSettings.tsx index 120558e79..cabcf6091 100644 --- a/src/renderer/src/pages/settings/DataSettings/DataSettings.tsx +++ b/src/renderer/src/pages/settings/DataSettings/DataSettings.tsx @@ -163,9 +163,9 @@ const DataSettings: FC = () => { await window.api.clearCache() await window.api.trace.cleanLocalData() await window.api.getCacheSize().then(setCacheSize) - window.message.success(t('settings.data.clear_cache.success')) + window.toast.success(t('settings.data.clear_cache.success')) } catch (error) { - window.message.error(t('settings.data.clear_cache.error')) + window.toast.error(t('settings.data.clear_cache.error')) } } }) @@ -178,7 +178,7 @@ const DataSettings: FC = () => { content: t('settings.data.app_knowledge.remove_all_confirm'), onOk: async () => { await removeAllFiles() - window.message.success(t('settings.data.app_knowledge.remove_all_success')) + window.toast.success(t('settings.data.app_knowledge.remove_all_success')) }, okText: t('common.delete'), okButtonProps: { @@ -205,28 +205,28 @@ const DataSettings: FC = () => { // if is root path, show error const pathParts = newAppDataPath.split(/[/\\]/).filter((part: string) => part !== '') if (pathParts.length <= 1) { - window.message.error(t('settings.data.app_data.select_error_root_path')) + window.toast.error(t('settings.data.app_data.select_error_root_path')) return } // check new app data path is not in old app data path const isInOldPath = await window.api.isPathInside(newAppDataPath, appInfo.appDataPath) if (isInOldPath) { - window.message.error(t('settings.data.app_data.select_error_same_path')) + window.toast.error(t('settings.data.app_data.select_error_same_path')) return } // check new app data path is not in app install path const isInInstallPath = await window.api.isPathInside(newAppDataPath, appInfo.installPath) if (isInInstallPath) { - window.message.error(t('settings.data.app_data.select_error_in_app_path')) + window.toast.error(t('settings.data.app_data.select_error_in_app_path')) return } // check new app data path has write permission const hasWritePermission = await window.api.hasWritePermission(newAppDataPath) if (!hasWritePermission) { - window.message.error(t('settings.data.app_data.select_error_write_permission')) + window.toast.error(t('settings.data.app_data.select_error_write_permission')) return } @@ -245,9 +245,9 @@ const DataSettings: FC = () => { okText: t('common.confirm'), cancelText: t('common.cancel'), onOk: () => { - window.message.info({ - content: t('settings.data.app_data.restart_notice'), - duration: 2 + window.toast.info({ + title: t('settings.data.app_data.restart_notice'), + timeout: 2000 }) setTimeoutTimer( 'doubleConfirmModalBeforeCopyData', @@ -335,9 +335,9 @@ const DataSettings: FC = () => { return } - window.message.info({ - content: t('settings.data.app_data.restart_notice'), - duration: 3 + window.toast.info({ + title: t('settings.data.app_data.restart_notice'), + timeout: 3000 }) setTimeoutTimer( 'showMigrationConfirmModal_1', @@ -352,7 +352,7 @@ const DataSettings: FC = () => { } // 如果不复制数据,直接设置新的应用数据路径 await window.api.setAppDataPath(newPath) - window.message.success(t('settings.data.app_data.path_changed_without_copy')) + window.toast.success(t('settings.data.app_data.path_changed_without_copy')) // 更新应用数据路径 setAppInfo(await window.api.getAppInfo()) @@ -361,7 +361,7 @@ const DataSettings: FC = () => { setTimeoutTimer( 'showMigrationConfirmModal_2', () => { - window.message.success(t('settings.data.app_data.select_success')) + window.toast.success(t('settings.data.app_data.select_success')) window.api.setStopQuitApp(false, '') window.api.relaunchApp() }, @@ -369,9 +369,9 @@ const DataSettings: FC = () => { ) } catch (error) { window.api.setStopQuitApp(false, '') - window.message.error({ - content: t('settings.data.app_data.path_change_failed') + ': ' + error, - duration: 5 + window.toast.error({ + title: t('settings.data.app_data.path_change_failed') + ': ' + error, + timeout: 5000 }) } } @@ -390,7 +390,6 @@ const DataSettings: FC = () => {
{t('settings.data.app_data.migration_title')}
) const className = 'migration-modal' - const messageKey = 'data-migration' // 显示进度模态框 const showProgressModal = (title: React.ReactNode, className: string, PathsContent: React.FC) => { @@ -463,8 +462,7 @@ const DataSettings: FC = () => { newPath: string, progressInterval: NodeJS.Timeout | null, updateProgress: (progress: number, status?: 'active' | 'success') => void, - loadingModal: { destroy: () => void }, - messageKey: string + loadingModal: { destroy: () => void } ): Promise => { // flush app data await window.api.flushAppData() @@ -494,10 +492,9 @@ const DataSettings: FC = () => { 'startMigration_2', () => { loadingModal.destroy() - window.message.error({ - content: t('settings.data.app_data.copy_failed') + ': ' + copyResult.error, - key: messageKey, - duration: 5 + window.toast.error({ + title: t('settings.data.app_data.copy_failed') + ': ' + copyResult.error, + timeout: 5000 }) resolve() }, @@ -517,10 +514,9 @@ const DataSettings: FC = () => { // 关闭加载模态框 loadingModal.destroy() - window.message.success({ - content: t('settings.data.app_data.copy_success'), - key: messageKey, - duration: 2 + window.toast.success({ + title: t('settings.data.app_data.copy_success'), + timeout: 2000 }) } @@ -541,7 +537,7 @@ const DataSettings: FC = () => { const { loadingModal, progressInterval, updateProgress } = showProgressModal(title, className, PathsContent) try { window.api.setStopQuitApp(true, t('settings.data.app_data.stop_quit_app_reason')) - await startMigration(originalPath, newDataPath, progressInterval, updateProgress, loadingModal, messageKey) + await startMigration(originalPath, newDataPath, progressInterval, updateProgress, loadingModal) // 更新应用数据路径 setAppInfo(await window.api.getAppInfo()) @@ -550,7 +546,7 @@ const DataSettings: FC = () => { setTimeoutTimer( 'handleDataMigration', () => { - window.message.success(t('settings.data.app_data.select_success')) + window.toast.success(t('settings.data.app_data.select_success')) window.api.setStopQuitApp(false, '') window.api.relaunchApp({ args: ['--user-data-dir=' + newDataPath] @@ -560,10 +556,9 @@ const DataSettings: FC = () => { ) } catch (error) { window.api.setStopQuitApp(false, '') - window.message.error({ - content: t('settings.data.app_data.copy_failed') + ': ' + error, - key: messageKey, - duration: 5 + window.toast.error({ + title: t('settings.data.app_data.copy_failed') + ': ' + error, + timeout: 5000 }) } finally { if (progressInterval) { diff --git a/src/renderer/src/pages/settings/DataSettings/JoplinSettings.tsx b/src/renderer/src/pages/settings/DataSettings/JoplinSettings.tsx index 664b65a60..91826d661 100644 --- a/src/renderer/src/pages/settings/DataSettings/JoplinSettings.tsx +++ b/src/renderer/src/pages/settings/DataSettings/JoplinSettings.tsx @@ -42,11 +42,11 @@ const JoplinSettings: FC = () => { const handleJoplinConnectionCheck = async () => { try { if (!joplinToken) { - window.message.error(t('settings.data.joplin.check.empty_token')) + window.toast.error(t('settings.data.joplin.check.empty_token')) return } if (!joplinUrl) { - window.message.error(t('settings.data.joplin.check.empty_url')) + window.toast.error(t('settings.data.joplin.check.empty_url')) return } @@ -55,13 +55,13 @@ const JoplinSettings: FC = () => { const data = await response.json() if (!response.ok || data?.error) { - window.message.error(t('settings.data.joplin.check.fail')) + window.toast.error(t('settings.data.joplin.check.fail')) return } - window.message.success(t('settings.data.joplin.check.success')) + window.toast.success(t('settings.data.joplin.check.success')) } catch (e) { - window.message.error(t('settings.data.joplin.check.fail')) + window.toast.error(t('settings.data.joplin.check.fail')) } } diff --git a/src/renderer/src/pages/settings/DataSettings/LocalBackupSettings.tsx b/src/renderer/src/pages/settings/DataSettings/LocalBackupSettings.tsx index 97e910518..f967e381b 100644 --- a/src/renderer/src/pages/settings/DataSettings/LocalBackupSettings.tsx +++ b/src/renderer/src/pages/settings/DataSettings/LocalBackupSettings.tsx @@ -83,21 +83,21 @@ const LocalBackupSettings: React.FC = () => { // check new local backup dir is not in app data path // if is in app data path, show error if (await window.api.isPathInside(resolvedDir, appInfo!.appDataPath)) { - window.message.error(t('settings.data.local.directory.select_error_app_data_path')) + window.toast.error(t('settings.data.local.directory.select_error_app_data_path')) return false } // check new local backup dir is not in app install path // if is in app install path, show error if (await window.api.isPathInside(resolvedDir, appInfo!.installPath)) { - window.message.error(t('settings.data.local.directory.select_error_in_app_install_path')) + window.toast.error(t('settings.data.local.directory.select_error_in_app_install_path')) return false } // check new app data path has write permission const hasWritePermission = await window.api.hasWritePermission(resolvedDir) if (!hasWritePermission) { - window.message.error(t('settings.data.local.directory.select_error_write_permission')) + window.toast.error(t('settings.data.local.directory.select_error_write_permission')) return false } diff --git a/src/renderer/src/pages/settings/DataSettings/NotionSettings.tsx b/src/renderer/src/pages/settings/DataSettings/NotionSettings.tsx index 3b97b5cbc..2ea657796 100644 --- a/src/renderer/src/pages/settings/DataSettings/NotionSettings.tsx +++ b/src/renderer/src/pages/settings/DataSettings/NotionSettings.tsx @@ -42,11 +42,11 @@ const NotionSettings: FC = () => { const handleNotionConnectionCheck = () => { if (notionApiKey === null) { - window.message.error(t('settings.data.notion.check.empty_api_key')) + window.toast.error(t('settings.data.notion.check.empty_api_key')) return } if (notionDatabaseID === null) { - window.message.error(t('settings.data.notion.check.empty_database_id')) + window.toast.error(t('settings.data.notion.check.empty_database_id')) return } const notion = new Client({ auth: notionApiKey }) @@ -56,13 +56,13 @@ const NotionSettings: FC = () => { }) .then((result) => { if (result) { - window.message.success(t('settings.data.notion.check.success')) + window.toast.success(t('settings.data.notion.check.success')) } else { - window.message.error(t('settings.data.notion.check.fail')) + window.toast.error(t('settings.data.notion.check.fail')) } }) .catch(() => { - window.message.error(t('settings.data.notion.check.error')) + window.toast.error(t('settings.data.notion.check.error')) }) } diff --git a/src/renderer/src/pages/settings/DataSettings/NutstoreSettings.tsx b/src/renderer/src/pages/settings/DataSettings/NutstoreSettings.tsx index e040b13cc..a5d2a7f77 100644 --- a/src/renderer/src/pages/settings/DataSettings/NutstoreSettings.tsx +++ b/src/renderer/src/pages/settings/DataSettings/NutstoreSettings.tsx @@ -106,11 +106,9 @@ const NutstoreSettings: FC = () => { setCheckConnectionLoading(true) const isConnectedToNutstore = await checkConnection() - window.message[isConnectedToNutstore ? 'success' : 'error']({ - key: 'api-check', - style: { marginTop: '3vh' }, - duration: 2, - content: isConnectedToNutstore + window.toast[isConnectedToNutstore ? 'success' : 'error']({ + timeout: 2000, + title: isConnectedToNutstore ? t('settings.data.nutstore.checkConnection.success') : t('settings.data.nutstore.checkConnection.fail') }) diff --git a/src/renderer/src/pages/settings/DataSettings/SiyuanSettings.tsx b/src/renderer/src/pages/settings/DataSettings/SiyuanSettings.tsx index 8045fd5f1..fb6abe456 100644 --- a/src/renderer/src/pages/settings/DataSettings/SiyuanSettings.tsx +++ b/src/renderer/src/pages/settings/DataSettings/SiyuanSettings.tsx @@ -53,7 +53,7 @@ const SiyuanSettings: FC = () => { const handleCheckConnection = async () => { try { if (!siyuanApiUrl || !siyuanToken) { - window.message.error(t('settings.data.siyuan.check.empty_config')) + window.toast.error(t('settings.data.siyuan.check.empty_config')) return } @@ -66,20 +66,20 @@ const SiyuanSettings: FC = () => { }) if (!response.ok) { - window.message.error(t('settings.data.siyuan.check.fail')) + window.toast.error(t('settings.data.siyuan.check.fail')) return } const data = await response.json() if (data.code !== 0) { - window.message.error(t('settings.data.siyuan.check.fail')) + window.toast.error(t('settings.data.siyuan.check.fail')) return } - window.message.success(t('settings.data.siyuan.check.success')) + window.toast.success(t('settings.data.siyuan.check.success')) } catch (error) { logger.error('Check Siyuan connection failed:', error as Error) - window.message.error(t('settings.data.siyuan.check.error')) + window.toast.error(t('settings.data.siyuan.check.error')) } } diff --git a/src/renderer/src/pages/settings/DataSettings/YuqueSettings.tsx b/src/renderer/src/pages/settings/DataSettings/YuqueSettings.tsx index 57ee29246..45dbf2925 100644 --- a/src/renderer/src/pages/settings/DataSettings/YuqueSettings.tsx +++ b/src/renderer/src/pages/settings/DataSettings/YuqueSettings.tsx @@ -31,11 +31,11 @@ const YuqueSettings: FC = () => { const handleYuqueConnectionCheck = async () => { if (!yuqueToken) { - window.message.error(t('settings.data.yuque.check.empty_token')) + window.toast.error(t('settings.data.yuque.check.empty_token')) return } if (!yuqueUrl) { - window.message.error(t('settings.data.yuque.check.empty_repo_url')) + window.toast.error(t('settings.data.yuque.check.empty_repo_url')) return } @@ -46,7 +46,7 @@ const YuqueSettings: FC = () => { }) if (!response.ok) { - window.message.error(t('settings.data.yuque.check.fail')) + window.toast.error(t('settings.data.yuque.check.fail')) return } const yuqueSlug = yuqueUrl.replace('https://www.yuque.com/', '') @@ -56,12 +56,12 @@ const YuqueSettings: FC = () => { } }) if (!repoIDResponse.ok) { - window.message.error(t('settings.data.yuque.check.fail')) + window.toast.error(t('settings.data.yuque.check.fail')) return } const data = await repoIDResponse.json() dispatch(setYuqueRepoId(data.data.id)) - window.message.success(t('settings.data.yuque.check.success')) + window.toast.success(t('settings.data.yuque.check.success')) } const handleYuqueHelpClick = () => { diff --git a/src/renderer/src/pages/settings/DocProcessSettings/OcrImageSettings.tsx b/src/renderer/src/pages/settings/DocProcessSettings/OcrImageSettings.tsx index b91c78b95..622d1349d 100644 --- a/src/renderer/src/pages/settings/DocProcessSettings/OcrImageSettings.tsx +++ b/src/renderer/src/pages/settings/DocProcessSettings/OcrImageSettings.tsx @@ -30,7 +30,7 @@ const OcrImageSettings = ({ setProvider }: Props) => { const provider = imageProviders.find((p) => p.id === id) if (!provider) { logger.error(`Failed to find image provider by id: ${id}`) - window.message.error(t('settings.tool.ocr.image.error.provider_not_found')) + window.toast.error(t('settings.tool.ocr.image.error.provider_not_found')) return } diff --git a/src/renderer/src/pages/settings/GeneralSettings.tsx b/src/renderer/src/pages/settings/GeneralSettings.tsx index 76be5987b..84f54168a 100644 --- a/src/renderer/src/pages/settings/GeneralSettings.tsx +++ b/src/renderer/src/pages/settings/GeneralSettings.tsx @@ -20,6 +20,7 @@ import { import { LanguageVarious } from '@renderer/types' import { NotificationSource } from '@renderer/types/notification' import { isValidProxyUrl } from '@renderer/utils' +import { formatErrorMessage } from '@renderer/utils/error' import { defaultByPassRules, defaultLanguage } from '@shared/config/constant' import { Flex, Input, Switch, Tooltip } from 'antd' import { FC, useState } from 'react' @@ -96,7 +97,7 @@ const GeneralSettings: FC = () => { const onSetProxyUrl = () => { if (proxyUrl && !isValidProxyUrl(proxyUrl)) { - window.message.error({ content: t('message.error.invalid.proxy.url'), key: 'proxy-error' }) + window.toast.error(t('message.error.invalid.proxy.url')) return } @@ -165,10 +166,7 @@ const GeneralSettings: FC = () => { try { setDisableHardwareAcceleration(checked) } catch (error) { - window.message.error({ - content: (error as Error).message, - key: 'disable-hardware-acceleration-error' - }) + window.toast.error(formatErrorMessage(error)) return } diff --git a/src/renderer/src/pages/settings/MCPSettings/AddMcpServerModal.tsx b/src/renderer/src/pages/settings/MCPSettings/AddMcpServerModal.tsx index a3d63ea93..46bd8f3af 100644 --- a/src/renderer/src/pages/settings/MCPSettings/AddMcpServerModal.tsx +++ b/src/renderer/src/pages/settings/MCPSettings/AddMcpServerModal.tsx @@ -131,10 +131,7 @@ const AddMcpServerModal: FC = ({ if (importMethod === 'dxt') { if (!dxtFile) { - window.message.error({ - content: t('settings.mcp.addServer.importFrom.noDxtFile'), - key: 'mcp-no-dxt-file' - }) + window.toast.error(t('settings.mcp.addServer.importFrom.noDxtFile')) setLoading(false) return } @@ -144,10 +141,7 @@ const AddMcpServerModal: FC = ({ const result = await window.api.mcp.uploadDxt(dxtFile) if (!result.success) { - window.message.error({ - content: result.error || t('settings.mcp.addServer.importFrom.dxtProcessFailed'), - key: 'mcp-dxt-process-failed' - }) + window.toast.error(result.error || t('settings.mcp.addServer.importFrom.dxtProcessFailed')) setLoading(false) return } @@ -156,10 +150,7 @@ const AddMcpServerModal: FC = ({ // Check for duplicate names if (existingServers && existingServers.some((server) => server.name === manifest.name)) { - window.message.error({ - content: t('settings.mcp.addServer.importFrom.nameExists', { name: manifest.name }), - key: 'mcp-name-exists' - }) + window.toast.error(t('settings.mcp.addServer.importFrom.nameExists', { name: manifest.name })) setLoading(false) return } @@ -227,10 +218,7 @@ const AddMcpServerModal: FC = ({ ) // Delay to ensure server is properly added to store } catch (error) { logger.error('DXT processing error:', error as Error) - window.message.error({ - content: t('settings.mcp.addServer.importFrom.dxtProcessFailed'), - key: 'mcp-dxt-error' - }) + window.toast.error(t('settings.mcp.addServer.importFrom.dxtProcessFailed')) setLoading(false) return } @@ -286,10 +274,7 @@ const AddMcpServerModal: FC = ({ }) .catch((connError: any) => { logger.error(`Connectivity check failed for ${newServer.name}:`, connError) - window.message.error({ - content: newServer.name + t('settings.mcp.addServer.importFrom.connectionFailed'), - key: 'mcp-quick-add-failed' - }) + window.toast.error(newServer.name + t('settings.mcp.addServer.importFrom.connectionFailed')) }) } } finally { diff --git a/src/renderer/src/pages/settings/MCPSettings/BuiltinMCPServerList.tsx b/src/renderer/src/pages/settings/MCPSettings/BuiltinMCPServerList.tsx index 249d51424..c5af5da06 100644 --- a/src/renderer/src/pages/settings/MCPSettings/BuiltinMCPServerList.tsx +++ b/src/renderer/src/pages/settings/MCPSettings/BuiltinMCPServerList.tsx @@ -37,7 +37,7 @@ const BuiltinMCPServerList: FC = () => { } addMCPServer(server) - window.message.success({ content: t('settings.mcp.addSuccess'), key: 'mcp-add-builtin-server' }) + window.toast.success(t('settings.mcp.addSuccess')) }} disabled={isInstalled} /> diff --git a/src/renderer/src/pages/settings/MCPSettings/EditMcpJsonPopup.tsx b/src/renderer/src/pages/settings/MCPSettings/EditMcpJsonPopup.tsx index c1e5f1553..37e5aaa4f 100644 --- a/src/renderer/src/pages/settings/MCPSettings/EditMcpJsonPopup.tsx +++ b/src/renderer/src/pages/settings/MCPSettings/EditMcpJsonPopup.tsx @@ -58,7 +58,7 @@ const PopupContainer: React.FC = ({ resolve }) => { try { if (!jsonConfig.trim()) { dispatch(setMCPServers([])) - window.message.success(t('settings.mcp.jsonSaveSuccess')) + window.toast.success(t('settings.mcp.jsonSaveSuccess')) setJsonError('') setJsonSaving(false) return @@ -92,13 +92,13 @@ const PopupContainer: React.FC = ({ resolve }) => { dispatch(setMCPServers(serversArray)) - window.message.success(t('settings.mcp.jsonSaveSuccess')) + window.toast.success(t('settings.mcp.jsonSaveSuccess')) setJsonError('') setOpen(false) } catch (error: any) { logger.error('Failed to save JSON config:', error) setJsonError(error.message || t('settings.mcp.jsonSaveError')) - window.message.error(t('settings.mcp.jsonSaveError')) + window.toast.error(t('settings.mcp.jsonSaveError')) } finally { setJsonSaving(false) } diff --git a/src/renderer/src/pages/settings/MCPSettings/InstallNpxUv.tsx b/src/renderer/src/pages/settings/MCPSettings/InstallNpxUv.tsx index f1a569a2f..b213b982d 100644 --- a/src/renderer/src/pages/settings/MCPSettings/InstallNpxUv.tsx +++ b/src/renderer/src/pages/settings/MCPSettings/InstallNpxUv.tsx @@ -54,7 +54,7 @@ const InstallNpxUv: FC = ({ mini = false }) => { setIsInstallingUv(false) dispatch(setIsUvInstalled(true)) } catch (error: any) { - window.message.error({ content: `${t('settings.mcp.installError')}: ${error.message}`, key: 'mcp-install-error' }) + window.toast.error(`${t('settings.mcp.installError')}: ${error.message}`) setIsInstallingUv(false) } clearTimeout(checkBinariesTimerRef.current) @@ -68,10 +68,7 @@ const InstallNpxUv: FC = ({ mini = false }) => { setIsInstallingBun(false) dispatch(setIsBunInstalled(true)) } catch (error: any) { - window.message.error({ - content: `${t('settings.mcp.installError')}: ${error.message}`, - key: 'mcp-install-error' - }) + window.toast.error(`${t('settings.mcp.installError')}: ${error.message}`) setIsInstallingBun(false) } clearTimeout(checkBinariesTimerRef.current) diff --git a/src/renderer/src/pages/settings/MCPSettings/McpServersList.tsx b/src/renderer/src/pages/settings/MCPSettings/McpServersList.tsx index f45eb8501..67ae6231a 100644 --- a/src/renderer/src/pages/settings/MCPSettings/McpServersList.tsx +++ b/src/renderer/src/pages/settings/MCPSettings/McpServersList.tsx @@ -116,7 +116,7 @@ const McpServersList: FC = () => { } addMCPServer(newServer) navigate(`/settings/mcp/settings/${encodeURIComponent(newServer.id)}`) - window.message.success({ content: t('settings.mcp.addSuccess'), key: 'mcp-list' }) + window.toast.success(t('settings.mcp.addSuccess')) }, [addMCPServer, navigate, t]) const onDeleteMcpServer = useCallback( @@ -129,14 +129,11 @@ const McpServersList: FC = () => { onOk: async () => { await window.api.mcp.removeServer(server) deleteMCPServer(server.id) - window.message.success({ content: t('settings.mcp.deleteSuccess'), key: 'mcp-list' }) + window.toast.success(t('settings.mcp.deleteSuccess')) } }) } catch (error: any) { - window.message.error({ - content: `${t('settings.mcp.deleteError')}: ${error.message}`, - key: 'mcp-list' - }) + window.toast.error(`${t('settings.mcp.deleteError')}: ${error.message}`) } }, // eslint-disable-next-line react-hooks/exhaustive-deps @@ -151,7 +148,7 @@ const McpServersList: FC = () => { async (server: MCPServer) => { addMCPServer(server) setIsAddModalVisible(false) - window.message.success({ content: t('settings.mcp.addSuccess'), key: 'mcp-quick-add' }) + window.toast.success(t('settings.mcp.addSuccess')) // Optionally navigate to the new server's settings page // navigate(`/settings/mcp/settings/${encodeURIComponent(server.id)}`) }, diff --git a/src/renderer/src/pages/settings/MCPSettings/McpSettings.tsx b/src/renderer/src/pages/settings/MCPSettings/McpSettings.tsx index aeab238a6..db7484ac2 100644 --- a/src/renderer/src/pages/settings/MCPSettings/McpSettings.tsx +++ b/src/renderer/src/pages/settings/MCPSettings/McpSettings.tsx @@ -302,7 +302,7 @@ const McpSettings: React.FC = () => { try { await window.api.mcp.restartServer(mcpServer) updateMCPServer({ ...mcpServer, isActive: true }) - window.message.success({ content: t('settings.mcp.updateSuccess'), key: 'mcp-update-success' }) + window.toast.success(t('settings.mcp.updateSuccess')) setIsFormChanged(false) } catch (error: any) { updateMCPServer({ ...mcpServer, isActive: false }) @@ -314,7 +314,7 @@ const McpSettings: React.FC = () => { } } else { updateMCPServer({ ...mcpServer, isActive: false }) - window.message.success({ content: t('settings.mcp.updateSuccess'), key: 'mcp-update-success' }) + window.toast.success(t('settings.mcp.updateSuccess')) setIsFormChanged(false) } setLoading(false) @@ -381,15 +381,12 @@ const McpSettings: React.FC = () => { onOk: async () => { await window.api.mcp.removeServer(server) deleteMCPServer(server.id) - window.message.success({ content: t('settings.mcp.deleteSuccess'), key: 'mcp-list' }) + window.toast.success(t('settings.mcp.deleteSuccess')) navigate('/settings/mcp') } }) } catch (error: any) { - window.message.error({ - content: `${t('settings.mcp.deleteError')}: ${error.message}`, - key: 'mcp-list' - }) + window.toast.error(`${t('settings.mcp.deleteError')}: ${error.message}`) } }, // eslint-disable-next-line react-hooks/exhaustive-deps diff --git a/src/renderer/src/pages/settings/MCPSettings/NpxSearch.tsx b/src/renderer/src/pages/settings/MCPSettings/NpxSearch.tsx index 0b6a001b8..e2279e977 100644 --- a/src/renderer/src/pages/settings/MCPSettings/NpxSearch.tsx +++ b/src/renderer/src/pages/settings/MCPSettings/NpxSearch.tsx @@ -43,7 +43,7 @@ const NpxSearch: FC = () => { const searchScope = scopeOverride || npmScope if (!searchScope.trim()) { - window.message.warning({ content: t('settings.mcp.npx_list.scope_required'), key: 'mcp-npx-scope-required' }) + window.toast.warning(t('settings.mcp.npx_list.scope_required')) return } @@ -79,18 +79,15 @@ const NpxSearch: FC = () => { setSearchResults(formattedResults) if (formattedResults.length === 0) { - window.message.info({ content: t('settings.mcp.npx_list.no_packages'), key: 'mcp-npx-no-packages' }) + window.toast.info(t('settings.mcp.npx_list.no_packages')) } } catch (error: unknown) { setSearchResults([]) _searchResults = [] if (error instanceof Error) { - window.message.error({ - content: `${t('settings.mcp.npx_list.search_error')}: ${error.message}`, - key: 'mcp-npx-search-error' - }) + window.toast.error(`${t('settings.mcp.npx_list.search_error')}: ${error.message}`) } else { - window.message.error({ content: t('settings.mcp.npx_list.search_error'), key: 'mcp-npx-search-error' }) + window.toast.error(t('settings.mcp.npx_list.search_error')) } } finally { setSearchLoading(false) @@ -186,7 +183,7 @@ const NpxSearch: FC = () => { } addMCPServer(newServer) - window.message.success({ content: t('settings.mcp.addSuccess'), key: 'mcp-add-server' }) + window.toast.success(t('settings.mcp.addSuccess')) }} /> diff --git a/src/renderer/src/pages/settings/MCPSettings/SyncServersPopup.tsx b/src/renderer/src/pages/settings/MCPSettings/SyncServersPopup.tsx index cea47ae0e..ea6d6dfe0 100644 --- a/src/renderer/src/pages/settings/MCPSettings/SyncServersPopup.tsx +++ b/src/renderer/src/pages/settings/MCPSettings/SyncServersPopup.tsx @@ -152,18 +152,18 @@ const PopupContainer: React.FC = ({ resolve, existingServers }) => { updateMCPServer(server) } } - window.message.success(result.message) + window.toast.success(result.message) setOpen(false) } else { // Show message but keep dialog open if (result.success) { - window.message.info(result.message) + window.toast.info(result.message) } else { - window.message.error(result.message) + window.toast.error(result.message) } } } catch (error: any) { - window.message.error(`${t('settings.mcp.sync.error')}: ${error.message}`) + window.toast.error(`${t('settings.mcp.sync.error')}: ${error.message}`) } finally { setIsSyncing(false) } diff --git a/src/renderer/src/pages/settings/MemorySettings/MemorySettings.tsx b/src/renderer/src/pages/settings/MemorySettings/MemorySettings.tsx index 7d5920e67..ecf8d74b6 100644 --- a/src/renderer/src/pages/settings/MemorySettings/MemorySettings.tsx +++ b/src/renderer/src/pages/settings/MemorySettings/MemorySettings.tsx @@ -330,7 +330,7 @@ const MemorySettings = () => { setAllMemories(result.results || []) } catch (error) { logger.error('Failed to load memories:', error as Error) - window.message.error(t('memory.load_failed')) + window.toast.error(t('memory.load_failed')) } finally { setLoading(false) } @@ -390,25 +390,25 @@ const MemorySettings = () => { try { // The memory service will automatically use the current user from its state await memoryService.add(memory, {}) - window.message.success(t('memory.add_success')) + window.toast.success(t('memory.add_success')) // Go to first page to see the newly added memory setCurrentPage(1) await loadMemories(currentUser) } catch (error) { logger.error('Failed to add memory:', error as Error) - window.message.error(t('memory.add_failed')) + window.toast.error(t('memory.add_failed')) } } const handleDeleteMemory = async (id: string) => { try { await memoryService.delete(id) - window.message.success(t('memory.delete_success')) + window.toast.success(t('memory.delete_success')) // Reload all memories await loadMemories(currentUser) } catch (error) { logger.error('Failed to delete memory:', error as Error) - window.message.error(t('memory.delete_failed')) + window.toast.error(t('memory.delete_failed')) } } @@ -419,13 +419,13 @@ const MemorySettings = () => { const handleUpdateMemory = async (id: string, memory: string, metadata?: Record) => { try { await memoryService.update(id, memory, metadata) - window.message.success(t('memory.update_success')) + window.toast.success(t('memory.update_success')) setEditingMemory(null) // Reload all memories await loadMemories(currentUser) } catch (error) { logger.error('Failed to update memory:', error as Error) - window.message.error(t('memory.update_failed')) + window.toast.error(t('memory.update_failed')) } } @@ -445,12 +445,12 @@ const MemorySettings = () => { // Explicitly load memories for the new user await loadMemories(userId) - window.message.success( + window.toast.success( t('memory.user_switched', { user: userId === DEFAULT_USER_ID ? t('memory.default_user') : userId }) ) } catch (error) { logger.error('Failed to switch user:', error as Error) - window.message.error(t('memory.user_switch_failed')) + window.toast.error(t('memory.user_switch_failed')) } } @@ -466,11 +466,11 @@ const MemorySettings = () => { // Switch to the newly created user await handleUserSwitch(userId) - window.message.success(t('memory.user_created', { user: userId })) + window.toast.success(t('memory.user_created', { user: userId })) setAddUserModalVisible(false) } catch (error) { logger.error('Failed to add user:', error as Error) - window.message.error(t('memory.add_user_failed')) + window.toast.error(t('memory.add_user_failed')) } } @@ -501,13 +501,13 @@ const MemorySettings = () => { onOk: async () => { try { await memoryService.deleteAllMemoriesForUser(userId) - window.message.success(t('memory.memories_reset_success', { user: getUserDisplayName(userId) })) + window.toast.success(t('memory.memories_reset_success', { user: getUserDisplayName(userId) })) // Reload memories to show the empty state await loadMemories(currentUser) } catch (error) { logger.error('Failed to reset memories:', error as Error) - window.message.error(t('memory.reset_memories_failed')) + window.toast.error(t('memory.reset_memories_failed')) } } }) @@ -515,7 +515,7 @@ const MemorySettings = () => { const handleDeleteUser = async (userId: string) => { if (userId === DEFAULT_USER_ID) { - window.message.error(t('memory.cannot_delete_default_user')) + window.toast.error(t('memory.cannot_delete_default_user')) return } @@ -528,7 +528,7 @@ const MemorySettings = () => { onOk: async () => { try { await memoryService.deleteUser(userId) - window.message.success(t('memory.user_deleted', { user: userId })) + window.toast.success(t('memory.user_deleted', { user: userId })) // Refresh the users list from database after deletion await loadUniqueUsers() @@ -541,7 +541,7 @@ const MemorySettings = () => { } } catch (error) { logger.error('Failed to delete user:', error as Error) - window.message.error(t('memory.delete_user_failed')) + window.toast.error(t('memory.delete_user_failed')) } } }) @@ -572,7 +572,7 @@ const MemorySettings = () => { }) } - window.message.success(t('memory.global_memory_disabled_title')) + window.toast.success(t('memory.global_memory_disabled_title')) } const { theme } = useTheme() diff --git a/src/renderer/src/pages/settings/NotesSettings.tsx b/src/renderer/src/pages/settings/NotesSettings.tsx index 6903c9379..1a062145d 100644 --- a/src/renderer/src/pages/settings/NotesSettings.tsx +++ b/src/renderer/src/pages/settings/NotesSettings.tsx @@ -71,10 +71,10 @@ const NotesSettings: FC = () => { updateNotesPath(tempPath) initWorkSpace(tempPath, 'sort_a2z') - window.message.success(t('notes.settings.data.path_updated')) + window.toast.success(t('notes.settings.data.path_updated')) } catch (error) { logger.error('Failed to apply notes path:', error as Error) - window.message.error(t('notes.settings.data.apply_path_failed')) + window.toast.error(t('notes.settings.data.apply_path_failed')) } } @@ -84,10 +84,10 @@ const NotesSettings: FC = () => { setTempPath(info.notesPath) updateNotesPath(info.notesPath) initWorkSpace(info.notesPath, 'sort_a2z') - window.message.success(t('notes.settings.data.reset_to_default')) + window.toast.success(t('notes.settings.data.reset_to_default')) } catch (error) { logger.error('Failed to reset to default:', error as Error) - window.message.error(t('notes.settings.data.reset_failed')) + window.toast.error(t('notes.settings.data.reset_failed')) } } diff --git a/src/renderer/src/pages/settings/ProviderSettings/AddProviderPopup.tsx b/src/renderer/src/pages/settings/ProviderSettings/AddProviderPopup.tsx index b011d243c..f02485c45 100644 --- a/src/renderer/src/pages/settings/ProviderSettings/AddProviderPopup.tsx +++ b/src/renderer/src/pages/settings/ProviderSettings/AddProviderPopup.tsx @@ -84,7 +84,7 @@ const PopupContainer: React.FC = ({ provider, resolve }) => { setLogoPickerOpen(false) } catch (error: any) { - window.message.error(error.message) + window.toast.error(error.message) } } @@ -98,7 +98,7 @@ const PopupContainer: React.FC = ({ provider, resolve }) => { setDropdownOpen(false) } catch (error: any) { - window.message.error(error.message) + window.toast.error(error.message) } } @@ -146,7 +146,7 @@ const PopupContainer: React.FC = ({ provider, resolve }) => { } setDropdownOpen(false) } catch (error: any) { - window.message.error(error.message) + window.toast.error(error.message) } }}> {t('settings.general.image_upload')} diff --git a/src/renderer/src/pages/settings/ProviderSettings/AnthropicSettings.tsx b/src/renderer/src/pages/settings/ProviderSettings/AnthropicSettings.tsx index e625b0630..46ee7e0a7 100644 --- a/src/renderer/src/pages/settings/ProviderSettings/AnthropicSettings.tsx +++ b/src/renderer/src/pages/settings/ProviderSettings/AnthropicSettings.tsx @@ -46,7 +46,7 @@ const AnthropicSettings = () => { setCodeModalVisible(true) } catch (error) { logger.error('OAuth redirect failed:', error as Error) - window.message.error(t('settings.provider.anthropic.auth_failed')) + window.toast.error(t('settings.provider.anthropic.auth_failed')) } finally { setLoading(false) } @@ -60,10 +60,10 @@ const AnthropicSettings = () => { await window.api.anthropic_oauth.completeOAuthWithCode(authCode) setAuthStatus(AuthStatus.AUTHENTICATED) setCodeModalVisible(false) - window.message.success(t('settings.provider.anthropic.auth_success')) + window.toast.success(t('settings.provider.anthropic.auth_success')) } catch (error) { logger.error('Code submission failed:', error as Error) - window.message.error(t('settings.provider.anthropic.code_error')) + window.toast.error(t('settings.provider.anthropic.code_error')) } finally { setLoading(false) } @@ -82,10 +82,10 @@ const AnthropicSettings = () => { try { await window.api.anthropic_oauth.clearCredentials() setAuthStatus(AuthStatus.NOT_STARTED) - window.message.success(t('settings.provider.anthropic.logout_success')) + window.toast.success(t('settings.provider.anthropic.logout_success')) } catch (error) { logger.error('Logout failed:', error as Error) - window.message.error(t('settings.provider.anthropic.logout_failed')) + window.toast.error(t('settings.provider.anthropic.logout_failed')) } } diff --git a/src/renderer/src/pages/settings/ProviderSettings/CustomHeaderPopup.tsx b/src/renderer/src/pages/settings/ProviderSettings/CustomHeaderPopup.tsx index 628016771..80324fd99 100644 --- a/src/renderer/src/pages/settings/ProviderSettings/CustomHeaderPopup.tsx +++ b/src/renderer/src/pages/settings/ProviderSettings/CustomHeaderPopup.tsx @@ -40,9 +40,9 @@ const PopupContainer: React.FC = ({ provider, resolve }) => { updateProvider({ ...provider, extra_headers: headers }) } - window.message.success({ content: t('message.save.success.title') }) + window.toast.success(t('message.save.success.title')) } catch (error) { - window.message.error({ content: t('settings.provider.copilot.invalid_json') }) + window.toast.error(t('settings.provider.copilot.invalid_json')) } }, [headerText, provider, t, updateDefaultHeaders, updateProvider]) diff --git a/src/renderer/src/pages/settings/ProviderSettings/GithubCopilotSettings.tsx b/src/renderer/src/pages/settings/ProviderSettings/GithubCopilotSettings.tsx index 3c50758f4..eab065e54 100644 --- a/src/renderer/src/pages/settings/ProviderSettings/GithubCopilotSettings.tsx +++ b/src/renderer/src/pages/settings/ProviderSettings/GithubCopilotSettings.tsx @@ -67,13 +67,13 @@ const GithubCopilotSettings: FC = ({ providerId }) = // 自动复制授权码到剪贴板 try { await navigator.clipboard.writeText(user_code) - window.message.success(t('settings.provider.copilot.code_copied')) + window.toast.success(t('settings.provider.copilot.code_copied')) } catch (error) { logger.error('Failed to copy to clipboard:', error as Error) } } catch (error) { logger.error('Failed to get device code:', error as Error) - window.message.error(t('settings.provider.copilot.code_failed')) + window.toast.error(t('settings.provider.copilot.code_failed')) setCurrentStep(0) } finally { setLoading(false) @@ -95,11 +95,11 @@ const GithubCopilotSettings: FC = ({ providerId }) = setAuthStatus(AuthStatus.AUTHENTICATED) updateState({ username: login, avatar: avatar }) updateProvider({ ...provider, apiKey: token, isAuthed: true }) - window.message.success(t('settings.provider.copilot.auth_success')) + window.toast.success(t('settings.provider.copilot.auth_success')) } } catch (error) { logger.error('Failed to get token:', error as Error) - window.message.error(t('settings.provider.copilot.auth_failed')) + window.toast.error(t('settings.provider.copilot.auth_failed')) setCurrentStep(2) } finally { setLoading(false) @@ -125,10 +125,10 @@ const GithubCopilotSettings: FC = ({ providerId }) = setVerificationPageOpened(false) setCurrentStep(0) - window.message.success(t('settings.provider.copilot.logout_success')) + window.toast.success(t('settings.provider.copilot.logout_success')) } catch (error) { logger.error('Failed to logout:', error as Error) - window.message.error(t('settings.provider.copilot.logout_failed')) + window.toast.error(t('settings.provider.copilot.logout_failed')) // 如果登出失败,重置登出状态 updateProvider({ ...provider, apiKey: '', isAuthed: false }) } finally { @@ -140,10 +140,10 @@ const GithubCopilotSettings: FC = ({ providerId }) = const handleCopyUserCode = useCallback(async () => { try { await navigator.clipboard.writeText(userCode) - window.message.success(t('common.copied')) + window.toast.success(t('common.copied')) } catch (error) { logger.error('Failed to copy to clipboard:', error as Error) - window.message.error(t('common.copy_failed')) + window.toast.error(t('common.copy_failed')) } }, [userCode, t]) diff --git a/src/renderer/src/pages/settings/ProviderSettings/ModelList/AddModelPopup.tsx b/src/renderer/src/pages/settings/ProviderSettings/ModelList/AddModelPopup.tsx index 0d8932aba..5a992074d 100644 --- a/src/renderer/src/pages/settings/ProviderSettings/ModelList/AddModelPopup.tsx +++ b/src/renderer/src/pages/settings/ProviderSettings/ModelList/AddModelPopup.tsx @@ -46,7 +46,7 @@ const PopupContainer: React.FC = ({ title, provider, resolve }) => { const id = values.id.trim() if (find(models, { id })) { - window.message.error(t('error.model.exists')) + window.toast.error(t('error.model.exists')) return } diff --git a/src/renderer/src/pages/settings/ProviderSettings/ModelList/NewApiAddModelPopup.tsx b/src/renderer/src/pages/settings/ProviderSettings/ModelList/NewApiAddModelPopup.tsx index c5fd2180b..4e81b39aa 100644 --- a/src/renderer/src/pages/settings/ProviderSettings/ModelList/NewApiAddModelPopup.tsx +++ b/src/renderer/src/pages/settings/ProviderSettings/ModelList/NewApiAddModelPopup.tsx @@ -51,7 +51,7 @@ const PopupContainer: React.FC = ({ title, provider, resolve, model, endp const id = values.id.trim() if (find(models, { id })) { - window.message.error(t('error.model.exists')) + window.toast.error(t('error.model.exists')) return } diff --git a/src/renderer/src/pages/settings/ProviderSettings/ModelList/useHealthCheck.ts b/src/renderer/src/pages/settings/ProviderSettings/ModelList/useHealthCheck.ts index 71b9bccb8..ca73ad831 100644 --- a/src/renderer/src/pages/settings/ProviderSettings/ModelList/useHealthCheck.ts +++ b/src/renderer/src/pages/settings/ProviderSettings/ModelList/useHealthCheck.ts @@ -19,11 +19,9 @@ export const useHealthCheck = (provider: Provider, models: Model[]) => { const modelsToCheck = models.filter((model) => !isRerankModel(model)) if (isEmpty(modelsToCheck)) { - window.message.error({ - key: 'no-models', - style: { marginTop: '3vh' }, - duration: 5, - content: t('settings.provider.no_models_for_check') + window.toast.error({ + timeout: 5000, + title: t('settings.provider.no_models_for_check') }) return } @@ -80,11 +78,9 @@ export const useHealthCheck = (provider: Provider, models: Model[]) => { } ) - window.message.info({ - key: 'health-check-summary', - style: { marginTop: '3vh' }, - duration: 5, - content: summarizeHealthResults(checkResults, provider.name) + window.toast.info({ + timeout: 5000, + title: summarizeHealthResults(checkResults, provider.name) }) setIsChecking(false) diff --git a/src/renderer/src/pages/settings/ProviderSettings/ProviderList.tsx b/src/renderer/src/pages/settings/ProviderSettings/ProviderList.tsx index f64e09d49..d023f3d0d 100644 --- a/src/renderer/src/pages/settings/ProviderSettings/ProviderList.tsx +++ b/src/renderer/src/pages/settings/ProviderSettings/ProviderList.tsx @@ -113,7 +113,7 @@ const ProviderList: FC = () => { } setSelectedProvider(updatedProvider) - window.message.success(t('settings.models.provider_key_added', { provider: displayName })) + window.toast.success(t('settings.models.provider_key_added', { provider: displayName })) } // 检查 URL 参数 @@ -125,14 +125,14 @@ const ProviderList: FC = () => { try { const { id, apiKey: newApiKey, baseUrl, type, name } = JSON.parse(addProviderData) if (!id || !newApiKey || !baseUrl) { - window.message.error(t('settings.models.provider_key_add_failed_by_invalid_data')) + window.toast.error(t('settings.models.provider_key_add_failed_by_invalid_data')) window.navigate('/settings/provider') return } handleProviderAddKey({ id, apiKey: newApiKey, baseUrl, type, name }) } catch (error) { - window.message.error(t('settings.models.provider_key_add_failed_by_invalid_data')) + window.toast.error(t('settings.models.provider_key_add_failed_by_invalid_data')) window.navigate('/settings/provider') } // eslint-disable-next-line react-hooks/exhaustive-deps @@ -167,7 +167,7 @@ const ProviderList: FC = () => { setProviderLogos(updatedLogos) } catch (error) { logger.error('Failed to save logo', error as Error) - window.message.error('保存Provider Logo失败') + window.toast.error('保存Provider Logo失败') } } @@ -202,7 +202,7 @@ const ProviderList: FC = () => { })) } catch (error) { logger.error('Failed to save logo', error as Error) - window.message.error('更新Provider Logo失败') + window.toast.error('更新Provider Logo失败') } } else if (logo === undefined && logoFile === undefined) { try { diff --git a/src/renderer/src/pages/settings/ProviderSettings/ProviderSetting.tsx b/src/renderer/src/pages/settings/ProviderSettings/ProviderSetting.tsx index 75f31ed12..d5da53a23 100644 --- a/src/renderer/src/pages/settings/ProviderSettings/ProviderSetting.tsx +++ b/src/renderer/src/pages/settings/ProviderSettings/ProviderSetting.tsx @@ -153,11 +153,9 @@ const ProviderSetting: FC = ({ providerId }) => { const modelsToCheck = models.filter((model) => !isEmbeddingModel(model) && !isRerankModel(model)) if (isEmpty(modelsToCheck)) { - window.message.error({ - key: 'no-models', - style: { marginTop: '3vh' }, - duration: 5, - content: t('settings.provider.no_models_for_check') + window.toast.error({ + timeout: 5000, + title: t('settings.provider.no_models_for_check') }) return } @@ -165,7 +163,7 @@ const ProviderSetting: FC = ({ providerId }) => { const model = await SelectProviderModelPopup.show({ provider }) if (!model) { - window.message.error({ content: i18n.t('message.error.enter.model'), key: 'api-check' }) + window.toast.error(i18n.t('message.error.enter.model')) return } @@ -173,11 +171,9 @@ const ProviderSetting: FC = ({ providerId }) => { setApiKeyConnectivity((prev) => ({ ...prev, checking: true, status: HealthStatus.NOT_CHECKED })) await checkApi({ ...provider, apiHost }, model) - window.message.success({ - key: 'api-check', - style: { marginTop: '3vh' }, - duration: 2, - content: i18n.t('message.api.connection.success') + window.toast.success({ + timeout: 2000, + title: i18n.t('message.api.connection.success') }) setApiKeyConnectivity((prev) => ({ ...prev, status: HealthStatus.SUCCESS })) @@ -189,11 +185,9 @@ const ProviderSetting: FC = ({ providerId }) => { 3000 ) } catch (error: any) { - window.message.error({ - key: 'api-check', - style: { marginTop: '3vh' }, - duration: 8, - content: i18n.t('message.api.connection.failed') + window.toast.error({ + timeout: 8000, + title: i18n.t('message.api.connection.failed') }) setApiKeyConnectivity((prev) => ({ ...prev, status: HealthStatus.FAILED, error: formatErrorMessage(error) })) diff --git a/src/renderer/src/pages/settings/ProviderSettings/SelectProviderModelPopup.tsx b/src/renderer/src/pages/settings/ProviderSettings/SelectProviderModelPopup.tsx index cf65c4244..581169488 100644 --- a/src/renderer/src/pages/settings/ProviderSettings/SelectProviderModelPopup.tsx +++ b/src/renderer/src/pages/settings/ProviderSettings/SelectProviderModelPopup.tsx @@ -35,7 +35,7 @@ const PopupContainer: React.FC = ({ provider, resolve, reject }) => { const onOk = () => { if (!model) { - window.message.error({ content: i18n.t('message.error.enter.model'), key: 'api-check' }) + window.toast.error(i18n.t('message.error.enter.model')) return } setOpen(false) diff --git a/src/renderer/src/pages/settings/QuickAssistantSettings.tsx b/src/renderer/src/pages/settings/QuickAssistantSettings.tsx index f4de427cc..b7279fce6 100644 --- a/src/renderer/src/pages/settings/QuickAssistantSettings.tsx +++ b/src/renderer/src/pages/settings/QuickAssistantSettings.tsx @@ -36,11 +36,10 @@ const QuickAssistantSettings: FC = () => { !enable && window.api.miniWindow.close() if (enable && !clickTrayToShowQuickAssistant) { - window.message.info({ - content: t('settings.quickAssistant.use_shortcut_to_show'), - duration: 4, - icon: , - key: 'quick-assistant-info' + window.toast.info({ + title: t('settings.quickAssistant.use_shortcut_to_show'), + timeout: 4000, + icon: }) } diff --git a/src/renderer/src/pages/settings/TranslateSettingsPopup/CustomLanguageModal.tsx b/src/renderer/src/pages/settings/TranslateSettingsPopup/CustomLanguageModal.tsx index fc50b5ed1..1a1965c10 100644 --- a/src/renderer/src/pages/settings/TranslateSettingsPopup/CustomLanguageModal.tsx +++ b/src/renderer/src/pages/settings/TranslateSettingsPopup/CustomLanguageModal.tsx @@ -62,17 +62,17 @@ const CustomLanguageModal = ({ isOpen, editingCustomLanguage, onAdd, onEdit, onC try { await updateCustomLanguage(editingCustomLanguage, value, emoji, langCode) onEdit({ ...editingCustomLanguage, emoji, value, langCode }) - window.message.success(t('settings.translate.custom.success.update')) + window.toast.success(t('settings.translate.custom.success.update')) } catch (e) { - window.message.error(t('settings.translate.custom.error.update') + ': ' + (e as Error).message) + window.toast.error(t('settings.translate.custom.error.update') + ': ' + (e as Error).message) } } else { try { const added = await addCustomLanguage(value, emoji, langCode) onAdd(added) - window.message.success(t('settings.translate.custom.success.add')) + window.toast.success(t('settings.translate.custom.success.add')) } catch (e) { - window.message.error(t('settings.translate.custom.error.add') + ': ' + (e as Error).message) + window.toast.error(t('settings.translate.custom.error.add') + ': ' + (e as Error).message) } } onCancel() diff --git a/src/renderer/src/pages/settings/TranslateSettingsPopup/CustomLanguageSettings.tsx b/src/renderer/src/pages/settings/TranslateSettingsPopup/CustomLanguageSettings.tsx index b254b6251..abe34eabf 100644 --- a/src/renderer/src/pages/settings/TranslateSettingsPopup/CustomLanguageSettings.tsx +++ b/src/renderer/src/pages/settings/TranslateSettingsPopup/CustomLanguageSettings.tsx @@ -24,9 +24,9 @@ const CustomLanguageSettings = () => { try { await deleteCustomLanguage(id) setDisplayedItems((prev) => prev.filter((item) => item.id !== id)) - window.message.success(t('settings.translate.custom.success.delete')) + window.toast.success(t('settings.translate.custom.success.delete')) } catch (e) { - window.message.error(t('settings.translate.custom.error.delete')) + window.toast.error(t('settings.translate.custom.error.delete')) } }, [t] diff --git a/src/renderer/src/pages/settings/WebSearchSettings/AddSubscribePopup.tsx b/src/renderer/src/pages/settings/WebSearchSettings/AddSubscribePopup.tsx index aa288e6e8..4d975e825 100644 --- a/src/renderer/src/pages/settings/WebSearchSettings/AddSubscribePopup.tsx +++ b/src/renderer/src/pages/settings/WebSearchSettings/AddSubscribePopup.tsx @@ -38,7 +38,7 @@ const PopupContainer: React.FC = ({ title, resolve }) => { const name = values.name?.trim() || url if (!url) { - window.message.error(t('settings.tool.websearch.url_required')) + window.toast.error(t('settings.tool.websearch.url_required')) return } @@ -46,7 +46,7 @@ const PopupContainer: React.FC = ({ title, resolve }) => { try { new URL(url) } catch (e) { - window.message.error(t('settings.tool.websearch.url_invalid')) + window.toast.error(t('settings.tool.websearch.url_invalid')) return } diff --git a/src/renderer/src/pages/settings/WebSearchSettings/BlacklistSettings.tsx b/src/renderer/src/pages/settings/WebSearchSettings/BlacklistSettings.tsx index 47852fd08..8486a62f4 100644 --- a/src/renderer/src/pages/settings/WebSearchSettings/BlacklistSettings.tsx +++ b/src/renderer/src/pages/settings/WebSearchSettings/BlacklistSettings.tsx @@ -98,11 +98,10 @@ const BlacklistSettings: FC = () => { if (hasError) return dispatch(setExcludeDomains(validDomains)) - window.message.info({ - content: t('message.save.success.title'), - duration: 4, - icon: , - key: 'save-blacklist-info' + window.toast.info({ + title: t('message.save.success.title'), + timeout: 4000, + icon: }) } const onSelectChange = (newSelectedRowKeys: React.Key[]) => { @@ -146,9 +145,9 @@ const BlacklistSettings: FC = () => { } catch (error) { logger.error(`Error updating subscribe source ${source.url}:`, error as Error) // 显示具体源更新失败的消息 - window.message.warning({ - content: t('settings.tool.websearch.subscribe_update_failed', { url: source.url }), - duration: 3 + window.toast.warning({ + title: t('settings.tool.websearch.subscribe_update_failed', { url: source.url }), + timeout: 3000 }) } } @@ -158,9 +157,9 @@ const BlacklistSettings: FC = () => { setSubscribeSources(updatedSources) setSubscribeValid(true) // 显示成功消息 - window.message.success({ - content: t('settings.tool.websearch.subscribe_update_success'), - duration: 2 + window.toast.success({ + title: t('settings.tool.websearch.subscribe_update_success'), + timeout: 2000 }) setTimeoutTimer('updateSubscribe', () => setSubscribeValid(false), 3000) } else { @@ -169,9 +168,9 @@ const BlacklistSettings: FC = () => { } } catch (error) { logger.error('Error updating subscribes:', error as Error) - window.message.error({ - content: t('settings.tool.websearch.subscribe_update_failed'), - duration: 2 + window.toast.error({ + title: t('settings.tool.websearch.subscribe_update_failed'), + timeout: 2000 }) } setSubscribeChecking(false) @@ -200,16 +199,16 @@ const BlacklistSettings: FC = () => { }) setSubscribeValid(true) // 显示成功消息 - window.message.success({ - content: t('settings.tool.websearch.subscribe_add_success'), - duration: 2 + window.toast.success({ + title: t('settings.tool.websearch.subscribe_add_success'), + timeout: 2000 }) setTimeoutTimer('handleAddSubscribe', () => setSubscribeValid(false), 3000) } catch (error) { setSubscribeValid(false) - window.message.error({ - content: t('settings.tool.websearch.subscribe_add_failed'), - duration: 2 + window.toast.error({ + title: t('settings.tool.websearch.subscribe_add_failed'), + timeout: 2000 }) } } diff --git a/src/renderer/src/pages/settings/WebSearchSettings/WebSearchProviderSetting.tsx b/src/renderer/src/pages/settings/WebSearchSettings/WebSearchProviderSetting.tsx index 77993106f..f1fa43fab 100644 --- a/src/renderer/src/pages/settings/WebSearchSettings/WebSearchProviderSetting.tsx +++ b/src/renderer/src/pages/settings/WebSearchSettings/WebSearchProviderSetting.tsx @@ -88,11 +88,10 @@ const WebSearchProviderSetting: FC = ({ providerId }) => { async function checkSearch() { if (!provider) { - window.message.error({ - content: t('settings.no_provider_selected'), - duration: 3, - icon: , - key: 'no-provider-selected' + window.toast.error({ + title: t('settings.no_provider_selected'), + timeout: 3000, + icon: }) return } @@ -107,11 +106,9 @@ const WebSearchProviderSetting: FC = ({ providerId }) => { const { valid, error } = await WebSearchService.checkSearch(provider) const errorMessage = error && error?.message ? ' ' + error?.message : '' - window.message[valid ? 'success' : 'error']({ - key: 'api-check', - style: { marginTop: '3vh' }, - duration: valid ? 2 : 8, - content: valid + window.toast[valid ? 'success' : 'error']({ + timeout: valid ? 2000 : 8000, + title: valid ? t('settings.tool.websearch.check_success') : t('settings.tool.websearch.check_failed') + errorMessage }) @@ -120,11 +117,9 @@ const WebSearchProviderSetting: FC = ({ providerId }) => { } catch (err) { logger.error('Check search error:', err as Error) setApiValid(false) - window.message.error({ - key: 'check-search-error', - style: { marginTop: '3vh' }, - duration: 8, - content: t('settings.tool.websearch.check_failed') + window.toast.error({ + timeout: 8000, + title: t('settings.tool.websearch.check_failed') }) } finally { setApiChecking(false) diff --git a/src/renderer/src/pages/translate/TranslateHistory.tsx b/src/renderer/src/pages/translate/TranslateHistory.tsx index 5930fa9e6..e9a79200a 100644 --- a/src/renderer/src/pages/translate/TranslateHistory.tsx +++ b/src/renderer/src/pages/translate/TranslateHistory.tsx @@ -84,7 +84,7 @@ const TranslateHistoryList: FC = ({ isOpen, onHistoryItem try { deleteHistory(id) } catch (e) { - window.message.error(t('translate.history.error.delete')) + window.toast.error(t('translate.history.error.delete')) } }, [t] diff --git a/src/renderer/src/pages/translate/TranslatePage.tsx b/src/renderer/src/pages/translate/TranslatePage.tsx index 9c829e0b2..91171fd44 100644 --- a/src/renderer/src/pages/translate/TranslatePage.tsx +++ b/src/renderer/src/pages/translate/TranslatePage.tsx @@ -25,6 +25,7 @@ import { FileMetadata, isSupportedOcrFile, type Model, + SupportedOcrFile, type TranslateHistory, type TranslateLanguage } from '@renderer/types' @@ -139,8 +140,7 @@ const TranslatePage: FC = () => { setCopied(true) } catch (error) { logger.error('Failed to copy text to clipboard:', error as Error) - // TODO: use toast - window.message.error(t('common.copy_failed')) + window.toast.error(t('common.copy_failed')) } }, [setCopied, t, translatedContent]) @@ -169,16 +169,16 @@ const TranslatePage: FC = () => { translated = await translateText(text, actualTargetLanguage, throttle(setTranslatedContent, 100), abortKey) } catch (e) { if (isAbortError(e)) { - window.message.info(t('translate.info.aborted')) + window.toast.info(t('translate.info.aborted')) } else { logger.error('Failed to translate text', e as Error) - window.message.error(t('translate.error.failed') + ': ' + formatErrorMessage(e)) + window.toast.error(t('translate.error.failed') + ': ' + formatErrorMessage(e)) } setTranslating(false) return } - window.message.success(t('translate.complete')) + window.toast.success(t('translate.complete')) if (autoCopy) { setTimeoutTimer( 'auto-copy', @@ -193,11 +193,11 @@ const TranslatePage: FC = () => { await saveTranslateHistory(text, translated, actualSourceLanguage.langCode, actualTargetLanguage.langCode) } catch (e) { logger.error('Failed to save translate history', e as Error) - window.message.error(t('translate.history.error.save') + ': ' + formatErrorMessage(e)) + window.toast.error(t('translate.history.error.save') + ': ' + formatErrorMessage(e)) } } catch (e) { logger.error('Failed to translate', e as Error) - window.message.error(t('translate.error.unknown') + ': ' + formatErrorMessage(e)) + window.toast.error(t('translate.error.unknown') + ': ' + formatErrorMessage(e)) } }, [autoCopy, dispatch, onCopy, setTimeoutTimer, setTranslatedContent, setTranslating, t, translating] @@ -220,10 +220,7 @@ const TranslatePage: FC = () => { if (!couldTranslate) return if (!text.trim()) return if (!translateModel) { - window.message.error({ - content: t('translate.error.not_configured'), - key: 'translate-message' - }) + window.toast.error(t('translate.error.not_configured')) return } @@ -248,10 +245,7 @@ const TranslatePage: FC = () => { errorMessage = t('translate.language.not_pair') } - window.message.warning({ - content: errorMessage, - key: 'translate-message' - }) + window.toast.warning(errorMessage) return } @@ -263,7 +257,7 @@ const TranslatePage: FC = () => { await translate(text, actualSourceLanguage, actualTargetLanguage) } catch (error) { logger.error('Translation error:', error as Error) - window.message.error(t('translate.error.failed') + ': ' + formatErrorMessage(error)) + window.toast.error(t('translate.error.failed') + ': ' + formatErrorMessage(error)) return } finally { setTranslating(false) @@ -329,11 +323,11 @@ const TranslatePage: FC = () => { } const source = sourceLanguage === 'auto' ? detectedLanguage : sourceLanguage if (!source) { - window.message.error(t('translate.error.invalid_source')) + window.toast.error(t('translate.error.invalid_source')) return } if (source.langCode === UNKNOWN.langCode) { - window.message.error(t('translate.error.detect.unknown')) + window.toast.error(t('translate.error.detect.unknown')) return } const target = targetLanguage @@ -424,7 +418,7 @@ const TranslatePage: FC = () => { setAutoDetectionMethod(method) } catch (e) { logger.error('Failed to update auto detection method setting.', e as Error) - window.message.error(t('translate.error.detect.update_setting') + formatErrorMessage(e)) + window.toast.error(t('translate.error.detect.update_setting') + formatErrorMessage(e)) } } @@ -479,6 +473,52 @@ const TranslatePage: FC = () => { // 控制token估计 const tokenCount = useMemo(() => estimateTextTokens(text + prompt), [prompt, text]) + const readFile = useCallback( + async (file: FileMetadata) => { + const _readFile = async () => { + let isText: boolean + try { + // 检查文件是否为文本文件 + isText = await isTextFile(file.path) + } catch (e) { + logger.error('Failed to check if file is text.', e as Error) + window.toast.error(t('translate.files.error.check_type') + ': ' + formatErrorMessage(e)) + return + } + + if (!isText) { + window.toast.error(t('common.file.not_supported', { type: getFileExtension(file.path) })) + logger.error('Unsupported file type.') + return + } + + // the threshold may be too large + if (file.size > 5 * MB) { + window.toast.error(t('translate.files.error.too_large') + ' (0 ~ 5 MB)') + } else { + try { + const result = await window.api.fs.readText(file.path) + setText(text + result) + } catch (e) { + logger.error('Failed to read text file.', e as Error) + window.toast.error(t('translate.files.error.unknown') + ': ' + formatErrorMessage(e)) + } + } + } + const promise = _readFile() + window.toast.loading({ title: t('translate.files.reading'), promise }) + }, + [setText, t, text] + ) + + const ocrFile = useCallback( + async (file: SupportedOcrFile) => { + const ocrResult = await ocr(file) + setText(text + ocrResult.text) + }, + [ocr, setText, text] + ) + // 统一的文件处理 const processFile = useCallback( async (file: FileMetadata) => { @@ -486,53 +526,12 @@ const TranslatePage: FC = () => { const shouldOCR = isSupportedOcrFile(file) if (shouldOCR) { - try { - const ocrResult = await ocr(file) - setText(text + ocrResult.text) - } finally { - // do nothing when failed. because error should be handled inside - } + await ocrFile(file) } else { - try { - window.message.loading({ content: t('translate.files.reading'), key: 'translate_files_reading', duration: 0 }) - let isText: boolean - try { - // 检查文件是否为文本文件 - isText = await isTextFile(file.path) - } catch (e) { - logger.error('Failed to check if file is text.', e as Error) - window.message.error(t('translate.files.error.check_type') + ': ' + formatErrorMessage(e)) - throw e - } - - if (!isText) { - window.message.error({ - key: 'file_not_supported', - content: t('common.file.not_supported', { type: getFileExtension(file.path) }) - }) - logger.error('Unsupported file type.') - throw new Error('Unsupported file type') - } - - // the threshold may be too large - if (file.size > 5 * MB) { - window.message.error(t('translate.files.error.too_large') + ' (0 ~ 5 MB)') - } else { - try { - const result = await window.api.fs.readText(file.path) - setText(text + result) - } catch (e) { - logger.error('Failed to read text file.', e as Error) - window.message.error(t('translate.files.error.unknown') + ': ' + formatErrorMessage(e)) - } - } - } finally { - // do nothing when failed because error should be handled inside - window.message.destroy('translate_files_reading') - } + await readFile(file) } }, - [ocr, setText, t, text] + [ocrFile, readFile] ) // 点击上传文件按钮 @@ -544,11 +543,10 @@ const TranslatePage: FC = () => { if (!file) { return } - - return await processFile(file) + await processFile(file) } catch (e) { logger.error('Unknown error when selecting file.', e as Error) - window.message.error(t('translate.files.error.unknown') + ': ' + formatErrorMessage(e)) + window.toast.error(t('translate.files.error.unknown') + ': ' + formatErrorMessage(e)) } finally { clearFiles() setIsProcessing(false) @@ -560,10 +558,7 @@ const TranslatePage: FC = () => { if (files.length === 0) return null if (files.length > 1) { // 多文件上传时显示提示信息 - window.message.error({ - key: 'multiple_files', - content: t('translate.files.error.multiple') - }) + window.toast.error(t('translate.files.error.multiple')) return null } return files[0] @@ -589,10 +584,7 @@ const TranslatePage: FC = () => { // const supportedFiles = await filterSupportedFiles(_files, extensions) const data = await getTextFromDropEvent(e).catch((err) => { logger.error('getTextFromDropEvent', err) - window.message.error({ - key: 'file_error', - content: t('translate.files.error.unknown') - }) + window.toast.error(t('translate.files.error.unknown')) return null }) if (data === null) { @@ -602,10 +594,7 @@ const TranslatePage: FC = () => { const droppedFiles = await getFilesFromDropEvent(e).catch((err) => { logger.error('handleDrop:', err) - window.message.error({ - key: 'file_error', - content: t('translate.files.error.unknown') - }) + window.toast.error(t('translate.files.error.unknown')) return null }) @@ -634,7 +623,7 @@ const TranslatePage: FC = () => { async (event: React.ClipboardEvent) => { if (isProcessing) return setIsProcessing(true) - logger.debug('event', event) + // logger.debug('event', event) const clipboardText = event.clipboardData.getData('text') if (!isEmpty(clipboardText)) { // depend default. this branch is only for preventing files when clipboard contains text @@ -657,10 +646,7 @@ const TranslatePage: FC = () => { await window.api.file.write(tempFilePath, uint8Array) selectedFile = await window.api.file.get(tempFilePath) } else { - window.message.info({ - key: 'file_not_supported', - content: t('common.file.not_supported', { type: getFileExtension(filePath) }) - }) + window.toast.info(t('common.file.not_supported', { type: getFileExtension(filePath) })) return } } else { @@ -669,16 +655,13 @@ const TranslatePage: FC = () => { } if (!selectedFile) { - window.message.error({ - key: 'file_error', - content: t('translate.files.error.unknown') - }) + window.toast.error(t('translate.files.error.unknown')) return } await processFile(selectedFile) } catch (error) { logger.error('onPaste:', error as Error) - window.message.error(t('chat.input.file_error')) + window.toast.error(t('chat.input.file_error')) } } setIsProcessing(false) diff --git a/src/renderer/src/pages/translate/TranslateSettings.tsx b/src/renderer/src/pages/translate/TranslateSettings.tsx index 3495cf7b5..fd149e0d4 100644 --- a/src/renderer/src/pages/translate/TranslateSettings.tsx +++ b/src/renderer/src/pages/translate/TranslateSettings.tsx @@ -165,10 +165,7 @@ const TranslateSettings: FC<{ onChange={(value) => { const newPair: [TranslateLanguage, TranslateLanguage] = [getLanguageByLangcode(value), localPair[1]] if (newPair[0] === newPair[1]) { - window.message.warning({ - content: t('translate.language.same'), - key: 'translate-message' - }) + window.toast.warning(t('translate.language.same')) return } setLocalPair(newPair) @@ -186,10 +183,7 @@ const TranslateSettings: FC<{ onChange={(value) => { const newPair: [TranslateLanguage, TranslateLanguage] = [localPair[0], getLanguageByLangcode(value)] if (newPair[0] === newPair[1]) { - window.message.warning({ - content: t('translate.language.same'), - key: 'translate-message' - }) + window.toast.warning(t('translate.language.same')) return } setLocalPair(newPair) diff --git a/src/renderer/src/services/ApiService.ts b/src/renderer/src/services/ApiService.ts index 1d9fa438e..01807027b 100644 --- a/src/renderer/src/services/ApiService.ts +++ b/src/renderer/src/services/ApiService.ts @@ -366,9 +366,6 @@ export async function fetchModels(provider: Provider): Promise { } export function checkApiProvider(provider: Provider): void { - const key = 'api-check' - const style = { marginTop: '3vh' } - if ( provider.id !== 'ollama' && provider.id !== 'lmstudio' && @@ -376,18 +373,18 @@ export function checkApiProvider(provider: Provider): void { provider.id !== 'copilot' ) { if (!provider.apiKey) { - window.message.error({ content: i18n.t('message.error.enter.api.label'), key, style }) + window.toast.error(i18n.t('message.error.enter.api.label')) throw new Error(i18n.t('message.error.enter.api.label')) } } if (!provider.apiHost && provider.type !== 'vertexai') { - window.message.error({ content: i18n.t('message.error.enter.api.host'), key, style }) + window.toast.error(i18n.t('message.error.enter.api.host')) throw new Error(i18n.t('message.error.enter.api.host')) } if (isEmpty(provider.models)) { - window.message.error({ content: i18n.t('message.error.enter.model'), key, style }) + window.toast.error(i18n.t('message.error.enter.model')) throw new Error(i18n.t('message.error.enter.model')) } } diff --git a/src/renderer/src/services/AssistantService.ts b/src/renderer/src/services/AssistantService.ts index a8c95380a..734866f65 100644 --- a/src/renderer/src/services/AssistantService.ts +++ b/src/renderer/src/services/AssistantService.ts @@ -203,10 +203,7 @@ export async function createAssistantFromAgent(agent: Agent) { store.dispatch(addAssistant(assistant)) - window.message.success({ - content: i18n.t('message.assistant.added.content'), - key: 'assistant-added' - }) + window.toast.success(i18n.t('message.assistant.added.content')) return assistant } diff --git a/src/renderer/src/services/BackupService.ts b/src/renderer/src/services/BackupService.ts index 6216d2ae3..652360876 100644 --- a/src/renderer/src/services/BackupService.ts +++ b/src/renderer/src/services/BackupService.ts @@ -68,7 +68,7 @@ export async function backup(skipBackupFile: boolean) { const selectFolder = await window.api.file.selectFolder() if (selectFolder) { await window.api.backup.backup(filename, fileContnet, selectFolder, skipBackupFile) - window.message.success({ content: i18n.t('message.backup.success'), key: 'backup' }) + window.toast.success(i18n.t('message.backup.success')) } } @@ -101,7 +101,7 @@ export async function restore() { }) } catch (error) { logger.error('restore: Error restoring backup file:', error as Error) - window.message.error({ content: i18n.t('error.backup.file_format'), key: 'restore' }) + window.toast.error(i18n.t('error.backup.file_format')) } } } @@ -205,7 +205,7 @@ export async function backupToWebdav({ source: 'backup', channel: 'system' }) - showMessage && window.message.success({ content: i18n.t('message.backup.success'), key: 'backup' }) + showMessage && window.toast.success(i18n.t('message.backup.success')) // 清理旧备份文件 if (webdavMaxBackups > 0) { @@ -258,7 +258,7 @@ export async function backupToWebdav({ } store.dispatch(setWebDAVSyncState({ lastSyncError: 'Backup failed' })) - showMessage && window.message.error({ content: i18n.t('message.backup.failed'), key: 'backup' }) + showMessage && window.toast.error(i18n.t('message.backup.failed')) } } catch (error: any) { // if auto backup process, throw error @@ -276,7 +276,7 @@ export async function backupToWebdav({ channel: 'system' }) store.dispatch(setWebDAVSyncState({ lastSyncError: error.message })) - showMessage && window.message.error({ content: i18n.t('message.backup.failed'), key: 'backup' }) + showMessage && window.toast.error(i18n.t('message.backup.failed')) logger.error('[Backup] backupToWebdav: Error uploading file to WebDAV:', error) throw error } finally { @@ -311,7 +311,7 @@ export async function restoreFromWebdav(fileName?: string) { await handleData(JSON.parse(data)) } catch (error) { logger.error('[Backup] Error downloading file from WebDAV:', error as Error) - window.message.error({ content: i18n.t('error.backup.file_format'), key: 'restore' }) + window.toast.error(i18n.t('error.backup.file_format')) } } @@ -372,7 +372,7 @@ export async function backupToS3({ source: 'backup', channel: 'system' }) - showMessage && window.message.success({ content: i18n.t('message.backup.success'), key: 'backup' }) + showMessage && window.toast.success(i18n.t('message.backup.success')) // 清理旧备份文件 if (s3Config.maxBackups > 0) { @@ -410,7 +410,7 @@ export async function backupToS3({ } store.dispatch(setS3SyncState({ lastSyncError: 'Backup failed' })) - showMessage && window.message.error({ content: i18n.t('message.backup.failed'), key: 'backup' }) + showMessage && window.toast.error(i18n.t('message.backup.failed')) } } catch (error: any) { if (autoBackupProcess) { @@ -428,7 +428,7 @@ export async function backupToS3({ }) store.dispatch(setS3SyncState({ lastSyncError: error.message })) logger.error('backupToS3: Error uploading file to S3:', error) - showMessage && window.message.error({ content: i18n.t('message.backup.failed'), key: 'backup' }) + showMessage && window.toast.error(i18n.t('message.backup.failed')) throw error } finally { if (!autoBackupProcess) { @@ -835,7 +835,7 @@ export async function handleData(data: Record) { } await localStorage.setItem('persist:cherry-studio', data.localStorage['persist:cherry-studio']) - window.message.success({ content: i18n.t('message.restore.success'), key: 'restore' }) + window.toast.success(i18n.t('message.restore.success')) setTimeout(() => window.api.reload(), 1000) return } @@ -857,12 +857,12 @@ export async function handleData(data: Record) { }) } - window.message.success({ content: i18n.t('message.restore.success'), key: 'restore' }) + window.toast.success(i18n.t('message.restore.success')) setTimeout(() => window.api.reload(), 1000) return } - window.message.error({ content: i18n.t('error.backup.file_format'), key: 'restore' }) + window.toast.error(i18n.t('error.backup.file_format')) } async function backupDatabase() { @@ -1058,7 +1058,7 @@ export async function restoreFromLocal(fileName: string) { return true } catch (error) { logger.error('[LocalBackup] Restore failed:', error as Error) - window.message.error({ content: i18n.t('error.backup.file_format'), key: 'restore' }) + window.toast.error(i18n.t('error.backup.file_format')) throw error } } diff --git a/src/renderer/src/services/MessagesService.ts b/src/renderer/src/services/MessagesService.ts index 22f72f0fb..bbb5263c1 100644 --- a/src/renderer/src/services/MessagesService.ts +++ b/src/renderer/src/services/MessagesService.ts @@ -70,7 +70,7 @@ export function deleteMessageFiles(message: Message) { export function isGenerating() { return new Promise((resolve, reject) => { const generating = store.getState().runtime.generating - generating && window.message.warning({ content: i18n.t('message.switch.disabled'), key: 'switch-assistant' }) + generating && window.toast.warning(i18n.t('message.switch.disabled')) generating ? reject(false) : resolve(true) }) } @@ -203,29 +203,24 @@ export async function getMessageTitle(message: Message, length = 30): Promise(obj: T, keys: K[]): Omit { return result } +/** + * Makes specified properties required while keeping others as is + * @template T - The object type to modify + * @template K - Keys of T that should be required + * @example + * type User = { + * name?: string; + * age?: number; + * } + * + * type UserWithName = RequireSome + * // Result: { name: string; age?: number; } + */ +export type RequireSome = Omit & Required> + export type HexColor = string /** diff --git a/src/renderer/src/utils/__tests__/copy.test.ts b/src/renderer/src/utils/__tests__/copy.test.ts index 85480a686..5697fe6d6 100644 --- a/src/renderer/src/utils/__tests__/copy.test.ts +++ b/src/renderer/src/utils/__tests__/copy.test.ts @@ -21,8 +21,8 @@ const mockClipboard = { writeText: vi.fn() } -// Mock window.message -const mockMessage = { +// Mock window.toast +const mockedToast = { success: vi.fn() } @@ -61,8 +61,8 @@ describe('copy', () => { writable: true }) - Object.defineProperty(global.window, 'message', { - value: mockMessage, + Object.defineProperty(global.window, 'toast', { + value: mockedToast, writable: true }) @@ -86,7 +86,7 @@ describe('copy', () => { // 验证结果 expect(topicToMarkdown).toHaveBeenCalledWith(topic) expect(mockClipboard.writeText).toHaveBeenCalledWith(markdownContent) - expect(mockMessage.success).toHaveBeenCalledWith('message.copy.success') + expect(mockedToast.success).toHaveBeenCalledWith('message.copy.success') }) it('should handle export function errors', async () => { @@ -97,7 +97,7 @@ describe('copy', () => { await expect(copyTopicAsMarkdown(topic)).rejects.toThrow('Export error') expect(mockClipboard.writeText).not.toHaveBeenCalled() - expect(mockMessage.success).not.toHaveBeenCalled() + expect(mockedToast.success).not.toHaveBeenCalled() }) it('should handle clipboard write errors', async () => { @@ -110,7 +110,7 @@ describe('copy', () => { mockClipboard.writeText.mockRejectedValue(new Error('Clipboard error')) await expect(copyTopicAsMarkdown(topic)).rejects.toThrow('Clipboard error') - expect(mockMessage.success).not.toHaveBeenCalled() + expect(mockedToast.success).not.toHaveBeenCalled() }) }) @@ -128,7 +128,7 @@ describe('copy', () => { expect(topicToPlainText).toHaveBeenCalledWith(topic) expect(mockClipboard.writeText).toHaveBeenCalledWith(plainTextContent) - expect(mockMessage.success).toHaveBeenCalledWith('message.copy.success') + expect(mockedToast.success).toHaveBeenCalledWith('message.copy.success') }) it('should handle export function errors', async () => { @@ -139,7 +139,7 @@ describe('copy', () => { await expect(copyTopicAsPlainText(topic)).rejects.toThrow('Export error') expect(mockClipboard.writeText).not.toHaveBeenCalled() - expect(mockMessage.success).not.toHaveBeenCalled() + expect(mockedToast.success).not.toHaveBeenCalled() }) }) @@ -157,7 +157,7 @@ describe('copy', () => { expect(messageToPlainText).toHaveBeenCalledWith(message) expect(mockClipboard.writeText).toHaveBeenCalledWith(plainTextContent) - expect(mockMessage.success).toHaveBeenCalledWith('message.copy.success') + expect(mockedToast.success).toHaveBeenCalledWith('message.copy.success') }) it('should handle messageToPlainText errors', async () => { @@ -170,7 +170,7 @@ describe('copy', () => { await expect(copyMessageAsPlainText(message)).rejects.toThrow('Message conversion error') expect(mockClipboard.writeText).not.toHaveBeenCalled() - expect(mockMessage.success).not.toHaveBeenCalled() + expect(mockedToast.success).not.toHaveBeenCalled() }) }) diff --git a/src/renderer/src/utils/__tests__/download.test.ts b/src/renderer/src/utils/__tests__/download.test.ts index cd8780270..36c16b594 100644 --- a/src/renderer/src/utils/__tests__/download.test.ts +++ b/src/renderer/src/utils/__tests__/download.test.ts @@ -26,8 +26,8 @@ const mockRevokeObjectURL = vi.fn() // Mock fetch const mockFetch = vi.fn() -// Mock window.message -const mockMessage = { +// Mock window.toast +const mockedToast = { error: vi.fn(), success: vi.fn(), warning: vi.fn(), @@ -48,8 +48,8 @@ describe('download', () => { beforeEach(() => { vi.clearAllMocks() - // 设置 window.message mock - Object.defineProperty(window, 'message', { value: mockMessage, writable: true }) + // 设置 window.toast mock + Object.defineProperty(window, 'toast', { value: mockedToast, writable: true }) // 设置 DOM mock const mockElement = { @@ -218,7 +218,7 @@ describe('download', () => { expect(() => download('https://example.com/file.pdf')).not.toThrow() await waitForAsync() - expect(mockMessage.error).toHaveBeenCalledWith('下载失败:Network error') + expect(mockedToast.error).toHaveBeenCalledWith('下载失败:Network error') }) it('should handle fetch errors without message', async () => { @@ -227,7 +227,7 @@ describe('download', () => { expect(() => download('https://example.com/file.pdf')).not.toThrow() await waitForAsync() - expect(mockMessage.error).toHaveBeenCalledWith('下载失败') + expect(mockedToast.error).toHaveBeenCalledWith('下载失败') }) it('should handle HTTP errors gracefully', async () => { diff --git a/src/renderer/src/utils/__tests__/export.test.ts b/src/renderer/src/utils/__tests__/export.test.ts index da7cc6681..7a04b5919 100644 --- a/src/renderer/src/utils/__tests__/export.test.ts +++ b/src/renderer/src/utils/__tests__/export.test.ts @@ -695,9 +695,9 @@ describe('export', () => { } }) - // Mock window.message methods + // Mock window.toast methods vi.stubGlobal('window', { - message: { + toast: { success: vi.fn(), error: vi.fn(), warning: vi.fn(), diff --git a/src/renderer/src/utils/__tests__/image.test.ts b/src/renderer/src/utils/__tests__/image.test.ts index 7e799ba0d..0e0295739 100644 --- a/src/renderer/src/utils/__tests__/image.test.ts +++ b/src/renderer/src/utils/__tests__/image.test.ts @@ -23,9 +23,9 @@ vi.mock('html-to-image', () => ({ ) })) -// mock window.message +// mock window.toast beforeEach(() => { - window.message = { + window.toast = { error: vi.fn() } as any }) @@ -86,7 +86,7 @@ describe('utils/image', () => { Object.defineProperty(div, 'scrollHeight', { value: 40000, configurable: true }) const ref = { current: div } as React.RefObject await expect(captureScrollable(ref)).rejects.toBeUndefined() - expect(window.message.error).toHaveBeenCalled() + expect(window.toast.error).toHaveBeenCalled() }) }) diff --git a/src/renderer/src/utils/copy.ts b/src/renderer/src/utils/copy.ts index e79576901..04841a25c 100644 --- a/src/renderer/src/utils/copy.ts +++ b/src/renderer/src/utils/copy.ts @@ -6,17 +6,17 @@ import { messageToPlainText, topicToMarkdown, topicToPlainText } from './export' export const copyTopicAsMarkdown = async (topic: Topic) => { const markdown = await topicToMarkdown(topic) await navigator.clipboard.writeText(markdown) - window.message.success(i18next.t('message.copy.success')) + window.toast.success(i18next.t('message.copy.success')) } export const copyTopicAsPlainText = async (topic: Topic) => { const plainText = await topicToPlainText(topic) await navigator.clipboard.writeText(plainText) - window.message.success(i18next.t('message.copy.success')) + window.toast.success(i18next.t('message.copy.success')) } export const copyMessageAsPlainText = async (message: Message) => { const plainText = messageToPlainText(message) await navigator.clipboard.writeText(plainText) - window.message.success(i18next.t('message.copy.success')) + window.toast.success(i18next.t('message.copy.success')) } diff --git a/src/renderer/src/utils/download.ts b/src/renderer/src/utils/download.ts index 524da9e91..7c8abfecf 100644 --- a/src/renderer/src/utils/download.ts +++ b/src/renderer/src/utils/download.ts @@ -85,9 +85,9 @@ export const download = (url: string, filename?: string) => { logger.error('Download failed:', error) // 显示用户友好的错误提示 if (error.message) { - window.message?.error(`${i18n.t('message.download.failed')}:${error.message}`) + window.toast?.error(`${i18n.t('message.download.failed')}:${error.message}`) } else { - window.message?.error(i18n.t('message.download.failed')) + window.toast?.error(i18n.t('message.download.failed')) } }) } diff --git a/src/renderer/src/utils/export.ts b/src/renderer/src/utils/export.ts index e048a3e7c..d4fed4d02 100644 --- a/src/renderer/src/utils/export.ts +++ b/src/renderer/src/utils/export.ts @@ -401,7 +401,7 @@ export const exportTopicAsMarkdown = async ( excludeCitations?: boolean ): Promise => { if (getExportState()) { - window.message.warning({ content: i18n.t('message.warn.export.exporting'), key: 'markdown-exporting' }) + window.toast.warning(i18n.t('message.warn.export.exporting')) return } @@ -414,13 +414,10 @@ export const exportTopicAsMarkdown = async ( const markdown = await topicToMarkdown(topic, exportReasoning, excludeCitations) const result = await window.api.file.save(fileName, markdown) if (result) { - window.message.success({ - content: i18n.t('message.success.markdown.export.specified'), - key: 'markdown-success' - }) + window.toast.success(i18n.t('message.success.markdown.export.specified')) } } catch (error: any) { - window.message.error({ content: i18n.t('message.error.markdown.export.specified'), key: 'markdown-error' }) + window.toast.error(i18n.t('message.error.markdown.export.specified')) logger.error('Failed to export topic as markdown:', error) } finally { setExportingState(false) @@ -431,9 +428,9 @@ export const exportTopicAsMarkdown = async ( const fileName = removeSpecialCharactersForFileName(topic.name) + ` ${timestamp}.md` const markdown = await topicToMarkdown(topic, exportReasoning, excludeCitations) await window.api.file.write(markdownExportPath + '/' + fileName, markdown) - window.message.success({ content: i18n.t('message.success.markdown.export.preconf'), key: 'markdown-success' }) + window.toast.success(i18n.t('message.success.markdown.export.preconf')) } catch (error: any) { - window.message.error({ content: i18n.t('message.error.markdown.export.preconf'), key: 'markdown-error' }) + window.toast.error(i18n.t('message.error.markdown.export.preconf')) logger.error('Failed to export topic as markdown:', error) } finally { setExportingState(false) @@ -447,7 +444,7 @@ export const exportMessageAsMarkdown = async ( excludeCitations?: boolean ): Promise => { if (getExportState()) { - window.message.warning({ content: i18n.t('message.warn.export.exporting'), key: 'markdown-exporting' }) + window.toast.warning(i18n.t('message.warn.export.exporting')) return } @@ -463,13 +460,10 @@ export const exportMessageAsMarkdown = async ( : messageToMarkdown(message, excludeCitations) const result = await window.api.file.save(fileName, markdown) if (result) { - window.message.success({ - content: i18n.t('message.success.markdown.export.specified'), - key: 'markdown-success' - }) + window.toast.success(i18n.t('message.success.markdown.export.specified')) } } catch (error: any) { - window.message.error({ content: i18n.t('message.error.markdown.export.specified'), key: 'markdown-error' }) + window.toast.error(i18n.t('message.error.markdown.export.specified')) logger.error('Failed to export message as markdown:', error) } finally { setExportingState(false) @@ -483,9 +477,9 @@ export const exportMessageAsMarkdown = async ( ? messageToMarkdownWithReasoning(message, excludeCitations) : messageToMarkdown(message, excludeCitations) await window.api.file.write(markdownExportPath + '/' + fileName, markdown) - window.message.success({ content: i18n.t('message.success.markdown.export.preconf'), key: 'markdown-success' }) + window.toast.success(i18n.t('message.success.markdown.export.preconf')) } catch (error: any) { - window.message.error({ content: i18n.t('message.error.markdown.export.preconf'), key: 'markdown-error' }) + window.toast.error(i18n.t('message.error.markdown.export.preconf')) logger.error('Failed to export message as markdown:', error) } finally { setExportingState(false) @@ -575,18 +569,18 @@ const convertThinkingToNotionBlocks = async (thinkingContent: string): Promise => { if (getExportState()) { - window.message.warning({ content: i18n.t('message.warn.export.exporting'), key: 'notion-exporting' }) + window.toast.warning(i18n.t('message.warn.export.exporting')) return false } const { notionDatabaseID, notionApiKey } = store.getState().settings if (!notionApiKey || !notionDatabaseID) { - window.message.error({ content: i18n.t('message.error.notion.no_api_key'), key: 'notion-no-apikey-error' }) + window.toast.error(i18n.t('message.error.notion.no_api_key')) return false } if (allBlocks.length === 0) { - window.message.error({ content: i18n.t('message.error.notion.export'), key: 'notion-no-content-error' }) + window.toast.error(i18n.t('message.error.notion.export')) return false } @@ -600,13 +594,7 @@ const executeNotionExport = async (title: string, allBlocks: any[]): Promise => { if (getExportState()) { - window.message.warning({ content: i18n.t('message.warn.export.exporting'), key: 'obsidian-exporting' }) + window.toast.warning(i18n.t('message.warn.export.exporting')) return } @@ -797,12 +772,12 @@ export const exportMarkdownToObsidian = async (attributes: any): Promise = let isMarkdownFile = false if (!obsidianVault) { - window.message.error(i18n.t('chat.topics.export.obsidian_no_vault_selected')) + window.toast.error(i18n.t('chat.topics.export.obsidian_no_vault_selected')) return } if (!attributes.title) { - window.message.error(i18n.t('chat.topics.export.obsidian_title_required')) + window.toast.error(i18n.t('chat.topics.export.obsidian_title_required')) return } @@ -839,10 +814,10 @@ export const exportMarkdownToObsidian = async (attributes: any): Promise = } window.open(obsidianUrl) - window.message.success(i18n.t('chat.topics.export.obsidian_export_success')) + window.toast.success(i18n.t('chat.topics.export.obsidian_export_success')) } catch (error) { logger.error('Failed to export to Obsidian:', error as Error) - window.message.error(i18n.t('chat.topics.export.obsidian_export_failed')) + window.toast.error(i18n.t('chat.topics.export.obsidian_export_failed')) } finally { setExportingState(false) } @@ -900,12 +875,12 @@ export const exportMarkdownToJoplin = async ( const { joplinUrl, joplinToken, joplinExportReasoning, excludeCitationsInExport } = store.getState().settings if (getExportState()) { - window.message.warning({ content: i18n.t('message.warn.export.exporting'), key: 'joplin-exporting' }) + window.toast.warning(i18n.t('message.warn.export.exporting')) return } if (!joplinUrl || !joplinToken) { - window.message.error(i18n.t('message.error.joplin.no_config')) + window.toast.error(i18n.t('message.error.joplin.no_config')) return } @@ -946,11 +921,11 @@ export const exportMarkdownToJoplin = async ( throw new Error('response error') } - window.message.success(i18n.t('message.success.joplin.export')) + window.toast.success(i18n.t('message.success.joplin.export')) return data } catch (error: any) { logger.error('Failed to export to Joplin:', error) - window.message.error(i18n.t('message.error.joplin.export')) + window.toast.error(i18n.t('message.error.joplin.export')) return null } finally { setExportingState(false) @@ -966,12 +941,12 @@ export const exportMarkdownToSiyuan = async (title: string, content: string): Pr const { siyuanApiUrl, siyuanToken, siyuanBoxId, siyuanRootPath } = store.getState().settings if (getExportState()) { - window.message.warning({ content: i18n.t('message.warn.export.exporting'), key: 'siyuan-exporting' }) + window.toast.warning(i18n.t('message.warn.export.exporting')) return } if (!siyuanApiUrl || !siyuanToken || !siyuanBoxId) { - window.message.error({ content: i18n.t('message.error.siyuan.no_config'), key: 'siyuan-no-config-error' }) + window.toast.error(i18n.t('message.error.siyuan.no_config')) return } @@ -1006,16 +981,10 @@ export const exportMarkdownToSiyuan = async (title: string, content: string): Pr // 创建文档 await createSiyuanDoc(siyuanApiUrl, siyuanToken, siyuanBoxId, docPath, content) - window.message.success({ - content: i18n.t('message.success.siyuan.export'), - key: 'siyuan-success' - }) + window.toast.success(i18n.t('message.success.siyuan.export')) } catch (error) { logger.error('Failed to export to Siyuan:', error as Error) - window.message.error({ - content: i18n.t('message.error.siyuan.export') + (error instanceof Error ? `: ${error.message}` : ''), - key: 'siyuan-error' - }) + window.toast.error(i18n.t('message.error.siyuan.export') + (error instanceof Error ? `: ${error.message}` : '')) } finally { setExportingState(false) } @@ -1092,18 +1061,12 @@ export const exportMessageToNotes = async ( const cleanedContent = content.replace(/^## 🤖 Assistant(\n|$)/m, '') const note = await createNote(title, cleanedContent, folderPath) - window.message.success({ - content: i18n.t('message.success.notes.export'), - key: 'notes-export-success' - }) + window.toast.success(i18n.t('message.success.notes.export')) return note } catch (error) { logger.error('导出到笔记失败:', error as Error) - window.message.error({ - content: i18n.t('message.error.notes.export'), - key: 'notes-export-error' - }) + window.toast.error(i18n.t('message.error.notes.export')) throw error } } @@ -1119,18 +1082,12 @@ export const exportTopicToNotes = async (topic: Topic, folderPath: string): Prom const content = await topicToMarkdown(topic) const note = await createNote(topic.name, content, folderPath) - window.message.success({ - content: i18n.t('message.success.notes.export'), - key: 'notes-export-success' - }) + window.toast.success(i18n.t('message.success.notes.export')) return note } catch (error) { logger.error('导出到笔记失败:', error as Error) - window.message.error({ - content: i18n.t('message.error.notes.export'), - key: 'notes-export-error' - }) + window.toast.error(i18n.t('message.error.notes.export')) throw error } } diff --git a/src/renderer/src/utils/image.ts b/src/renderer/src/utils/image.ts index a9a15e468..a42f372f3 100644 --- a/src/renderer/src/utils/image.ts +++ b/src/renderer/src/utils/image.ts @@ -98,10 +98,7 @@ export const captureScrollable = async (elRef: React.RefObject { const style = doc.createElement('style') - style.textContent = `*, *::before, *::after { - animation: none !important; - transition: none !important; - // transform: none !important; + style.textContent = `*, *::before, *::after { + animation: none !important; + transition: none !important; + // transform: none !important; }` doc.head.appendChild(style) return style diff --git a/src/renderer/src/utils/mcp-tools.ts b/src/renderer/src/utils/mcp-tools.ts index c9de9e1c5..417509434 100644 --- a/src/renderer/src/utils/mcp-tools.ts +++ b/src/renderer/src/utils/mcp-tools.ts @@ -85,7 +85,7 @@ export function openAIToolsToMcpTool( } } catch (error) { logger.error(`Error parsing tool call: ${toolCall}`, error as Error) - window.message.error(t('chat.mcp.error.parse_tool_call', { toolCall: toolCall })) + window.toast.error(t('chat.mcp.error.parse_tool_call', { toolCall: toolCall })) return undefined } const tools = mcpTools.filter((mcpTool) => { @@ -93,12 +93,12 @@ export function openAIToolsToMcpTool( }) if (tools.length > 1) { logger.warn(`Multiple MCP Tools found for tool call: ${toolName}`) - window.message.warning(t('chat.mcp.warning.multiple_tools', { tool: tools[0].name })) + window.toast.warning(t('chat.mcp.warning.multiple_tools', { tool: tools[0].name })) } if (tools.length === 0) { logger.warn(`No MCP Tool found for tool call: ${toolName}`) - window.message.warning(t('chat.mcp.warning.no_tool', { tool: toolName })) + window.toast.warning(t('chat.mcp.warning.no_tool', { tool: toolName })) return undefined } @@ -197,12 +197,12 @@ export function anthropicToolUseToMcpTool(mcpTools: MCPTool[] | undefined, toolU const tools = mcpTools.filter((tool) => tool.id === toolUse.name) if (tools.length === 0) { logger.warn(`No MCP Tool found for tool call: ${toolUse.name}`) - window.message.warning(t('chat.mcp.warning.no_tool', { tool: toolUse.name })) + window.toast.warning(t('chat.mcp.warning.no_tool', { tool: toolUse.name })) return undefined } if (tools.length > 1) { logger.warn(`Multiple MCP Tools found for tool call: ${toolUse.name}`) - window.message.warning(t('chat.mcp.warning.multiple_tools', { tool: tools[0].name })) + window.toast.warning(t('chat.mcp.warning.multiple_tools', { tool: tools[0].name })) } return tools[0] } @@ -243,12 +243,12 @@ export function geminiFunctionCallToMcpTool( const tools = mcpTools.filter((tool) => tool.id.includes(toolName) || tool.name.includes(toolName)) if (tools.length > 1) { logger.warn(`Multiple MCP Tools found for tool call: ${toolName}`) - window.message.warning(t('chat.mcp.warning.multiple_tools', { tool: tools[0].name })) + window.toast.warning(t('chat.mcp.warning.multiple_tools', { tool: tools[0].name })) } if (tools.length === 0) { logger.warn(`No MCP Tool found for tool call: ${toolName}`) - window.message.warning(t('chat.mcp.warning.no_tool', { tool: toolName })) + window.toast.warning(t('chat.mcp.warning.no_tool', { tool: toolName })) return undefined } @@ -369,7 +369,7 @@ export function parseToolUse( const mcpTool = mcpTools.find((tool) => tool.id === toolName || tool.name === toolName) if (!mcpTool) { logger.error(`Tool "${toolName}" not found in MCP tools`) - window.message.error(i18n.t('settings.mcp.errors.toolNotFound', { name: toolName })) + window.toast.error(i18n.t('settings.mcp.errors.toolNotFound', { name: toolName })) continue } diff --git a/src/renderer/src/utils/oauth.ts b/src/renderer/src/utils/oauth.ts index 8f40c088d..9fbb632a0 100644 --- a/src/renderer/src/utils/oauth.ts +++ b/src/renderer/src/utils/oauth.ts @@ -52,7 +52,7 @@ export const oauthWithAihubmix = async (setKey) => { } catch (error) { logger.error('[oauthWithAihubmix] error', error as Error) popup?.close() - window.message.error(i18n.t('settings.provider.oauth.error')) + window.toast.error(i18n.t('settings.provider.oauth.error')) } } } @@ -140,7 +140,7 @@ export const oauthWithTokenFlux = async () => { const callbackUrl = `${TOKENFLUX_HOST}/auth/callback?redirect_to=/dashboard/api-keys` const resp = await fetch(`${TOKENFLUX_HOST}/api/auth/auth-url?type=login&callback=${callbackUrl}`, {}) if (!resp.ok) { - window.message.error(i18n.t('settings.provider.oauth.error')) + window.toast.error(i18n.t('settings.provider.oauth.error')) return } const data = await resp.json() diff --git a/src/renderer/src/windows/mini/MiniWindowApp.tsx b/src/renderer/src/windows/mini/MiniWindowApp.tsx index 7b7753dba..b459a3e02 100644 --- a/src/renderer/src/windows/mini/MiniWindowApp.tsx +++ b/src/renderer/src/windows/mini/MiniWindowApp.tsx @@ -1,9 +1,11 @@ import '@renderer/databases' +import { HeroUIProvider } from '@heroui/react' import { ErrorBoundary } from '@renderer/components/ErrorBoundary' +import { ToastPortal } from '@renderer/components/ToastPortal' +import { getToastUtilities } from '@renderer/components/TopView/toast' import { useSettings } from '@renderer/hooks/useSettings' import store, { persistor } from '@renderer/store' -import { message } from 'antd' import { useEffect } from 'react' import { Provider } from 'react-redux' import { PersistGate } from 'redux-persist/integration/react' @@ -35,24 +37,26 @@ function MiniWindowContent(): React.ReactElement { } function MiniWindow(): React.ReactElement { - //miniWindow should register its own message component - const [messageApi, messageContextHolder] = message.useMessage() - window.message = messageApi + useEffect(() => { + window.toast = getToastUtilities() + }, []) return ( - - - - - - {messageContextHolder} - - - - - - + + + + + + + + + + + + + + ) } diff --git a/src/renderer/src/windows/mini/home/HomeWindow.tsx b/src/renderer/src/windows/mini/home/HomeWindow.tsx index ca654d978..9d9b90520 100644 --- a/src/renderer/src/windows/mini/home/HomeWindow.tsx +++ b/src/renderer/src/windows/mini/home/HomeWindow.tsx @@ -465,7 +465,7 @@ const HomeWindow: FC<{ draggable?: boolean }> = ({ draggable = true }) => { if (lastMessage) { const content = getMainTextContent(lastMessage) navigator.clipboard.writeText(content) - window.message.success(t('message.copy.success')) + window.toast.success(t('message.copy.success')) } }, [currentTopic, t]) diff --git a/src/renderer/src/windows/mini/translate/TranslateWindow.tsx b/src/renderer/src/windows/mini/translate/TranslateWindow.tsx index 1973f51a2..681e96de1 100644 --- a/src/renderer/src/windows/mini/translate/TranslateWindow.tsx +++ b/src/renderer/src/windows/mini/translate/TranslateWindow.tsx @@ -65,7 +65,7 @@ const Translate: FC = ({ text }) => { useHotkeys('c', () => { navigator.clipboard.writeText(result) - window.message.success(t('message.copy.success')) + window.toast.success(t('message.copy.success')) }) return ( diff --git a/src/renderer/src/windows/selection/action/components/WindowFooter.tsx b/src/renderer/src/windows/selection/action/components/WindowFooter.tsx index 30832190b..5c0af0207 100644 --- a/src/renderer/src/windows/selection/action/components/WindowFooter.tsx +++ b/src/renderer/src/windows/selection/action/components/WindowFooter.tsx @@ -132,7 +132,7 @@ const WindowFooter: FC = ({ navigator.clipboard .writeText(content) .then(() => { - window.message.success(t('message.copy.success')) + window.toast.success(t('message.copy.success')) setIsCopyHovered(true) setTimeoutTimer( 'handleCopy', @@ -143,7 +143,7 @@ const WindowFooter: FC = ({ ) }) .catch(() => { - window.message.error(t('message.copy.failed')) + window.toast.error(t('message.copy.failed')) }) } diff --git a/src/renderer/src/windows/selection/action/entryPoint.tsx b/src/renderer/src/windows/selection/action/entryPoint.tsx index 4dcdd916f..b66303e4e 100644 --- a/src/renderer/src/windows/selection/action/entryPoint.tsx +++ b/src/renderer/src/windows/selection/action/entryPoint.tsx @@ -1,15 +1,17 @@ import '@renderer/assets/styles/index.css' import '@ant-design/v5-patch-for-react-19' +import { HeroUIProvider } from '@heroui/react' import KeyvStorage from '@kangfenmao/keyv-storage' import { loggerService } from '@logger' +import { ToastPortal } from '@renderer/components/ToastPortal' +import { getToastUtilities } from '@renderer/components/TopView/toast' import AntdProvider from '@renderer/context/AntdProvider' import { CodeStyleProvider } from '@renderer/context/CodeStyleProvider' import { ThemeProvider } from '@renderer/context/ThemeProvider' import storeSyncService from '@renderer/services/StoreSyncService' import store, { persistor } from '@renderer/store' -import { message } from 'antd' -import { FC } from 'react' +import { FC, useEffect } from 'react' import { createRoot } from 'react-dom/client' import { Provider } from 'react-redux' import { PersistGate } from 'redux-persist/integration/react' @@ -34,21 +36,24 @@ storeSyncService.subscribe() const App: FC = () => { //actionWindow should register its own message component - const [messageApi, messageContextHolder] = message.useMessage() - window.message = messageApi + useEffect(() => { + window.toast = getToastUtilities() + }, []) return ( - - - - - {messageContextHolder} - - - - - + + + + + + + + + + + + ) }