diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 0407ff2..2d40122 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -1,6 +1,7 @@ import React from 'react' import { BrowserRouter as Router, Routes, Route } from 'react-router-dom' import { WalletProvider } from './context/WalletContext' +import { ToastProvider } from './context/ToastContext' import { NotFoundPage } from './pages/NotFoundPage'; import AppShell from './components/layout/AppShell' import LandingPage from './pages/LandingPage' @@ -11,6 +12,7 @@ import RendererDemoPage from './pages/RendererDemoPage' import WalletPage from './pages/WalletPage' import DashboardPage from './pages/dashboard' import ErrorBoundary from './components/common/ErrorBoundary' +import './components/common/Toast.css' const AppContent: React.FC = () => { return ( @@ -40,7 +42,9 @@ const App: React.FC = () => { return ( - + + + ) diff --git a/frontend/src/components/agents/TaskSubmissionForm.tsx b/frontend/src/components/agents/TaskSubmissionForm.tsx index a9680b7..63f3960 100644 --- a/frontend/src/components/agents/TaskSubmissionForm.tsx +++ b/frontend/src/components/agents/TaskSubmissionForm.tsx @@ -5,6 +5,7 @@ import { z } from 'zod'; import { useNavigate } from 'react-router-dom'; import { DAGPreview } from './DAGPreview'; import { useTaskSubmit } from '../../hooks/useTaskSubmit'; +import { useToast } from '../../context/ToastContext'; import type { AgentPreference, TaskSubmitResponse } from '../../services/taskService'; const agentPreferences = [ @@ -33,9 +34,9 @@ type TaskFormValues = z.infer; export function TaskSubmissionForm() { const navigate = useNavigate(); - const [toast, setToast] = useState(null); const [preview, setPreview] = useState(null); const { submitTask, status, error, data } = useTaskSubmit(); + const { showToast } = useToast(); const { register, @@ -55,14 +56,13 @@ export function TaskSubmissionForm() { try { const result = await submitTask(values); setPreview(result.dagPreview); - setToast(null); window.setTimeout(() => { navigate(`/tasks/${result.taskId}`); }, 300); } catch (submitError) { const message = submitError instanceof Error ? submitError.message : 'Unable to submit task'; - setToast(message); + showToast(message, 'error'); } }; @@ -207,41 +207,7 @@ export function TaskSubmissionForm() { {!isLoading && } - {toast && ( -
-
{toast}
- -
- )} - - {error && !toast && ( + {error && (
= ({ walletAddress, loading }) => { const [tasks, setTasks] = React.useState([]); + const { showToast } = useToast(); React.useEffect(() => { if (!walletAddress) return; @@ -24,12 +26,12 @@ export const RecentTasksTable: React.FC = ({ walletAddress, loading }) => })); setTasks(mappedTasks); } catch (e) { - console.error(e); + showToast('Failed to fetch recent tasks', 'error'); setTasks([]); } }; fetchTasks(); - }, [walletAddress]); + }, [walletAddress, showToast]); if (loading) { return ( diff --git a/frontend/src/context/ToastContext.tsx b/frontend/src/context/ToastContext.tsx new file mode 100644 index 0000000..1382451 --- /dev/null +++ b/frontend/src/context/ToastContext.tsx @@ -0,0 +1,61 @@ +import { createContext, useContext, useState, useCallback, type ReactNode } from 'react'; + +export type ToastType = 'success' | 'error' | 'warning' | 'info'; + +export interface Toast { + id: string; + message: string; + type: ToastType; + duration?: number; +} + +interface ToastContextValue { + toasts: Toast[]; + showToast: (message: string, type?: ToastType, duration?: number) => void; + dismissToast: (id: string) => void; +} + +const ToastContext = createContext(null); + +export function ToastProvider({ children }: { children: ReactNode }) { + const [toasts, setToasts] = useState([]); + + const dismissToast = useCallback((id: string) => { + setToasts(prev => prev.filter(t => t.id !== id)); + }, []); + + const showToast = useCallback((message: string, type: ToastType = 'info', duration = 5000) => { + const id = Math.random().toString(36).slice(2); + setToasts(prev => [...prev, { id, message, type, duration }]); + + if (duration > 0) { + setTimeout(() => dismissToast(id), duration); + } + }, [dismissToast]); + + return ( + + {children} + + + ); +} + +export function useToast() { + const context = useContext(ToastContext); + if (!context) throw new Error('useToast must be used within ToastProvider'); + return context; +} + +function ToastContainer({ toasts, onDismiss }: { toasts: Toast[]; onDismiss: (id: string) => void }) { + return ( +
+ {toasts.map(toast => ( +
+ {toast.message} + +
+ ))} +
+ ); +} \ No newline at end of file diff --git a/frontend/src/pages/dashboard.tsx b/frontend/src/pages/dashboard.tsx index 1501452..e370cf0 100644 --- a/frontend/src/pages/dashboard.tsx +++ b/frontend/src/pages/dashboard.tsx @@ -2,6 +2,7 @@ import React from 'react'; import { useWallet } from '../hooks/useWallet'; import { useNetworkStats } from '../hooks/useNetworkStats'; +import { useToast } from '../context/ToastContext'; import { DashboardLayout } from '../components/dashboard/DashboardLayout'; import { KpiCard } from '../components/dashboard/KpiCard'; import { NetworkHealthBadge } from '../components/dashboard/NetworkHealthBadge'; @@ -10,7 +11,15 @@ import styles from './dashboard.module.css'; export const DashboardPage: React.FC = () => { const { address } = useWallet(); - const { data, loading } = useNetworkStats(); + const { data, loading, error } = useNetworkStats(); + const { showToast } = useToast(); + + // Show error toast when network stats fetch fails + React.useEffect(() => { + if (error) { + showToast(`Failed to load network stats: ${error}`, 'error'); + } + }, [error, showToast]); // Redirect unauthenticated users React.useEffect(() => {