import { createContext, useContext, useState, useCallback, useEffect, type ReactNode, Component, type ErrorInfo } from 'react'; export type ToastLevel = 'info' | 'success' | 'error'; interface Toast { id: string; message: string; level: ToastLevel; } interface ToastContextType { toasts: Toast[]; notify: (message: string, level?: ToastLevel, duration?: number) => void; notifyError: (message: string) => void; notifySuccess: (message: string) => void; dismiss: (id: string) => void; } const ToastContext = createContext(undefined); export function ToastProvider({ children }: { children: ReactNode }) { const [toasts, setToasts] = useState([]); const dismiss = useCallback((id: string) => { setToasts(prev => prev.filter(t => t.id !== id)); }, []); const notify = useCallback((message: string, level: ToastLevel = 'info', duration = 4000) => { const id = crypto.randomUUID(); setToasts(prev => [...prev.slice(-4), { id, message, level }]); setTimeout(() => dismiss(id), duration); }, [dismiss]); const notifyError = useCallback((message: string) => notify(message, 'error', 6000), [notify]); const notifySuccess = useCallback((message: string) => notify(message, 'success', 3000), [notify]); // Listen for global error events from store/other modules that can't access context directly useEffect(() => { const handler = (e: Event) => { const msg = (e as CustomEvent).detail; if (msg) notifyError(String(msg)); }; window.addEventListener('muse:error', handler); window.addEventListener('muse:success', (e: Event) => notifySuccess(String((e as CustomEvent).detail))); window.addEventListener('muse:info', (e: Event) => notify(String((e as CustomEvent).detail))); return () => { window.removeEventListener('muse:error', handler); }; }, [notifyError, notifySuccess, notify]); return ( {children} {toasts.length > 0 && (
{toasts.map(toast => (
dismiss(toast.id)} > {toast.level === 'error' && } {toast.level === 'success' && } {toast.message}
))}
)}
); } export function useToast() { const ctx = useContext(ToastContext); if (!ctx) throw new Error('useToast must be used within ToastProvider'); return ctx; } /** * Safe invoke wrapper — surfaces Rust command errors as toast notifications. * Use instead of raw invoke().catch(() => {}). */ export async function safeInvoke( command: string, args: Record = {}, options?: { silent?: boolean; label?: string } ): Promise { const { invoke } = await import('@tauri-apps/api/core'); try { return await invoke(command, args); } catch (err) { const message = typeof err === 'string' ? err : (err as Error)?.message || 'Unknown error'; if (!options?.silent) { const label = options?.label || command.replace(/_/g, ' '); window.dispatchEvent(new CustomEvent('muse:error', { detail: `${label}: ${message}` })); } console.error(`[invoke:${command}]`, err); return undefined; } } /** * React Error Boundary — catches render crashes and shows recovery UI. */ interface ErrorBoundaryProps { children: ReactNode; fallback?: ReactNode; } interface ErrorBoundaryState { hasError: boolean; error: Error | null; } export class ErrorBoundary extends Component { constructor(props: ErrorBoundaryProps) { super(props); this.state = { hasError: false, error: null }; } static getDerivedStateFromError(error: Error) { return { hasError: true, error }; } componentDidCatch(error: Error, info: ErrorInfo) { console.error('[ErrorBoundary]', error, info.componentStack); } render() { if (this.state.hasError) { return this.props.fallback || (

Something went wrong

{this.state.error?.message || 'An unexpected error occurred'}

); } return this.props.children; } }