import { useState, useEffect, lazy, Suspense } 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'; // Not used currently
import { ParetoPlot } from '../components/ParetoPlot';
import { ParallelCoordinatesPlot } from '../components/ParallelCoordinatesPlot';
import { ParameterImportanceChart } from '../components/ParameterImportanceChart';
import { ConvergencePlot } from '../components/ConvergencePlot';
import { StudyReportViewer } from '../components/StudyReportViewer';
import { ConsoleOutput } from '../components/ConsoleOutput';
import { ExpandableChart } from '../components/ExpandableChart';
import type { Study, Trial, ConvergenceDataPoint, ParameterSpaceDataPoint } from '../types';
// Lazy load Plotly components for better initial load performance
const PlotlyParallelCoordinates = lazy(() => import('../components/plotly/PlotlyParallelCoordinates').then(m => ({ default: m.PlotlyParallelCoordinates })));
const PlotlyParetoPlot = lazy(() => import('../components/plotly/PlotlyParetoPlot').then(m => ({ default: m.PlotlyParetoPlot })));
const PlotlyConvergencePlot = lazy(() => import('../components/plotly/PlotlyConvergencePlot').then(m => ({ default: m.PlotlyConvergencePlot })));
const PlotlyParameterImportance = lazy(() => import('../components/plotly/PlotlyParameterImportance').then(m => ({ default: m.PlotlyParameterImportance })));
// Loading placeholder for lazy components
const ChartLoading = () => (
);
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');
// Parameter Space axis selection
const [paramXIndex, setParamXIndex] = useState(0);
const [paramYIndex, setParamYIndex] = useState(1);
// Protocol 13: New state for metadata and Pareto front
const [studyMetadata, setStudyMetadata] = useState(null);
const [paretoFront, setParetoFront] = useState([]);
const [allTrialsRaw, setAllTrialsRaw] = useState([]); // All trials for parallel coordinates
// Chart library toggle: 'recharts' (faster) or 'plotly' (more interactive but slower)
const [chartLibrary, setChartLibrary] = useState<'plotly' | 'recharts'>('recharts');
// 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: _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 => {
// Use count if available (new API), fallback to array length (legacy)
setPrunedCount(data.count ?? 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 => {
console.log('[Dashboard] Pareto front data:', paretoData);
if (paretoData.is_multi_objective && paretoData.pareto_front) {
console.log('[Dashboard] Setting Pareto front with', paretoData.pareto_front.length, 'trials');
setParetoFront(paretoData.pareto_front);
} else {
console.log('[Dashboard] No Pareto front or not multi-objective');
setParetoFront([]);
}
})
.catch(err => console.error('Failed to load Pareto front:', err));
// Fetch ALL trials (not just Pareto) for parallel coordinates and charts
fetch(`/api/optimization/studies/${selectedStudyId}/history`)
.then(res => res.json())
.then(data => {
// Transform to match the format expected by charts
// API returns 'objectives' (array) for multi-objective, 'objective' (number) for single
const trialsData = data.trials.map((t: any) => {
// Build values array: use objectives if available, otherwise wrap single objective
let values: number[] = [];
if (t.objectives && Array.isArray(t.objectives)) {
values = t.objectives;
} else if (t.objective !== null && t.objective !== undefined) {
values = [t.objective];
}
return {
trial_number: t.trial_number,
values,
params: t.design_variables || {},
user_attrs: t.user_attrs || {},
constraint_satisfied: t.constraint_satisfied !== false,
source: t.source || t.user_attrs?.source || 'FEA' // FEA vs NN differentiation
};
});
setAllTrialsRaw(trialsData);
})
.catch(err => console.error('Failed to load all trials:', 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[paramXIndex] || 0,
y: params[paramYIndex] || 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
{selectedStudyId && (
)}
{/* Chart library toggle */}
{/* Sidebar - Study List */}
{/* Main Content */}
{/* Study Name Header */}
{selectedStudyId && (
{selectedStudyId}
{studyMetadata?.description && (
{studyMetadata.description}
)}
)}
{/* 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}
{chartLibrary === 'plotly' ? (
}>
) : (
)}
)}
{/* Parallel Coordinates (full width for multi-objective) */}
{allTrialsRaw.length > 0 && studyMetadata && studyMetadata.objectives && studyMetadata.design_variables && (
{chartLibrary === 'plotly' ? (
}>
) : (
)}
)}
{/* Convergence Plot - Full Width */}
{allTrialsRaw.length > 0 && (
{chartLibrary === 'plotly' ? (
}>
) : (
)}
)}
{/* Parameter Importance - Full Width */}
{allTrialsRaw.length > 0 && (studyMetadata?.design_variables?.length > 0 || (allTrialsRaw[0]?.params && Object.keys(allTrialsRaw[0].params).length > 0)) && (
{chartLibrary === 'plotly' ? (
}>
0
? studyMetadata.design_variables
: Object.keys(allTrialsRaw[0]?.params || {}).map(name => ({ name }))
}
objectiveIndex={0}
objectiveName={studyMetadata?.objectives?.[0]?.name || 'Objective'}
height={350}
/>
) : (
0
? studyMetadata.design_variables
: Object.keys(allTrialsRaw[0]?.params || {}).map(name => ({ name }))
}
objectiveIndex={0}
objectiveName={studyMetadata?.objectives?.[0]?.name || 'Objective'}
/>
)}
)}
{/* Charts */}
{/* Convergence Chart */}
{convergenceData.length > 0 ? (
) : (
No trial data yet
)}
{/* Parameter Space Chart with Selectable Axes */}
Parameter Space
{paramNames.length > 2 && (
X:
Y:
)}
}>
{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 - Show ALL metrics */}
{!isExpanded && trial.results && Object.keys(trial.results).length > 0 && (
{Object.entries(trial.results).slice(0, 6).map(([key, val]) => {
// Format value based on type
const formatValue = (v: unknown): string => {
if (typeof v === 'number') {
// Use fewer decimals for quick preview
return Math.abs(v) < 0.01 ? v.toExponential(2) : v.toFixed(2);
}
if (Array.isArray(v)) return `[${v.length}]`;
return String(v);
};
// Format key: snake_case to Title Case, abbreviate long names
const formatKey = (k: string): string => {
const short = k.replace(/_/g, ' ')
.replace(/rel /g, 'Δ')
.replace(/filtered rms/g, 'fRMS')
.replace(/global rms/g, 'gRMS')
.replace(/ vs /g, '/')
.replace(/mfg /g, '')
.replace(/optician workload/g, 'work');
return short.length > 15 ? short.slice(0, 12) + '...' : short;
};
return (
{formatKey(key)}: {formatValue(val)}
);
})}
{Object.keys(trial.results).length > 6 && (
+{Object.keys(trial.results).length - 6} more
)}
)}
{/* 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...
)}
{/* Console Output - at the bottom */}
);
}