File size: 5,955 Bytes
3d7d9b5
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
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<ToastContextType | undefined>(undefined);

export function ToastProvider({ children }: { children: ReactNode }) {
  const [toasts, setToasts] = useState<Toast[]>([]);

  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 (
    <ToastContext.Provider value={{ toasts, notify, notifyError, notifySuccess, dismiss }}>

      {children}

      {toasts.length > 0 && (

        <div className="fixed bottom-4 left-1/2 -translate-x-1/2 z-[9999] flex flex-col gap-2 items-center pointer-events-none">

          {toasts.map(toast => (

            <div

              key={toast.id}

              className={`pointer-events-auto px-4 py-2.5 rounded-xl shadow-2xl backdrop-blur-md border text-[12px] font-medium flex items-center gap-2 max-w-[420px] cursor-pointer transition-all ${

                toast.level === 'error' ? 'bg-[#FF453A]/15 border-[#FF453A]/30 text-[#FF6961]' :

                toast.level === 'success' ? 'bg-[#30D158]/15 border-[#30D158]/30 text-[#30D158]' :

                'bg-[#1C1C1E]/90 border-white/10 text-white/90'

              }`}

              onClick={() => dismiss(toast.id)}

            >

              {toast.level === 'error' && <span className="shrink-0 text-[14px]"></span>}

              {toast.level === 'success' && <span className="shrink-0 text-[14px]"></span>}

              <span className="break-words">{toast.message}</span>

            </div>

          ))}

        </div>

      )}

    </ToastContext.Provider>
  );
}

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<T>(
  command: string,
  args: Record<string, unknown> = {},
  options?: { silent?: boolean; label?: string }
): Promise<T | undefined> {
  const { invoke } = await import('@tauri-apps/api/core');
  try {
    return await invoke<T>(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<ErrorBoundaryProps, ErrorBoundaryState> {
  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 || (
        <div className="w-screen h-screen bg-[#0A0A0B] flex items-center justify-center">

          <div className="text-center max-w-md">

            <div className="text-[#FF453A] text-4xl mb-4"></div>

            <h1 className="text-white text-xl font-semibold mb-2">Something went wrong</h1>

            <p className="text-white/50 text-sm mb-4">{this.state.error?.message || 'An unexpected error occurred'}</p>

            <button onClick={() => { this.setState({ hasError: false, error: null }); }} className="px-4 py-2 bg-white/10 hover:bg-white/15 border border-white/10 rounded-lg text-white text-sm font-medium transition-colors">Try Again</button>

            <button onClick={() => window.location.reload()} className="ml-3 px-4 py-2 bg-[#0A84FF]/20 hover:bg-[#0A84FF]/30 border border-[#0A84FF]/30 rounded-lg text-[#0A84FF] text-sm font-medium transition-colors">Reload App</button>

          </div>

        </div>
      );
    }
    return this.props.children;
  }
}