import { useState, useEffect } from 'react'; import { LineChart, Line, ScatterChart, Scatter, XAxis, YAxis, CartesianGrid, Tooltip, Legend, ResponsiveContainer, Cell } from 'recharts'; import { useWebSocket } from '../hooks/useWebSocket'; import { Card } from '../components/Card'; import { MetricCard } from '../components/MetricCard'; import { StudyCard } from '../components/StudyCard'; import { OptimizerPanel } from '../components/OptimizerPanel'; import { ParetoPlot } from '../components/ParetoPlot'; import { ParallelCoordinatesPlot } from '../components/ParallelCoordinatesPlot'; import type { Study, Trial, ConvergenceDataPoint, ParameterSpaceDataPoint } from '../types'; interface DashboardProps { studies: Study[]; selectedStudyId: string | null; onStudySelect: (studyId: string) => void; } export default function Dashboard({ studies, selectedStudyId, onStudySelect }: DashboardProps) { const [trials, setTrials] = useState([]); const [allTrials, setAllTrials] = useState([]); const [bestValue, setBestValue] = useState(Infinity); const [prunedCount, setPrunedCount] = useState(0); const [alerts, setAlerts] = useState>([]); const [alertIdCounter, setAlertIdCounter] = useState(0); // Protocol 13: New state for metadata and Pareto front const [studyMetadata, setStudyMetadata] = useState(null); const [paretoFront, setParetoFront] = useState([]); const showAlert = (type: 'success' | 'warning', message: string) => { const id = alertIdCounter; setAlertIdCounter(prev => prev + 1); setAlerts(prev => [...prev, { id, type, message }]); setTimeout(() => { setAlerts(prev => prev.filter(a => a.id !== id)); }, 5000); }; // WebSocket connection const { isConnected } = useWebSocket({ studyId: selectedStudyId, onTrialCompleted: (trial) => { setTrials(prev => [trial, ...prev].slice(0, 20)); setAllTrials(prev => [...prev, trial]); if (trial.objective < bestValue) { setBestValue(trial.objective); showAlert('success', `New best: ${trial.objective.toFixed(4)} (Trial #${trial.trial_number})`); } }, onNewBest: (trial) => { console.log('New best trial:', trial); }, onTrialPruned: (pruned) => { setPrunedCount(prev => prev + 1); showAlert('warning', `Trial #${pruned.trial_number} pruned: ${pruned.pruning_cause}`); }, }); // Load initial trial history when study changes useEffect(() => { if (selectedStudyId) { setTrials([]); setAllTrials([]); setBestValue(Infinity); setPrunedCount(0); // Fetch full history fetch(`/api/optimization/studies/${selectedStudyId}/history`) .then(res => res.json()) .then(data => { const sortedTrials = data.trials.sort((a: Trial, b: Trial) => a.trial_number - b.trial_number); setAllTrials(sortedTrials); setTrials(sortedTrials.slice(-20).reverse()); if (sortedTrials.length > 0) { const minObj = Math.min(...sortedTrials.map((t: Trial) => t.objective)); setBestValue(minObj); } }) .catch(err => console.error('Failed to load history:', err)); // Fetch pruning count fetch(`/api/optimization/studies/${selectedStudyId}/pruning`) .then(res => res.json()) .then(data => { setPrunedCount(data.pruned_trials?.length || 0); }) .catch(err => console.error('Failed to load pruning data:', err)); // Protocol 13: Fetch metadata fetch(`/api/optimization/studies/${selectedStudyId}/metadata`) .then(res => res.json()) .then(data => { setStudyMetadata(data); }) .catch(err => console.error('Failed to load metadata:', err)); // Protocol 13: Fetch Pareto front fetch(`/api/optimization/studies/${selectedStudyId}/pareto-front`) .then(res => res.json()) .then(data => { if (data.is_multi_objective) { setParetoFront(data.pareto_front); } else { setParetoFront([]); } }) .catch(err => console.error('Failed to load Pareto front:', err)); } }, [selectedStudyId]); // Prepare chart data const convergenceData: ConvergenceDataPoint[] = allTrials.map((trial, idx) => ({ trial_number: trial.trial_number, objective: trial.objective, best_so_far: Math.min(...allTrials.slice(0, idx + 1).map(t => t.objective)), })); const parameterSpaceData: ParameterSpaceDataPoint[] = allTrials.map(trial => { const params = Object.values(trial.design_variables); return { trial_number: trial.trial_number, x: params[0] || 0, y: params[1] || 0, objective: trial.objective, isBest: trial.objective === bestValue, }; }); // Calculate average objective const avgObjective = allTrials.length > 0 ? allTrials.reduce((sum, t) => sum + t.objective, 0) / allTrials.length : 0; // Get parameter names const paramNames = allTrials.length > 0 ? Object.keys(allTrials[0].design_variables) : []; // Helper: Format parameter label with unit from metadata const getParamLabel = (paramName: string, index: number): string => { if (!studyMetadata?.design_variables) { return paramName || `Parameter ${index + 1}`; } const dv = studyMetadata.design_variables.find((v: any) => v.name === paramName); if (dv && dv.unit) { return `${paramName} (${dv.unit})`; } return paramName || `Parameter ${index + 1}`; }; // Export functions const exportJSON = () => { if (allTrials.length === 0) return; const data = JSON.stringify(allTrials, null, 2); const blob = new Blob([data], { type: 'application/json' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = `${selectedStudyId}_trials.json`; a.click(); URL.revokeObjectURL(url); showAlert('success', 'JSON exported successfully!'); }; const exportCSV = () => { if (allTrials.length === 0) return; const headers = ['trial_number', 'objective', ...paramNames].join(','); const rows = allTrials.map(t => [ t.trial_number, t.objective, ...paramNames.map(k => t.design_variables[k]) ].join(',')); const csv = [headers, ...rows].join('\n'); const blob = new Blob([csv], { type: 'text/csv' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = `${selectedStudyId}_trials.csv`; a.click(); URL.revokeObjectURL(url); showAlert('success', 'CSV exported successfully!'); }; return (
{/* Alerts */}
{alerts.map(alert => (
{alert.message}
))}
{/* Header */}

Atomizer Dashboard

Real-time optimization monitoring

{/* Sidebar - Study List */} {/* Main Content */}
{/* Metrics Grid */}
0 ? avgObjective.toFixed(4) : '-'} valueColor="text-blue-400" />
0 ? 'text-red-400' : 'text-green-400'} />
{/* Protocol 13: Intelligent Optimizer & Pareto Front */} {selectedStudyId && (
{paretoFront.length > 0 && studyMetadata && ( )}
)} {/* Parallel Coordinates (full width for multi-objective) */} {paretoFront.length > 0 && studyMetadata && (
)} {/* Charts */}
{/* Convergence Chart */} {convergenceData.length > 0 ? ( ) : (
No trial data yet
)}
{/* Parameter Space Chart */} {parameterSpaceData.length > 0 ? ( { if (name === 'objective') return [value.toFixed(4), 'Objective']; return [value.toFixed(3), name]; }} /> {parameterSpaceData.map((entry, index) => ( ))} ) : (
No trial data yet
)}
{/* Trial Feed */}
{trials.length > 0 ? ( trials.map(trial => (
Trial #{trial.trial_number} {trial.objective.toFixed(4)}
{Object.entries(trial.design_variables).map(([key, val]) => ( {key}: {val.toFixed(3)} ))}
)) ) : (
No trials yet. Waiting for optimization to start...
)}
); }