import { useState, useEffect, useRef } from 'react'; import { useNavigate } from 'react-router-dom'; import { Settings } from 'lucide-react'; import { useOptimizationWebSocket } from '../hooks/useWebSocket'; import { useNotifications, formatOptimizationNotification } from '../hooks/useNotifications'; import { apiClient } from '../api/client'; import { useStudy } from '../context/StudyContext'; import { Card } from '../components/common/Card'; import { ControlPanel } from '../components/dashboard/ControlPanel'; import { NotificationSettings } from '../components/NotificationSettings'; import { ConfigEditor } from '../components/ConfigEditor'; 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 { ZernikeButton } from '../components/ZernikeViewer'; import { ExpandableChart } from '../components/ExpandableChart'; import { CurrentTrialPanel, OptimizerStatePanel } from '../components/tracker'; import { NivoParallelCoordinates } from '../components/charts'; import type { Trial } from '../types'; export default function Dashboard() { const navigate = useNavigate(); const { selectedStudy, refreshStudies, isInitialized } = useStudy(); const selectedStudyId = selectedStudy?.id || null; // All hooks must be declared before any conditional returns 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'); const [trialsPage, setTrialsPage] = useState(0); const trialsPerPage = 50; // Limit trials per page for performance const [showOnlyFEA, setShowOnlyFEA] = useState(false); // Filter to show only trials with OP2 results const [zernikeAvailableTrials, setZernikeAvailableTrials] = useState>(new Set()); // Trials with OP2 files // Parameter Space axis selection (reserved for future use) 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: 'nivo' (dark theme, default) or 'recharts' (simple) const [chartLibrary, setChartLibrary] = useState<'nivo' | 'recharts'>('nivo'); // Process status for tracker panels const [isRunning, setIsRunning] = useState(false); const [lastTrialTime, _setLastTrialTime] = useState(undefined); // Config editor modal const [showConfigEditor, setShowConfigEditor] = useState(false); // Desktop notifications const { showNotification } = useNotifications(); const previousBestRef = useRef(Infinity); // Check if there's a pending study in localStorage (user just navigated here) const pendingStudyId = localStorage.getItem('selectedStudyId'); const isWaitingForStudy = isInitialized && !selectedStudy && !!pendingStudyId; 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; // Avoid duplicates by checking if trial already exists setAllTrials(prev => { const exists = prev.some(t => t.trial_number === trial.trial_number); if (exists) return prev; return [...prev, trial]; }); if (trial.objective !== null && trial.objective !== undefined && trial.objective < bestValue) { const improvement = previousBestRef.current !== Infinity ? ((previousBestRef.current - trial.objective) / Math.abs(previousBestRef.current)) * 100 : 0; setBestValue(trial.objective); previousBestRef.current = trial.objective; showAlert('success', `New best: ${trial.objective.toFixed(4)} (Trial #${trial.trial_number})`); // Desktop notification for new best showNotification(formatOptimizationNotification({ type: 'new_best', studyName: selectedStudy?.name || selectedStudyId || 'Study', message: `Best value: ${trial.objective.toExponential(4)}`, value: trial.objective, improvement })); } } 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 // PERFORMANCE: Use limit to avoid loading thousands of trials at once const MAX_TRIALS_LOAD = 300; useEffect(() => { if (selectedStudyId) { setAllTrials([]); setBestValue(Infinity); setPrunedCount(0); setExpandedTrials(new Set()); // Single history fetch with limit - used for both trial list and charts // This replaces the duplicate fetch calls fetch(`/api/optimization/studies/${selectedStudyId}/history?limit=${MAX_TRIALS_LOAD}`) .then(res => res.json()) .then(data => { // Set trials for the trial list const validTrials = data.trials.filter((t: any) => t.objective !== null && t.objective !== undefined); setAllTrials(validTrials); if (validTrials.length > 0) { const minObj = Math.min(...validTrials.map((t: any) => t.objective)); setBestValue(minObj); } // Transform for charts (parallel coordinates, etc.) const trialsData = data.trials.map((t: any) => { 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' }; }); setAllTrialsRaw(trialsData); }) .catch(console.error); apiClient.getStudyPruning(selectedStudyId) .then(data => { setPrunedCount(data.count ?? data.pruned_trials?.length ?? 0); }) .catch(console.error); // Fetch metadata (small payload) fetch(`/api/optimization/studies/${selectedStudyId}/metadata`) .then(res => res.json()) .then(data => setStudyMetadata(data)) .catch(err => console.error('Failed to load metadata:', err)); // Fetch Pareto front (usually small) 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)); // Check process status apiClient.getProcessStatus(selectedStudyId) .then(data => { setIsRunning(data.is_running); }) .catch(err => console.error('Failed to load process status:', err)); // Fetch available Zernike trials (for mirror/zernike studies) if (selectedStudyId.includes('mirror') || selectedStudyId.includes('zernike')) { fetch(`/api/optimization/studies/${selectedStudyId}/zernike-available`) .then(res => res.json()) .then(data => { setZernikeAvailableTrials(new Set(data.available_trials || [])); }) .catch(err => console.error('Failed to load Zernike available trials:', err)); } else { setZernikeAvailableTrials(new Set()); } } }, [selectedStudyId]); // Poll process status periodically useEffect(() => { if (!selectedStudyId) return; const pollStatus = setInterval(() => { apiClient.getProcessStatus(selectedStudyId) .then(data => { setIsRunning(data.is_running); }) .catch(() => {}); }, 5000); return () => clearInterval(pollStatus); }, [selectedStudyId]); // Sort and filter trials based on selected options useEffect(() => { let filtered = [...allTrials]; // Filter to trials with OP2 results (for Zernike analysis) if (showOnlyFEA && zernikeAvailableTrials.size > 0) { filtered = filtered.filter(t => zernikeAvailableTrials.has(t.trial_number)); } // Sort if (sortBy === 'performance') { // Sort by objective (best first) filtered.sort((a, b) => { const aObj = a.objective ?? Infinity; const bObj = b.objective ?? Infinity; return aObj - bObj; }); } else { // Chronological (newest first) filtered.sort((a, b) => b.trial_number - a.trial_number); } setDisplayedTrials(filtered); setTrialsPage(0); // Reset pagination when filter changes }, [allTrials, sortBy, showOnlyFEA, zernikeAvailableTrials]); // Auto-refresh polling for trial history // PERFORMANCE: Use limit and longer interval for large studies useEffect(() => { if (!selectedStudyId) return; const refreshInterval = setInterval(() => { // Only fetch latest trials, not the entire history fetch(`/api/optimization/studies/${selectedStudyId}/history?limit=${MAX_TRIALS_LOAD}`) .then(res => res.json()) .then(data => { const validTrials = data.trials.filter((t: any) => t.objective !== null && t.objective !== undefined); setAllTrials(validTrials); if (validTrials.length > 0) { const minObj = Math.min(...validTrials.map((t: any) => t.objective)); setBestValue(minObj); } // Also update chart data const trialsData = data.trials.map((t: any) => { 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' }; }); setAllTrialsRaw(trialsData); }) .catch(err => console.error('Auto-refresh failed:', err)); }, 15000); // Poll every 15 seconds for performance return () => clearInterval(refreshInterval); }, [selectedStudyId]); // Show loading state while initializing or waiting for study context to sync if (!isInitialized || isWaitingForStudy) { return (

Loading study...

); } // If no study selected and no pending study, show message with link to home if (!selectedStudy) { return (

No study selected

); } // Note: Chart data sampling is handled by individual chart components // 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

{/* Config Editor Button */} {selectedStudyId && ( )} {/* Notification Toggle */} {selectedStudyId && ( )} {/* Chart library toggle */}
{/* Control Panel - Full Width on Top */}
{/* Tracker Panels - Current Trial and Optimizer State */}
t.source === 'FEA').length} nnTrials={allTrialsRaw.filter(t => t.source === 'NN').length} objectives={studyMetadata?.objectives || []} isMultiObjective={(studyMetadata?.objectives?.length || 0) > 1} paretoSize={paretoFront.length} />
{/* Main Layout: Charts (Claude Terminal is now global/floating) */}
{/* Main Content - Charts stacked vertically */}
{/* Study Name Header + Metrics in one row */}
{selectedStudyId && ( <>

{selectedStudyId}

{studyMetadata?.description && (

{studyMetadata.description}

)} )}
{/* Compact Metrics */}
{allTrials.length}
Trials
{bestValue === Infinity ? '-' : bestValue.toFixed(4)}
Best
{avgObjective > 0 ? avgObjective.toFixed(4) : '-'}
Avg
0 ? 'text-red-400' : 'text-green-400'}`}> {prunedCount}
Pruned
{/* Pareto Front - Full Width */} {selectedStudyId && paretoFront.length > 0 && studyMetadata && studyMetadata.objectives && (
)} {/* Parallel Coordinates - Full Width */} {allTrialsRaw.length > 0 && studyMetadata && studyMetadata.objectives && studyMetadata.design_variables && (
{chartLibrary === 'nivo' ? ( ) : ( )}
)} {/* Convergence Plot - Full Width */} {allTrialsRaw.length > 0 && (
)} {/* Parameter Importance - Full Width */} {allTrialsRaw.length > 0 && (studyMetadata?.design_variables?.length > 0 || (allTrialsRaw[0]?.params && Object.keys(allTrialsRaw[0].params).length > 0)) && (
0 ? studyMetadata.design_variables : Object.keys(allTrialsRaw[0]?.params || {}).map(name => ({ name })) } objectiveIndex={0} objectiveName={studyMetadata?.objectives?.[0]?.name || 'Objective'} />
)} {/* Trial History with Sort Controls and Pagination */} Trial History ({displayedTrials.length}{showOnlyFEA ? ' with OP2' : ''} trials{showOnlyFEA && allTrials.length !== displayedTrials.length ? ` of ${allTrials.length}` : ''})
{/* OP2/Zernike toggle - only show for mirror/zernike studies */} {selectedStudyId && (selectedStudyId.includes('mirror') || selectedStudyId.includes('zernike')) && zernikeAvailableTrials.size > 0 && ( )} {/* Pagination controls */} {displayedTrials.length > trialsPerPage && (
{trialsPage + 1}/{Math.ceil(displayedTrials.length / trialsPerPage)}
)}
} >
{displayedTrials.length > 0 ? ( displayedTrials.slice(trialsPage * trialsPerPage, (trialsPage + 1) * trialsPerPage).map((trial, idx) => { 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'} {/* Zernike viewer button - only show for mirror/Zernike studies */} {selectedStudyId && (selectedStudyId.includes('mirror') || selectedStudyId.includes('zernike')) && ( )} {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
)} {/* Zernike Analysis Button - Full size in expanded view */} {selectedStudyId && (selectedStudyId.includes('mirror') || selectedStudyId.includes('zernike')) && (
)}
)}
); }) ) : (
No trials yet. Waiting for optimization to start...
)}
{/* Console Output - at the bottom */}
{/* Config Editor Modal */} {showConfigEditor && selectedStudyId && ( setShowConfigEditor(false)} onSaved={() => refreshStudies()} /> )} ); }