diff --git a/src/renderer/src/App.tsx b/src/renderer/src/App.tsx index 78396c49e7..cffd79afe6 100644 --- a/src/renderer/src/App.tsx +++ b/src/renderer/src/App.tsx @@ -6,7 +6,6 @@ import { QueryClient, QueryClientProvider } from '@tanstack/react-query' import { Provider } from 'react-redux' import { PersistGate } from 'redux-persist/integration/react' -import { ToastPortal } from './components/ToastPortal' import TopViewContainer from './components/TopView' import AntdProvider from './context/AntdProvider' import { CodeStyleProvider } from './context/CodeStyleProvider' @@ -50,7 +49,6 @@ function App(): React.ReactElement { - diff --git a/src/renderer/src/components/MessageInitializer.tsx b/src/renderer/src/components/MessageInitializer.tsx new file mode 100644 index 0000000000..664c4413b9 --- /dev/null +++ b/src/renderer/src/components/MessageInitializer.tsx @@ -0,0 +1,21 @@ +import { App } from 'antd' +import { useEffect } from 'react' + +import { initMessageApi } from './TopView/toast' + +/** + * MessageInitializer component + * This component initializes the message API from App.useApp() hook + * It should be rendered inside the AntdProvider (which wraps App component) + * so that the message API can inherit the correct theme (dark/light mode) + */ +export const MessageInitializer = () => { + const { message } = App.useApp() + + useEffect(() => { + // Initialize the message API for use in toast.ts + initMessageApi(message) + }, [message]) + + return null +} diff --git a/src/renderer/src/components/ToastPortal.tsx b/src/renderer/src/components/ToastPortal.tsx deleted file mode 100644 index b149ae6fdf..0000000000 --- a/src/renderer/src/components/ToastPortal.tsx +++ /dev/null @@ -1,32 +0,0 @@ -import { ToastProvider } from '@heroui/toast' -import { useEffect, useState } from 'react' -import { createPortal } from 'react-dom' - -export const ToastPortal = () => { - const [mounted, setMounted] = useState(false) - - useEffect(() => { - setMounted(true) - return () => setMounted(false) - }, []) - - if (!mounted) return null - - return createPortal( - , - document.body - ) -} diff --git a/src/renderer/src/components/TopView/toast.ts b/src/renderer/src/components/TopView/toast.ts deleted file mode 100644 index d1e5726310..0000000000 --- a/src/renderer/src/components/TopView/toast.ts +++ /dev/null @@ -1,72 +0,0 @@ -import { addToast, closeAll, closeToast, getToastQueue, isToastClosing } from '@heroui/toast' -import type { 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/TopView/toast.tsx b/src/renderer/src/components/TopView/toast.tsx new file mode 100644 index 0000000000..081a436892 --- /dev/null +++ b/src/renderer/src/components/TopView/toast.tsx @@ -0,0 +1,231 @@ +import type { RequireSome } from '@renderer/types' +import { message as antdMessage } from 'antd' +import type { MessageInstance } from 'antd/es/message/interface' +import type React from 'react' + +// Global message instance for static usage +let messageApi: MessageInstance | null = null + +// Initialize message API - should be called once the App component is mounted +export const initMessageApi = (api: MessageInstance) => { + messageApi = api +} + +// Get message API instance +const getMessageApi = (): MessageInstance => { + if (!messageApi) { + // Fallback to static method if hook API is not available + return antdMessage + } + return messageApi +} + +type ToastColor = 'danger' | 'success' | 'warning' | 'default' +type MessageType = 'error' | 'success' | 'warning' | 'info' + +interface ToastConfig { + title?: React.ReactNode + icon?: React.ReactNode + description?: React.ReactNode + timeout?: number + key?: string | number + className?: string + style?: React.CSSProperties + onClick?: () => void + onClose?: () => void +} + +interface LoadingToastConfig extends ToastConfig { + promise: Promise +} + +const colorToType = (color: ToastColor): MessageType => { + switch (color) { + case 'danger': + return 'error' + case 'success': + return 'success' + case 'warning': + return 'warning' + case 'default': + return 'info' + } +} + +// Toast content component +const ToastContent: React.FC<{ title?: React.ReactNode; description?: React.ReactNode; icon?: React.ReactNode }> = ({ + title, + description, + icon +}) => { + return ( +
+ {(icon || title) && ( +
+ {icon} + {title} +
+ )} + {description &&
{description}
} +
+ ) +} + +const createToast = (color: ToastColor) => { + return (arg: ToastConfig | string): string | null => { + const api = getMessageApi() + const type = colorToType(color) as 'error' | 'success' | 'warning' | 'info' + + if (typeof arg === 'string') { + // antd message methods return a function to close the message + api[type](arg) + return null + } + + const { title, description, icon, timeout, ...restConfig } = arg + + // Convert timeout from milliseconds to seconds (antd uses seconds) + const duration = timeout !== undefined ? timeout / 1000 : 3 + + return ( + (api.open({ + type: type as 'error' | 'success' | 'warning' | 'info', + content: , + duration, + ...restConfig + }) as any) || null + ) + } +} + +/** + * 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 + */ +export const loading = (args: RequireSome): string | null => { + const api = getMessageApi() + const { title, description, icon, promise, timeout, ...restConfig } = args + + // Generate unique key for this loading message + const key = args.key || `loading-${Date.now()}-${Math.random()}` + + // Show loading message + api.loading({ + content: , + duration: 0, // Don't auto-close + key, + ...restConfig + }) + + // Handle promise resolution + promise + .then((result) => { + api.success({ + content: , + duration: timeout !== undefined ? timeout / 1000 : 2, + key, + ...restConfig + }) + return result + }) + .catch((err) => { + api.error({ + content: ( + + ), + duration: timeout !== undefined ? timeout / 1000 : 3, + key, + ...restConfig + }) + throw err + }) + + return key as string +} + +/** + * Add a toast notification + * @param config - Toast configuration object + * @returns Toast ID or null + */ +export const addToast = (config: ToastConfig) => info(config) + +/** + * Close a specific toast notification by its key + * @param key - Toast key (string) + */ +export const closeToast = (key: string) => { + getMessageApi().destroy(key) +} + +/** + * Close all toast notifications + */ +export const closeAll = () => { + getMessageApi().destroy() +} + +/** + * Stub functions for compatibility with previous toast API + * These are no-ops since antd message doesn't expose a queue + */ + +/** + * @deprecated This function is a no-op stub for backward compatibility only. + * Antd message doesn't expose a queue. Do not rely on this function. + * @returns Empty toast queue stub + */ +export const getToastQueue = (): any => ({ toasts: [] }) + +/** + * @deprecated This function is a no-op stub for backward compatibility only. + * Antd message doesn't track closing state. Do not rely on this function. + * @param key - Toast key (unused) + * @returns Always returns false + */ +export const isToastClosing = (key?: string): boolean => { + key // unused + return false +} + +export const getToastUtilities = () => + ({ + getToastQueue, + addToast, + closeToast, + closeAll, + isToastClosing, + error, + success, + warning, + info, + loading + }) as const diff --git a/src/renderer/src/context/AntdProvider.tsx b/src/renderer/src/context/AntdProvider.tsx index 685b3b0fbd..7981505987 100644 --- a/src/renderer/src/context/AntdProvider.tsx +++ b/src/renderer/src/context/AntdProvider.tsx @@ -1,6 +1,7 @@ +import { MessageInitializer } from '@renderer/components/MessageInitializer' import { useSettings } from '@renderer/hooks/useSettings' import type { LanguageVarious } from '@renderer/types' -import { ConfigProvider, theme } from 'antd' +import { App, ConfigProvider, theme } from 'antd' import deDE from 'antd/locale/de_DE' import elGR from 'antd/locale/el_GR' import enUS from 'antd/locale/en_US' @@ -114,7 +115,15 @@ const AntdProvider: FC = ({ children }) => { motionDurationMid: '100ms' } }}> - {children} + + + {children} + ) } diff --git a/src/renderer/src/env.d.ts b/src/renderer/src/env.d.ts index 563d74cb8d..f18d250d1f 100644 --- a/src/renderer/src/env.d.ts +++ b/src/renderer/src/env.d.ts @@ -1,12 +1,22 @@ /// import type { PermissionUpdate } from '@anthropic-ai/claude-agent-sdk' -import type { addToast, closeAll, closeToast, getToastQueue, isToastClosing } from '@heroui/toast' import type KeyvStorage from '@kangfenmao/keyv-storage' import type { HookAPI } from 'antd/es/modal/useModal' import type { NavigateFunction } from 'react-router-dom' -import type { error, info, loading, success, warning } from './components/TopView/toast' +import type { + addToast, + closeAll, + closeToast, + error, + getToastQueue, + info, + isToastClosing, + loading, + success, + warning +} from './components/TopView/toast' interface ImportMetaEnv { VITE_RENDERER_INTEGRATED_MODEL: string diff --git a/src/renderer/src/utils/dataLimit.ts b/src/renderer/src/utils/dataLimit.ts index d5f9e572c1..2c8db0697b 100644 --- a/src/renderer/src/utils/dataLimit.ts +++ b/src/renderer/src/utils/dataLimit.ts @@ -69,8 +69,10 @@ export async function checkDataLimit() { const toastId = window.toast.warning({ title: t('settings.data.limit.appDataDiskQuota'), description: t('settings.data.limit.appDataDiskQuotaDescription'), - timeout: 0, // Never auto-dismiss - hideCloseButton: true // Hide close button so user cannot dismiss + timeout: 0 // Never auto-dismiss + // hideCloseButton: true // Hide close button so user cannot dismiss + // commented out because antd message doesn't support hiding close button + // so we just rely on the timeout: 0 to keep it persistent }) currentToastId = toastId diff --git a/src/renderer/src/windows/mini/MiniWindowApp.tsx b/src/renderer/src/windows/mini/MiniWindowApp.tsx index 0f6d8bc60d..cb8a691782 100644 --- a/src/renderer/src/windows/mini/MiniWindowApp.tsx +++ b/src/renderer/src/windows/mini/MiniWindowApp.tsx @@ -1,7 +1,6 @@ import '@renderer/databases' import { ErrorBoundary } from '@renderer/components/ErrorBoundary' -import { ToastPortal } from '@renderer/components/ToastPortal' import { getToastUtilities } from '@renderer/components/TopView/toast' import { HeroUIProvider } from '@renderer/context/HeroUIProvider' import { useSettings } from '@renderer/hooks/useSettings' @@ -55,7 +54,6 @@ function MiniWindow(): React.ReactElement { - ) diff --git a/src/renderer/src/windows/selection/action/entryPoint.tsx b/src/renderer/src/windows/selection/action/entryPoint.tsx index e1ed8bac3a..d2a2d70972 100644 --- a/src/renderer/src/windows/selection/action/entryPoint.tsx +++ b/src/renderer/src/windows/selection/action/entryPoint.tsx @@ -4,7 +4,6 @@ import '@ant-design/v5-patch-for-react-19' 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' @@ -54,7 +53,6 @@ const App: FC = () => { - )