import { useState, useEffect } from 'react'; import { LineChart, Line, ScatterChart, Scatter, XAxis, YAxis, CartesianGrid, Tooltip, Legend, ResponsiveContainer, Cell } from 'recharts'; import { useOptimizationWebSocket } from '../hooks/useWebSocket'; import { apiClient } from '../api/client'; import { Card } from '../components/common/Card'; import { MetricCard } from '../components/dashboard/MetricCard'; import { StudyCard } from '../components/dashboard/StudyCard'; import { OptimizerPanel } from '../components/OptimizerPanel'; import { ParetoPlot } from '../components/ParetoPlot'; import { ParallelCoordinatesPlot } from '../components/ParallelCoordinatesPlot'; import type { Study, Trial, ConvergenceDataPoint, ParameterSpaceDataPoint } from '../types'; export default function Dashboard() { const [studies, setStudies] = useState([]); const [selectedStudyId, setSelectedStudyId] = useState(null); const [allTrials, setAllTrials] = useState([]); const [displayedTrials, setDisplayedTrials] = useState([]); const [bestValue, setBestValue] = useState(Infinity); const [prunedCount, setPrunedCount] = useState(0); const [alerts, setAlerts] = useState>([]); const [alertIdCounter, setAlertIdCounter] = useState(0); const [expandedTrials, setExpandedTrials] = useState>(new Set()); const [sortBy, setSortBy] = useState<'performance' | 'chronological'>('performance'); // Protocol 13: New state for metadata and Pareto front const [studyMetadata, setStudyMetadata] = useState(null); const [paretoFront, setParetoFront] = useState([]); // Load studies on mount useEffect(() => { apiClient.getStudies() .then(data => { setStudies(data.studies); if (data.studies.length > 0) { // Check LocalStorage for last selected study const savedStudyId = localStorage.getItem('lastSelectedStudyId'); const studyExists = data.studies.find(s => s.id === savedStudyId); if (savedStudyId && studyExists) { setSelectedStudyId(savedStudyId); } else { const running = data.studies.find(s => s.status === 'running'); setSelectedStudyId(running?.id || data.studies[0].id); } } }) .catch(console.error); }, []); 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 { connectionStatus } = useOptimizationWebSocket({ studyId: selectedStudyId, onMessage: (msg) => { if (msg.type === 'trial_completed') { const trial = msg.data as Trial; setAllTrials(prev => [...prev, trial]); if (trial.objective !== null && trial.objective !== undefined && trial.objective < bestValue) { setBestValue(trial.objective); showAlert('success', `New best: ${trial.objective.toFixed(4)} (Trial #${trial.trial_number})`); } } else if (msg.type === 'trial_pruned') { setPrunedCount(prev => prev + 1); showAlert('warning', `Trial pruned: ${msg.data.pruning_cause}`); } } }); // Load initial trial history when study changes useEffect(() => { if (selectedStudyId) { setAllTrials([]); setBestValue(Infinity); setPrunedCount(0); setExpandedTrials(new Set()); // Save to LocalStorage localStorage.setItem('lastSelectedStudyId', selectedStudyId); apiClient.getStudyHistory(selectedStudyId) .then(data => { const validTrials = data.trials.filter(t => t.objective !== null && t.objective !== undefined); setAllTrials(validTrials); if (validTrials.length > 0) { const minObj = Math.min(...validTrials.map(t => t.objective)); setBestValue(minObj); } }) .catch(console.error); apiClient.getStudyPruning(selectedStudyId) .then(data => { setPrunedCount(data.pruned_trials?.length || 0); }) .catch(console.error); // 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 (raw format for Protocol 13 components) fetch(`/api/optimization/studies/${selectedStudyId}/pareto-front`) .then(res => res.json()) .then(paretoData => { if (paretoData.is_multi_objective && paretoData.pareto_front) { setParetoFront(paretoData.pareto_front); } else { setParetoFront([]); } }) .catch(err => console.error('Failed to load Pareto front:', err)); } }, [selectedStudyId]); // Sort trials based on selected sort order useEffect(() => { let sorted = [...allTrials]; if (sortBy === 'performance') { // Sort by objective (best first) sorted.sort((a, b) => { const aObj = a.objective ?? Infinity; const bObj = b.objective ?? Infinity; return aObj - bObj; }); } else { // Chronological (newest first) sorted.sort((a, b) => b.trial_number - a.trial_number); } setDisplayedTrials(sorted); }, [allTrials, sortBy]); // Auto-refresh polling (every 3 seconds) for trial history useEffect(() => { if (!selectedStudyId) return; const refreshInterval = setInterval(() => { apiClient.getStudyHistory(selectedStudyId) .then(data => { const validTrials = data.trials.filter(t => t.objective !== null && t.objective !== undefined); setAllTrials(validTrials); if (validTrials.length > 0) { const minObj = Math.min(...validTrials.map(t => t.objective)); setBestValue(minObj); } }) .catch(err => console.error('Auto-refresh failed:', err)); }, 3000); // Poll every 3 seconds return () => clearInterval(refreshInterval); }, [selectedStudyId]); // Prepare chart data with proper null/undefined handling const convergenceData: ConvergenceDataPoint[] = allTrials .filter(t => t.objective !== null && t.objective !== undefined) .sort((a, b) => a.trial_number - b.trial_number) .map((trial, idx, arr) => { const previousTrials = arr.slice(0, idx + 1); const validObjectives = previousTrials.map(t => t.objective).filter(o => o !== null && o !== undefined); return { trial_number: trial.trial_number, objective: trial.objective, best_so_far: validObjectives.length > 0 ? Math.min(...validObjectives) : trial.objective, }; }); const parameterSpaceData: ParameterSpaceDataPoint[] = allTrials .filter(t => t.objective !== null && t.objective !== undefined && t.design_variables) .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 validObjectives = allTrials.filter(t => t.objective !== null && t.objective !== undefined).map(t => t.objective); const avgObjective = validObjectives.length > 0 ? validObjectives.reduce((sum, obj) => sum + obj, 0) / validObjectives.length : 0; // Get parameter names const paramNames = allTrials.length > 0 && allTrials[0].design_variables ? Object.keys(allTrials[0].design_variables) : []; // Toggle trial expansion const toggleTrialExpansion = (trialNumber: number) => { setExpandedTrials(prev => { const newSet = new Set(prev); if (newSet.has(trialNumber)) { newSet.delete(trialNumber); } else { newSet.add(trialNumber); } return newSet; }); }; // 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 */}

Live 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 && studyMetadata.objectives && (
Algorithm: {studyMetadata.sampler || 'NSGA-II'}
Type: Multi-objective
Objectives: {studyMetadata.objectives?.length || 2}
Design Variables: {studyMetadata.design_variables?.length || 0}
)} {/* Parallel Coordinates (full width for multi-objective) */} {paretoFront.length > 0 && studyMetadata && studyMetadata.objectives && studyMetadata.design_variables && (
)} {/* 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 History with Sort Controls */} Trial History ({displayedTrials.length} trials)
} >
{displayedTrials.length > 0 ? ( displayedTrials.map(trial => { const isExpanded = expandedTrials.has(trial.trial_number); const isBest = trial.objective === bestValue; return (
toggleTrialExpansion(trial.trial_number)} > {/* Collapsed View */}
Trial #{trial.trial_number} {isBest && BEST}
{trial.objective !== null && trial.objective !== undefined ? trial.objective.toFixed(4) : 'N/A'} {isExpanded ? '▼' : '▶'}
{/* Quick Preview */} {!isExpanded && trial.results && Object.keys(trial.results).length > 0 && (
{trial.results.mass && ( Mass: {trial.results.mass.toFixed(2)}g )} {trial.results.frequency && ( Freq: {trial.results.frequency.toFixed(2)}Hz )}
)}
{/* Expanded View */} {isExpanded && (
{/* Design Variables */} {trial.design_variables && Object.keys(trial.design_variables).length > 0 && (

Design Variables

{Object.entries(trial.design_variables).map(([key, val]) => (
{key}: {val.toFixed(4)}
))}
)} {/* Results */} {trial.results && Object.keys(trial.results).length > 0 && (

Extracted Results

{Object.entries(trial.results).map(([key, val]) => (
{key}: {typeof val === 'number' ? val.toFixed(4) : String(val)}
))}
)} {/* All User Attributes */} {trial.user_attrs && Object.keys(trial.user_attrs).length > 0 && (

All Attributes

                                  {JSON.stringify(trial.user_attrs, null, 2)}
                                
)} {/* Timestamps */} {trial.start_time && trial.end_time && (
Duration: {((new Date(trial.end_time).getTime() - new Date(trial.start_time).getTime()) / 1000).toFixed(1)}s
)}
)}
); }) ) : (
No trials yet. Waiting for optimization to start...
)}
); }