Files
Atomizer/atomizer-dashboard/frontend/src/pages/Dashboard.tsx
Antoine 48404fd743 feat: Add Zernike wavefront viewer and V14 TPE optimization study
Dashboard Zernike Analysis:
- Add ZernikeViewer component with tabbed UI (40°, 60°, 90° vs 20°)
- Generate 3D surface mesh plots with Mesh3d triangulation
- Full 50-mode Zernike coefficient tables with mode names
- Manufacturing metrics for 90_vs_20 (optician workload analysis)
- OP2 availability filter for FEA trials only
- Fix duplicate trial display with unique React keys
- Tab switching with proper event propagation

Backend API Enhancements:
- GET /studies/{id}/trials/{num}/zernike - Generate Zernike HTML on-demand
- GET /studies/{id}/zernike-available - List trials with OP2 files
- compute_manufacturing_metrics() for aberration analysis
- compute_rms_filter_j1to3() for optician workload metric

M1 Mirror V14 Study:
- TPE (Tree-structured Parzen Estimator) optimization
- Seeds from 496 prior FEA trials (V11+V12+V13)
- Weighted-sum objective: 5*obj_40 + 5*obj_60 + 1*obj_mfg
- Multivariate TPE with constant_liar for efficient exploration
- Ready for 8-hour overnight runs

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-10 21:34:07 -05:00

927 lines
41 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { useState, useEffect, lazy, Suspense, useRef } from 'react';
import { useNavigate } from 'react-router-dom';
import { Terminal, 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 { useClaudeTerminal } from '../context/ClaudeTerminalContext';
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 type { Trial } 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 = () => (
<div className="flex items-center justify-center h-64 text-dark-400">
<div className="animate-pulse">Loading chart...</div>
</div>
);
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<Trial[]>([]);
const [displayedTrials, setDisplayedTrials] = useState<Trial[]>([]);
const [bestValue, setBestValue] = useState<number>(Infinity);
const [prunedCount, setPrunedCount] = useState<number>(0);
const [alerts, setAlerts] = useState<Array<{ id: number; type: 'success' | 'warning'; message: string }>>([]);
const [alertIdCounter, setAlertIdCounter] = useState(0);
const [expandedTrials, setExpandedTrials] = useState<Set<number>>(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<Set<number>>(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<any>(null);
const [paretoFront, setParetoFront] = useState<any[]>([]);
const [allTrialsRaw, setAllTrialsRaw] = useState<any[]>([]); // All trials for parallel coordinates
// Chart library toggle: 'recharts' (faster) or 'plotly' (more interactive but slower)
const [chartLibrary, setChartLibrary] = useState<'plotly' | 'recharts'>('recharts');
// Process status for tracker panels
const [isRunning, setIsRunning] = useState(false);
const [lastTrialTime, _setLastTrialTime] = useState<number | undefined>(undefined);
// Config editor modal
const [showConfigEditor, setShowConfigEditor] = useState(false);
// Claude terminal from global context
const { isOpen: claudeTerminalOpen, setIsOpen: setClaudeTerminalOpen, isConnected: claudeConnected } = useClaudeTerminal();
// Desktop notifications
const { showNotification } = useNotifications();
const previousBestRef = useRef<number>(Infinity);
// Redirect to home if no study selected (but only after initialization completes)
useEffect(() => {
if (isInitialized && !selectedStudy) {
navigate('/');
}
}, [selectedStudy, navigate, isInitialized]);
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 (restoring study from localStorage)
if (!isInitialized) {
return (
<div className="flex items-center justify-center min-h-screen">
<div className="text-center">
<div className="animate-spin w-8 h-8 border-2 border-primary-500 border-t-transparent rounded-full mx-auto mb-4"></div>
<p className="text-dark-400">Loading study...</p>
</div>
</div>
);
}
// 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 (
<div className="w-full max-w-[2400px] mx-auto px-4">
{/* Alerts */}
<div className="fixed top-4 right-4 z-50 space-y-2">
{alerts.map(alert => (
<div
key={alert.id}
className={`px-4 py-3 rounded-lg shadow-lg transition-all duration-300 ${
alert.type === 'success'
? 'bg-green-900 border-l-4 border-green-400 text-green-100'
: 'bg-yellow-900 border-l-4 border-yellow-400 text-yellow-100'
}`}
>
{alert.message}
</div>
))}
</div>
{/* Header */}
<header className="mb-8 flex items-center justify-between border-b border-dark-600 pb-4">
<div>
<h1 className="text-3xl font-bold text-primary-400">Live Dashboard</h1>
<p className="text-dark-300 mt-1">Real-time optimization monitoring</p>
</div>
<div className="flex gap-2">
{/* Config Editor Button */}
{selectedStudyId && (
<button
onClick={() => setShowConfigEditor(true)}
className="flex items-center gap-1.5 px-2 py-1 rounded text-xs bg-dark-700 text-dark-400 hover:bg-dark-600 hover:text-white transition-colors"
title="Edit study configuration"
>
<Settings className="w-4 h-4" />
<span className="hidden sm:inline">Config</span>
</button>
)}
{/* Notification Toggle */}
<NotificationSettings compact />
{/* Claude Code Terminal Toggle Button */}
<button
onClick={() => setClaudeTerminalOpen(!claudeTerminalOpen)}
className={`flex items-center gap-2 px-4 py-2 rounded-lg transition-colors ${
claudeTerminalOpen
? 'bg-primary-600 text-white'
: claudeConnected
? 'bg-green-700 text-white border border-green-600'
: 'bg-dark-700 text-dark-200 hover:bg-dark-600 hover:text-white border border-dark-600'
}`}
title={claudeConnected ? 'Claude Terminal (Connected)' : 'Open Claude Code terminal'}
>
<Terminal className="w-4 h-4" />
<span className="hidden sm:inline">Claude Code</span>
{claudeConnected && !claudeTerminalOpen && (
<span className="w-2 h-2 bg-green-400 rounded-full animate-pulse" />
)}
</button>
{selectedStudyId && (
<StudyReportViewer studyId={selectedStudyId} />
)}
<button
onClick={() => {
// Open Optuna dashboard on port 8081
// Note: The dashboard needs to be started separately with the correct study database
window.open('http://localhost:8081', '_blank');
}}
className="btn-secondary"
title="Open Optuna Dashboard (runs on port 8081)"
>
Optuna Dashboard
</button>
<button onClick={exportJSON} className="btn-secondary" disabled={allTrials.length === 0}>
Export JSON
</button>
<button onClick={exportCSV} className="btn-secondary" disabled={allTrials.length === 0}>
Export CSV
</button>
{/* Chart library toggle */}
<div className="flex rounded-lg overflow-hidden border border-dark-500 ml-2">
<button
onClick={() => setChartLibrary('plotly')}
className={`px-3 py-1.5 text-sm transition-colors ${
chartLibrary === 'plotly'
? 'bg-primary-500 text-white'
: 'bg-dark-600 text-dark-200 hover:bg-dark-500'
}`}
title="Interactive Plotly charts with zoom, pan, and export"
>
Plotly
</button>
<button
onClick={() => setChartLibrary('recharts')}
className={`px-3 py-1.5 text-sm transition-colors ${
chartLibrary === 'recharts'
? 'bg-primary-500 text-white'
: 'bg-dark-600 text-dark-200 hover:bg-dark-500'
}`}
title="Simple Recharts visualization"
>
Simple
</button>
</div>
</div>
</header>
{/* Control Panel - Full Width on Top */}
<div className="mb-6">
<ControlPanel onStatusChange={refreshStudies} horizontal />
</div>
{/* Tracker Panels - Current Trial and Optimizer State */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 mb-6">
<CurrentTrialPanel
studyId={selectedStudyId}
totalTrials={selectedStudy?.progress.total || 100}
completedTrials={allTrials.length}
isRunning={isRunning}
lastTrialTime={lastTrialTime}
/>
<OptimizerStatePanel
sampler={studyMetadata?.sampler}
nTrials={selectedStudy?.progress.total || 100}
completedTrials={allTrials.length}
feaTrials={allTrialsRaw.filter(t => t.source === 'FEA').length}
nnTrials={allTrialsRaw.filter(t => t.source === 'NN').length}
objectives={studyMetadata?.objectives || []}
isMultiObjective={(studyMetadata?.objectives?.length || 0) > 1}
paretoSize={paretoFront.length}
/>
</div>
{/* Main Layout: Charts (Claude Terminal is now global/floating) */}
<div className="grid gap-4 grid-cols-1">
{/* Main Content - Charts stacked vertically */}
<main>
{/* Study Name Header + Metrics in one row */}
<div className="mb-4 pb-3 border-b border-dark-600 flex items-center justify-between">
<div>
{selectedStudyId && (
<>
<h2 className="text-xl font-semibold text-primary-300">
{selectedStudyId}
</h2>
{studyMetadata?.description && (
<p className="text-sm text-dark-400 mt-1">{studyMetadata.description}</p>
)}
</>
)}
</div>
{/* Compact Metrics */}
<div className="flex gap-3">
<div className="text-center px-3">
<div className="text-2xl font-bold text-white">{allTrials.length}</div>
<div className="text-xs text-dark-400">Trials</div>
</div>
<div className="text-center px-3 border-l border-dark-600">
<div className="text-2xl font-bold text-green-400">
{bestValue === Infinity ? '-' : bestValue.toFixed(4)}
</div>
<div className="text-xs text-dark-400">Best</div>
</div>
<div className="text-center px-3 border-l border-dark-600">
<div className="text-2xl font-bold text-blue-400">
{avgObjective > 0 ? avgObjective.toFixed(4) : '-'}
</div>
<div className="text-xs text-dark-400">Avg</div>
</div>
<div className="text-center px-3 border-l border-dark-600">
<div className={`text-2xl font-bold ${prunedCount > 0 ? 'text-red-400' : 'text-green-400'}`}>
{prunedCount}
</div>
<div className="text-xs text-dark-400">Pruned</div>
</div>
</div>
</div>
{/* Pareto Front - Full Width */}
{selectedStudyId && paretoFront.length > 0 && studyMetadata && studyMetadata.objectives && (
<div className="mb-4">
<ExpandableChart
title="Pareto Front"
subtitle={`${paretoFront.length} Pareto-optimal solutions | ${studyMetadata.sampler || 'NSGA-II'} | ${studyMetadata.objectives?.length || 2} objectives`}
>
{chartLibrary === 'plotly' ? (
<Suspense fallback={<ChartLoading />}>
<PlotlyParetoPlot
trials={allTrialsRaw}
paretoFront={paretoFront}
objectives={studyMetadata.objectives}
height={300}
/>
</Suspense>
) : (
<ParetoPlot
paretoData={paretoFront}
objectives={studyMetadata.objectives}
allTrials={allTrialsRaw}
/>
)}
</ExpandableChart>
</div>
)}
{/* Parallel Coordinates - Full Width */}
{allTrialsRaw.length > 0 && studyMetadata && studyMetadata.objectives && studyMetadata.design_variables && (
<div className="mb-4">
<ExpandableChart
title="Parallel Coordinates"
subtitle={`${allTrialsRaw.length} trials - Design Variables → Objectives`}
>
{chartLibrary === 'plotly' ? (
<Suspense fallback={<ChartLoading />}>
<PlotlyParallelCoordinates
trials={allTrialsRaw}
objectives={studyMetadata.objectives}
designVariables={studyMetadata.design_variables}
paretoFront={paretoFront}
height={350}
/>
</Suspense>
) : (
<ParallelCoordinatesPlot
paretoData={allTrialsRaw}
objectives={studyMetadata.objectives}
designVariables={studyMetadata.design_variables}
paretoFront={paretoFront}
/>
)}
</ExpandableChart>
</div>
)}
{/* Convergence Plot - Full Width */}
{allTrialsRaw.length > 0 && (
<div className="mb-4">
<ExpandableChart
title="Convergence"
subtitle={`Best ${studyMetadata?.objectives?.[0]?.name || 'Objective'} over ${allTrialsRaw.length} trials`}
>
{chartLibrary === 'plotly' ? (
<Suspense fallback={<ChartLoading />}>
<PlotlyConvergencePlot
trials={allTrialsRaw}
objectiveIndex={0}
objectiveName={studyMetadata?.objectives?.[0]?.name || 'Objective'}
direction="minimize"
height={280}
/>
</Suspense>
) : (
<ConvergencePlot
trials={allTrialsRaw}
objectiveIndex={0}
objectiveName={studyMetadata?.objectives?.[0]?.name || 'Objective'}
direction="minimize"
/>
)}
</ExpandableChart>
</div>
)}
{/* Parameter Importance - Full Width */}
{allTrialsRaw.length > 0 && (studyMetadata?.design_variables?.length > 0 || (allTrialsRaw[0]?.params && Object.keys(allTrialsRaw[0].params).length > 0)) && (
<div className="mb-4">
<ExpandableChart
title="Parameter Importance"
subtitle={`Correlation with ${studyMetadata?.objectives?.[0]?.name || 'Objective'}`}
>
{chartLibrary === 'plotly' ? (
<Suspense fallback={<ChartLoading />}>
<PlotlyParameterImportance
trials={allTrialsRaw}
designVariables={
studyMetadata?.design_variables?.length > 0
? studyMetadata.design_variables
: Object.keys(allTrialsRaw[0]?.params || {}).map(name => ({ name }))
}
objectiveIndex={0}
objectiveName={studyMetadata?.objectives?.[0]?.name || 'Objective'}
height={280}
/>
</Suspense>
) : (
<ParameterImportanceChart
trials={allTrialsRaw}
designVariables={
studyMetadata?.design_variables?.length > 0
? studyMetadata.design_variables
: Object.keys(allTrialsRaw[0]?.params || {}).map(name => ({ name }))
}
objectiveIndex={0}
objectiveName={studyMetadata?.objectives?.[0]?.name || 'Objective'}
/>
)}
</ExpandableChart>
</div>
)}
{/* Trial History with Sort Controls and Pagination */}
<Card
title={
<div className="flex items-center justify-between w-full">
<span>Trial History ({displayedTrials.length}{showOnlyFEA ? ' with OP2' : ''} trials{showOnlyFEA && allTrials.length !== displayedTrials.length ? ` of ${allTrials.length}` : ''})</span>
<div className="flex gap-2 items-center">
{/* OP2/Zernike toggle - only show for mirror/zernike studies */}
{selectedStudyId && (selectedStudyId.includes('mirror') || selectedStudyId.includes('zernike')) && zernikeAvailableTrials.size > 0 && (
<button
onClick={() => setShowOnlyFEA(!showOnlyFEA)}
className={`px-3 py-1 rounded text-sm ${
showOnlyFEA
? 'bg-green-600 text-white'
: 'bg-dark-500 text-dark-200 hover:bg-dark-400'
}`}
title={`Show only ${zernikeAvailableTrials.size} trials with OP2 results (for Zernike analysis)`}
>
OP2 Only ({zernikeAvailableTrials.size})
</button>
)}
<button
onClick={() => setSortBy('performance')}
className={`px-3 py-1 rounded text-sm ${
sortBy === 'performance'
? 'bg-primary-500 text-white'
: 'bg-dark-500 text-dark-200 hover:bg-dark-400'
}`}
>
Best First
</button>
<button
onClick={() => setSortBy('chronological')}
className={`px-3 py-1 rounded text-sm ${
sortBy === 'chronological'
? 'bg-primary-500 text-white'
: 'bg-dark-500 text-dark-200 hover:bg-dark-400'
}`}
>
Newest First
</button>
{/* Pagination controls */}
{displayedTrials.length > trialsPerPage && (
<div className="flex items-center gap-1 ml-2 border-l border-dark-500 pl-2">
<button
onClick={() => setTrialsPage(Math.max(0, trialsPage - 1))}
disabled={trialsPage === 0}
className="px-2 py-1 text-sm bg-dark-500 rounded disabled:opacity-50 hover:bg-dark-400"
>
</button>
<span className="text-xs text-dark-300 px-2">
{trialsPage + 1}/{Math.ceil(displayedTrials.length / trialsPerPage)}
</span>
<button
onClick={() => setTrialsPage(Math.min(Math.ceil(displayedTrials.length / trialsPerPage) - 1, trialsPage + 1))}
disabled={trialsPage >= Math.ceil(displayedTrials.length / trialsPerPage) - 1}
className="px-2 py-1 text-sm bg-dark-500 rounded disabled:opacity-50 hover:bg-dark-400"
>
</button>
</div>
)}
</div>
</div>
}
>
<div className="space-y-2 max-h-[600px] overflow-y-auto">
{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 (
<div
key={`trial-${trial.trial_number}-${idx}`}
className={`rounded-lg transition-all duration-200 cursor-pointer ${
isBest
? 'bg-green-900 border-l-4 border-green-400'
: 'bg-dark-500 hover:bg-dark-400'
}`}
onClick={() => toggleTrialExpansion(trial.trial_number)}
>
{/* Collapsed View */}
<div className="p-3">
<div className="flex justify-between items-center">
<span className="font-semibold text-primary-400">
Trial #{trial.trial_number}
{isBest && <span className="ml-2 text-xs bg-green-700 text-green-100 px-2 py-1 rounded">BEST</span>}
</span>
<div className="flex items-center gap-3">
<span className={`font-mono text-lg ${
isBest ? 'text-green-400 font-bold' : 'text-dark-100'
}`}>
{trial.objective !== null && trial.objective !== undefined
? trial.objective.toFixed(4)
: 'N/A'}
</span>
{/* Zernike viewer button - only show for mirror/Zernike studies */}
{selectedStudyId && (selectedStudyId.includes('mirror') || selectedStudyId.includes('zernike')) && (
<ZernikeButton
studyId={selectedStudyId}
trialNumber={trial.trial_number}
compact
/>
)}
<span className="text-dark-400 text-sm">
{isExpanded ? '▼' : '▶'}
</span>
</div>
</div>
{/* Quick Preview - Show ALL metrics */}
{!isExpanded && trial.results && Object.keys(trial.results).length > 0 && (
<div className="text-xs text-primary-300 flex flex-wrap gap-3 mt-2">
{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 (
<span key={key} title={`${key}: ${val}`}>
{formatKey(key)}: {formatValue(val)}
</span>
);
})}
{Object.keys(trial.results).length > 6 && (
<span className="text-dark-400">+{Object.keys(trial.results).length - 6} more</span>
)}
</div>
)}
</div>
{/* Expanded View */}
{isExpanded && (
<div className="px-3 pb-3 space-y-3">
{/* Design Variables */}
{trial.design_variables && Object.keys(trial.design_variables).length > 0 && (
<div className="border-t border-dark-400 pt-3">
<h4 className="text-sm font-semibold text-dark-200 mb-2">Design Variables</h4>
<div className="grid grid-cols-2 gap-2 text-xs">
{Object.entries(trial.design_variables).map(([key, val]) => (
<div key={key} className="flex justify-between bg-dark-600 px-2 py-1 rounded">
<span className="text-dark-300">{key}:</span>
<span className="text-dark-100 font-mono">{val.toFixed(4)}</span>
</div>
))}
</div>
</div>
)}
{/* Results */}
{trial.results && Object.keys(trial.results).length > 0 && (
<div className="border-t border-dark-400 pt-3">
<h4 className="text-sm font-semibold text-dark-200 mb-2">Extracted Results</h4>
<div className="grid grid-cols-2 gap-2 text-xs">
{Object.entries(trial.results).map(([key, val]) => (
<div key={key} className="flex justify-between bg-dark-600 px-2 py-1 rounded">
<span className="text-dark-300">{key}:</span>
<span className="text-primary-300 font-mono">
{typeof val === 'number' ? val.toFixed(4) : String(val)}
</span>
</div>
))}
</div>
</div>
)}
{/* All User Attributes */}
{trial.user_attrs && Object.keys(trial.user_attrs).length > 0 && (
<div className="border-t border-dark-400 pt-3">
<h4 className="text-sm font-semibold text-dark-200 mb-2">All Attributes</h4>
<div className="max-h-48 overflow-y-auto">
<pre className="text-xs text-dark-300 bg-dark-700 p-2 rounded">
{JSON.stringify(trial.user_attrs, null, 2)}
</pre>
</div>
</div>
)}
{/* Timestamps */}
{trial.start_time && trial.end_time && (
<div className="border-t border-dark-400 pt-3 text-xs text-dark-400">
<div className="flex justify-between">
<span>Duration:</span>
<span>
{((new Date(trial.end_time).getTime() - new Date(trial.start_time).getTime()) / 1000).toFixed(1)}s
</span>
</div>
</div>
)}
{/* Zernike Analysis Button - Full size in expanded view */}
{selectedStudyId && (selectedStudyId.includes('mirror') || selectedStudyId.includes('zernike')) && (
<div className="border-t border-dark-400 pt-3 mt-3">
<ZernikeButton
studyId={selectedStudyId}
trialNumber={trial.trial_number}
/>
</div>
)}
</div>
)}
</div>
);
})
) : (
<div className="text-center py-8 text-dark-300">
No trials yet. Waiting for optimization to start...
</div>
)}
</div>
</Card>
{/* Console Output - at the bottom */}
<div className="mt-4">
<ConsoleOutput
studyId={selectedStudyId}
refreshInterval={2000}
maxLines={150}
/>
</div>
</main>
</div>
{/* Config Editor Modal */}
{showConfigEditor && selectedStudyId && (
<ConfigEditor
studyId={selectedStudyId}
onClose={() => setShowConfigEditor(false)}
onSaved={() => refreshStudies()}
/>
)}
</div>
);
}