Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion frontend/src/App.tsx
Original file line number Diff line number Diff line change
@@ -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'
Expand All @@ -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 (
Expand Down Expand Up @@ -40,7 +42,9 @@ const App: React.FC = () => {
return (
<ErrorBoundary>
<WalletProvider>
<AppContent />
<ToastProvider>
<AppContent />
</ToastProvider>
</WalletProvider>
</ErrorBoundary>
)
Expand Down
42 changes: 4 additions & 38 deletions frontend/src/components/agents/TaskSubmissionForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 = [
Expand Down Expand Up @@ -33,9 +34,9 @@ type TaskFormValues = z.infer<typeof taskSchema>;

export function TaskSubmissionForm() {
const navigate = useNavigate();
const [toast, setToast] = useState<string | null>(null);
const [preview, setPreview] = useState<TaskSubmitResponse['dagPreview'] | null>(null);
const { submitTask, status, error, data } = useTaskSubmit();
const { showToast } = useToast();

const {
register,
Expand All @@ -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');
}
};

Expand Down Expand Up @@ -207,41 +207,7 @@ export function TaskSubmissionForm() {
{!isLoading && <DAGPreview dagPreview={previewData ?? undefined} />}
</section>

{toast && (
<div
role="alert"
aria-live="assertive"
style={{
position: 'fixed',
right: 24,
bottom: 24,
maxWidth: 360,
padding: 16,
background: '#fef2f2',
border: '1px solid #fca5a5',
borderRadius: 12,
boxShadow: '0 12px 40px rgba(15, 23, 42, 0.15)',
}}
>
<div style={{ color: '#991b1b', marginBottom: 12 }}>{toast}</div>
<button
type="button"
onClick={() => setToast(null)}
style={{
padding: '8px 14px',
borderRadius: 8,
border: 'none',
background: '#ef4444',
color: '#ffffff',
cursor: 'pointer',
}}
>
Dismiss
</button>
</div>
)}

{error && !toast && (
{error && (
<div
role="status"
style={{
Expand Down
100 changes: 100 additions & 0 deletions frontend/src/components/common/Toast.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
.toast-container {
position: fixed;
top: 20px;
right: 20px;
z-index: 10000;
display: flex;
flex-direction: column;
gap: 8px;
max-width: 400px;
pointer-events: none;
}

.toast {
display: flex;
align-items: flex-start;
gap: 12px;
padding: 14px 18px;
border-radius: 10px;
box-shadow: 0 8px 32px rgba(15, 23, 42, 0.18);
font-size: 14px;
line-height: 1.5;
pointer-events: auto;
animation: toast-slide-in 0.3s ease-out;
word-break: break-word;
}

.toast button {
flex-shrink: 0;
background: none;
border: none;
font-size: 18px;
cursor: pointer;
padding: 0 2px;
line-height: 1;
opacity: 0.7;
transition: opacity 0.15s;
}

.toast button:hover {
opacity: 1;
}

.toast-success {
background: #f0fdf4;
border: 1px solid #86efac;
color: #166534;
}

.toast-success button {
color: #166534;
}

.toast-error {
background: #fef2f2;
border: 1px solid #fca5a5;
color: #991b1b;
}

.toast-error button {
color: #991b1b;
}

.toast-warning {
background: #fffbeb;
border: 1px solid #fcd34d;
color: #92400e;
}

.toast-warning button {
color: #92400e;
}

.toast-info {
background: #eff6ff;
border: 1px solid #93c5fd;
color: #1e40af;
}

.toast-info button {
color: #1e40af;
}

@keyframes toast-slide-in {
from {
transform: translateX(100%);
opacity: 0;
}
to {
transform: translateX(0);
opacity: 1;
}
}

@media (max-width: 480px) {
.toast-container {
left: 12px;
right: 12px;
max-width: none;
}
}
6 changes: 4 additions & 2 deletions frontend/src/components/dashboard/RecentTasksTable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import React from 'react';
import { Skeleton } from '../common/Skeleton';
import styles from './RecentTasksTable.module.css';
import { getRecentTasks } from '@services/api';
import { useToast } from '../../context/ToastContext';
import type { TaskResponse } from '../../types/api';

interface Props {
Expand All @@ -12,6 +13,7 @@ interface Props {

export const RecentTasksTable: React.FC<Props> = ({ walletAddress, loading }) => {
const [tasks, setTasks] = React.useState<TaskResponse[]>([]);
const { showToast } = useToast();

React.useEffect(() => {
if (!walletAddress) return;
Expand All @@ -24,12 +26,12 @@ export const RecentTasksTable: React.FC<Props> = ({ walletAddress, loading }) =>
}));
setTasks(mappedTasks);
} catch (e) {
console.error(e);
showToast('Failed to fetch recent tasks', 'error');
setTasks([]);
}
};
fetchTasks();
}, [walletAddress]);
}, [walletAddress, showToast]);

if (loading) {
return (
Expand Down
61 changes: 61 additions & 0 deletions frontend/src/context/ToastContext.tsx
Original file line number Diff line number Diff line change
@@ -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<ToastContextValue | null>(null);

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

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 (
<ToastContext.Provider value={{ toasts, showToast, dismissToast }}>
{children}
<ToastContainer toasts={toasts} onDismiss={dismissToast} />
</ToastContext.Provider>
);
}

export function useToast() {
const context = useContext(ToastContext);
if (!context) throw new Error('useToast must be used within ToastProvider');

Check failure on line 46 in frontend/src/context/ToastContext.tsx

View workflow job for this annotation

GitHub Actions / Frontend

src/components/agents/TaskSubmissionForm.test.tsx > TaskSubmissionForm > shows and dismisses a toast when the network request fails

Error: useToast must be used within ToastProvider ❯ Module.useToast src/context/ToastContext.tsx:46:23 ❯ TaskSubmissionForm src/components/agents/TaskSubmissionForm.tsx:39:25 ❯ renderWithHooks node_modules/react-dom/cjs/react-dom.development.js:15486:18 ❯ mountIndeterminateComponent node_modules/react-dom/cjs/react-dom.development.js:20103:13 ❯ beginWork node_modules/react-dom/cjs/react-dom.development.js:21626:16 ❯ beginWork$1 node_modules/react-dom/cjs/react-dom.development.js:27465:14 ❯ performUnitOfWork node_modules/react-dom/cjs/react-dom.development.js:26599:12 ❯ workLoopSync node_modules/react-dom/cjs/react-dom.development.js:26505:5 ❯ renderRootSync node_modules/react-dom/cjs/react-dom.development.js:26473:7 ❯ recoverFromConcurrentError node_modules/react-dom/cjs/react-dom.development.js:25889:20

Check failure on line 46 in frontend/src/context/ToastContext.tsx

View workflow job for this annotation

GitHub Actions / Frontend

src/components/agents/TaskSubmissionForm.test.tsx > TaskSubmissionForm > redirects to the task detail route within 500ms of a successful API response

Error: useToast must be used within ToastProvider ❯ Module.useToast src/context/ToastContext.tsx:46:23 ❯ TaskSubmissionForm src/components/agents/TaskSubmissionForm.tsx:39:25 ❯ renderWithHooks node_modules/react-dom/cjs/react-dom.development.js:15486:18 ❯ mountIndeterminateComponent node_modules/react-dom/cjs/react-dom.development.js:20103:13 ❯ beginWork node_modules/react-dom/cjs/react-dom.development.js:21626:16 ❯ beginWork$1 node_modules/react-dom/cjs/react-dom.development.js:27465:14 ❯ performUnitOfWork node_modules/react-dom/cjs/react-dom.development.js:26599:12 ❯ workLoopSync node_modules/react-dom/cjs/react-dom.development.js:26505:5 ❯ renderRootSync node_modules/react-dom/cjs/react-dom.development.js:26473:7 ❯ recoverFromConcurrentError node_modules/react-dom/cjs/react-dom.development.js:25889:20

Check failure on line 46 in frontend/src/context/ToastContext.tsx

View workflow job for this annotation

GitHub Actions / Frontend

src/components/agents/TaskSubmissionForm.test.tsx > TaskSubmissionForm > shows a validation error when maxBudgetXLM is below 0.1

Error: useToast must be used within ToastProvider ❯ Module.useToast src/context/ToastContext.tsx:46:23 ❯ TaskSubmissionForm src/components/agents/TaskSubmissionForm.tsx:39:25 ❯ renderWithHooks node_modules/react-dom/cjs/react-dom.development.js:15486:18 ❯ mountIndeterminateComponent node_modules/react-dom/cjs/react-dom.development.js:20103:13 ❯ beginWork node_modules/react-dom/cjs/react-dom.development.js:21626:16 ❯ beginWork$1 node_modules/react-dom/cjs/react-dom.development.js:27465:14 ❯ performUnitOfWork node_modules/react-dom/cjs/react-dom.development.js:26599:12 ❯ workLoopSync node_modules/react-dom/cjs/react-dom.development.js:26505:5 ❯ renderRootSync node_modules/react-dom/cjs/react-dom.development.js:26473:7 ❯ recoverFromConcurrentError node_modules/react-dom/cjs/react-dom.development.js:25889:20

Check failure on line 46 in frontend/src/context/ToastContext.tsx

View workflow job for this annotation

GitHub Actions / Frontend

src/components/agents/TaskSubmissionForm.test.tsx > TaskSubmissionForm > shows a validation error for empty prompt without making an API call

Error: useToast must be used within ToastProvider ❯ Module.useToast src/context/ToastContext.tsx:46:23 ❯ TaskSubmissionForm src/components/agents/TaskSubmissionForm.tsx:39:25 ❯ renderWithHooks node_modules/react-dom/cjs/react-dom.development.js:15486:18 ❯ mountIndeterminateComponent node_modules/react-dom/cjs/react-dom.development.js:20103:13 ❯ beginWork node_modules/react-dom/cjs/react-dom.development.js:21626:16 ❯ beginWork$1 node_modules/react-dom/cjs/react-dom.development.js:27465:14 ❯ performUnitOfWork node_modules/react-dom/cjs/react-dom.development.js:26599:12 ❯ workLoopSync node_modules/react-dom/cjs/react-dom.development.js:26505:5 ❯ renderRootSync node_modules/react-dom/cjs/react-dom.development.js:26473:7 ❯ recoverFromConcurrentError node_modules/react-dom/cjs/react-dom.development.js:25889:20
return context;
}

function ToastContainer({ toasts, onDismiss }: { toasts: Toast[]; onDismiss: (id: string) => void }) {
return (
<div className="toast-container" role="alert" aria-live="polite">
{toasts.map(toast => (
<div key={toast.id} className={`toast toast-${toast.type}`}>
<span>{toast.message}</span>
<button onClick={() => onDismiss(toast.id)} aria-label="Dismiss">&times;</button>
</div>
))}
</div>
);
}
11 changes: 10 additions & 1 deletion frontend/src/pages/dashboard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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(() => {
Expand Down
Loading