Files
Atomizer/atomizer-dashboard/frontend/src/pages/Analysis.tsx
Anto01 ba0b9a1fae feat(dashboard): Enhanced chat, spec management, and Claude integration
Backend:
- spec.py: New AtomizerSpec REST API endpoints
- spec_manager.py: SpecManager service for unified config
- interview_engine.py: Study creation interview logic
- claude.py: Enhanced Claude API with context
- optimization.py: Extended optimization endpoints
- context_builder.py, session_manager.py: Improved services

Frontend:
- Chat components: Enhanced message rendering, tool call cards
- Hooks: useClaudeCode, useSpecWebSocket, improved useChat
- Pages: Updated Dashboard, Analysis, Insights, Setup, Home
- Components: ParallelCoordinatesPlot, ParetoPlot improvements
- App.tsx: Route updates for canvas/studio

Infrastructure:
- vite.config.ts: Build configuration updates
- start/stop-dashboard.bat: Script improvements
2026-01-20 13:10:47 -05:00

840 lines
36 KiB
TypeScript

import { useState, useEffect, useMemo } from 'react';
import { useNavigate } from 'react-router-dom';
import {
BarChart3,
TrendingUp,
Grid3X3,
Target,
Filter,
Brain,
RefreshCw,
Download,
Layers,
LucideIcon
} from 'lucide-react';
import { useStudy } from '../context/StudyContext';
import { Card } from '../components/common/Card';
import { ConvergencePlot } from '../components/ConvergencePlot';
import { ParameterImportanceChart } from '../components/ParameterImportanceChart';
import { ParallelCoordinatesPlot } from '../components/ParallelCoordinatesPlot';
import { ParetoPlot } from '../components/ParetoPlot';
const NoData = ({ message = 'No data available' }: { message?: string }) => (
<div className="flex items-center justify-center h-64 text-dark-500">
<div className="flex flex-col items-center gap-2">
<BarChart3 className="w-8 h-8" />
<span className="text-sm">{message}</span>
</div>
</div>
);
type AnalysisTab = 'overview' | 'parameters' | 'pareto' | 'correlations' | 'constraints' | 'surrogate' | 'runs';
interface RunData {
run_id: number;
name: string;
source: 'FEA' | 'NN';
trial_count: number;
best_value: number | null;
avg_value: number | null;
first_trial: string | null;
last_trial: string | null;
}
interface TrialData {
trial_number: number;
values: number[];
params: Record<string, number>;
user_attrs?: Record<string, any>;
constraint_satisfied?: boolean;
source?: 'FEA' | 'NN' | 'V10_FEA';
}
interface ObjectiveData {
name: string;
direction: 'minimize' | 'maximize';
}
interface StudyMetadata {
objectives?: ObjectiveData[];
design_variables?: Array<{ name: string; min?: number; max?: number }>;
sampler?: string;
description?: string;
}
export default function Analysis() {
const navigate = useNavigate();
const { selectedStudy, isInitialized } = useStudy();
const [activeTab, setActiveTab] = useState<AnalysisTab>('overview');
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [trials, setTrials] = useState<TrialData[]>([]);
const [metadata, setMetadata] = useState<StudyMetadata | null>(null);
const [paretoFront, setParetoFront] = useState<any[]>([]);
const [runs, setRuns] = useState<RunData[]>([]);
// Redirect if no study selected
useEffect(() => {
if (isInitialized && !selectedStudy) {
navigate('/');
}
}, [selectedStudy, navigate, isInitialized]);
// Load study data
useEffect(() => {
if (!selectedStudy) return;
const loadData = async () => {
setLoading(true);
setError(null);
try {
// Load trial history (required)
const historyRes = await fetch(`/api/optimization/studies/${selectedStudy.id}/history?limit=500`);
if (!historyRes.ok) {
throw new Error(`Failed to load trial history: ${historyRes.statusText}`);
}
const historyData = await historyRes.json();
const trialsData = historyData.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];
}
const rawSource = t.source || t.user_attrs?.source || 'FEA';
const source: 'FEA' | 'NN' | 'V10_FEA' = rawSource === 'NN' ? 'NN' : rawSource === 'V10_FEA' ? 'V10_FEA' : 'FEA';
return {
trial_number: t.trial_number,
values,
params: t.design_variables || {},
user_attrs: t.user_attrs || {},
constraint_satisfied: t.constraint_satisfied !== false,
source
};
});
setTrials(trialsData);
// Load metadata (optional, graceful fallback)
try {
const metadataRes = await fetch(`/api/optimization/studies/${selectedStudy.id}/metadata`);
if (metadataRes.ok) {
const metadataData = await metadataRes.json();
setMetadata(metadataData);
}
} catch {
console.warn('Metadata not available');
}
// Load Pareto front (optional)
try {
const paretoRes = await fetch(`/api/optimization/studies/${selectedStudy.id}/pareto-front`);
if (paretoRes.ok) {
const paretoData = await paretoRes.json();
if (paretoData.is_multi_objective && paretoData.pareto_front) {
setParetoFront(paretoData.pareto_front);
}
}
} catch {
console.warn('Pareto data not available');
}
// Load runs data for comparison (optional)
try {
const runsRes = await fetch(`/api/optimization/studies/${selectedStudy.id}/runs`);
if (runsRes.ok) {
const runsData = await runsRes.json();
if (runsData.runs) {
setRuns(runsData.runs);
}
}
} catch {
console.warn('Runs data not available');
}
} catch (err) {
console.error('Failed to load analysis data:', err);
setError(err instanceof Error ? err.message : 'Failed to load analysis data');
} finally {
setLoading(false);
}
};
loadData();
}, [selectedStudy]);
// Calculate statistics
const stats = useMemo(() => {
if (trials.length === 0) return null;
const objectives = trials.map(t => t.values[0]).filter(v => v !== undefined && !isNaN(v));
if (objectives.length === 0) return null;
const sorted = [...objectives].sort((a, b) => a - b);
const min = sorted[0];
const max = sorted[sorted.length - 1];
const mean = objectives.reduce((a, b) => a + b, 0) / objectives.length;
const median = sorted[Math.floor(sorted.length / 2)];
const stdDev = Math.sqrt(objectives.reduce((sum, val) => sum + Math.pow(val - mean, 2), 0) / objectives.length);
const p25 = sorted[Math.floor(sorted.length * 0.25)];
const p75 = sorted[Math.floor(sorted.length * 0.75)];
const p90 = sorted[Math.floor(sorted.length * 0.90)];
const feaTrials = trials.filter(t => t.source === 'FEA').length;
const nnTrials = trials.filter(t => t.source === 'NN').length;
const feasible = trials.filter(t => t.constraint_satisfied).length;
return {
min,
max,
mean,
median,
stdDev,
p25,
p75,
p90,
feaTrials,
nnTrials,
feasible,
total: trials.length,
feasibilityRate: (feasible / trials.length) * 100
};
}, [trials]);
// Tabs configuration
const tabs: { id: AnalysisTab; label: string; icon: LucideIcon; disabled?: boolean }[] = [
{ id: 'overview', label: 'Overview', icon: BarChart3 },
{ id: 'parameters', label: 'Parameters', icon: TrendingUp },
{ id: 'pareto', label: 'Pareto', icon: Target, disabled: (metadata?.objectives?.length || 0) <= 1 },
{ id: 'correlations', label: 'Correlations', icon: Grid3X3 },
{ id: 'constraints', label: 'Constraints', icon: Filter },
{ id: 'surrogate', label: 'Surrogate', icon: Brain, disabled: trials.filter(t => t.source === 'NN').length === 0 },
{ id: 'runs', label: 'Runs', icon: Layers, disabled: runs.length <= 1 },
];
// Export data
const handleExportCSV = () => {
if (trials.length === 0) return;
const paramNames = Object.keys(trials[0].params);
const headers = ['trial', 'objective', ...paramNames, 'source', 'feasible'].join(',');
const rows = trials.map(t => [
t.trial_number,
t.values[0],
...paramNames.map(p => t.params[p]),
t.source,
t.constraint_satisfied
].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 = `${selectedStudy?.id}_analysis.csv`;
a.click();
URL.revokeObjectURL(url);
};
if (!isInitialized || !selectedStudy) {
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...</p>
</div>
</div>
);
}
const isMultiObjective = (metadata?.objectives?.length || 0) > 1;
return (
<div className="w-full">
{/* Header */}
<header className="mb-6 flex items-center justify-between border-b border-dark-600 pb-4">
<div>
<h1 className="text-2xl font-bold text-primary-400">Analysis</h1>
<p className="text-dark-400 text-sm">Deep analysis for {selectedStudy.name || selectedStudy.id}</p>
</div>
<div className="flex items-center gap-2">
<button
onClick={handleExportCSV}
className="flex items-center gap-2 px-4 py-2 bg-dark-700 hover:bg-dark-600 text-white rounded-lg transition-colors"
disabled={trials.length === 0}
>
<Download className="w-4 h-4" />
Export CSV
</button>
</div>
</header>
{/* Tab Navigation */}
<div className="flex gap-1 mb-6 border-b border-dark-600 overflow-x-auto">
{tabs.map(tab => (
<button
key={tab.id}
onClick={() => !tab.disabled && setActiveTab(tab.id)}
disabled={tab.disabled}
className={`flex items-center gap-2 px-4 py-3 text-sm font-medium transition-colors whitespace-nowrap ${
activeTab === tab.id
? 'text-primary-400 border-b-2 border-primary-400 -mb-[2px]'
: tab.disabled
? 'text-dark-600 cursor-not-allowed'
: 'text-dark-400 hover:text-white'
}`}
>
<tab.icon className="w-4 h-4" />
{tab.label}
</button>
))}
</div>
{loading ? (
<div className="flex items-center justify-center py-16">
<RefreshCw className="w-8 h-8 animate-spin text-dark-400" />
</div>
) : error ? (
<div className="flex flex-col items-center justify-center py-16 gap-4">
<div className="text-red-400 text-6xl">!</div>
<div className="text-red-400 text-lg font-medium">Error Loading Data</div>
<div className="text-dark-400 text-sm max-w-md text-center">{error}</div>
<button
onClick={() => window.location.reload()}
className="mt-4 px-4 py-2 bg-primary-600 hover:bg-primary-500 text-white rounded-lg transition-colors"
>
Retry
</button>
</div>
) : trials.length === 0 ? (
<NoData message="No trials found for this study" />
) : (
<>
{/* Overview Tab */}
{activeTab === 'overview' && (
<div className="space-y-6">
{/* Summary Stats */}
{stats && (
<div className="grid grid-cols-2 md:grid-cols-4 lg:grid-cols-6 gap-4">
<Card className="p-4">
<div className="text-xs text-dark-400 uppercase mb-1">Total Trials</div>
<div className="text-2xl font-bold text-white">{stats.total}</div>
</Card>
<Card className="p-4">
<div className="text-xs text-dark-400 uppercase mb-1">Best Value</div>
<div className="text-2xl font-bold text-green-400">{stats.min.toExponential(3)}</div>
</Card>
<Card className="p-4">
<div className="text-xs text-dark-400 uppercase mb-1">Mean</div>
<div className="text-2xl font-bold text-white">{stats.mean.toExponential(3)}</div>
</Card>
<Card className="p-4">
<div className="text-xs text-dark-400 uppercase mb-1">Median</div>
<div className="text-2xl font-bold text-white">{stats.median.toExponential(3)}</div>
</Card>
<Card className="p-4">
<div className="text-xs text-dark-400 uppercase mb-1">Std Dev</div>
<div className="text-2xl font-bold text-white">{stats.stdDev.toExponential(3)}</div>
</Card>
<Card className="p-4">
<div className="text-xs text-dark-400 uppercase mb-1">Feasibility</div>
<div className="text-2xl font-bold text-primary-400">{stats.feasibilityRate.toFixed(1)}%</div>
</Card>
</div>
)}
{/* Percentile Distribution */}
{stats && (
<Card title="Objective Distribution">
<div className="grid grid-cols-4 gap-4 mb-4">
<div className="text-center p-3 bg-dark-750 rounded-lg">
<div className="text-xs text-dark-400 mb-1">Min</div>
<div className="text-lg font-mono text-green-400">{stats.min.toExponential(3)}</div>
</div>
<div className="text-center p-3 bg-dark-750 rounded-lg">
<div className="text-xs text-dark-400 mb-1">25th %</div>
<div className="text-lg font-mono text-white">{stats.p25.toExponential(3)}</div>
</div>
<div className="text-center p-3 bg-dark-750 rounded-lg">
<div className="text-xs text-dark-400 mb-1">75th %</div>
<div className="text-lg font-mono text-white">{stats.p75.toExponential(3)}</div>
</div>
<div className="text-center p-3 bg-dark-750 rounded-lg">
<div className="text-xs text-dark-400 mb-1">90th %</div>
<div className="text-lg font-mono text-white">{stats.p90.toExponential(3)}</div>
</div>
</div>
</Card>
)}
{/* Convergence Plot */}
{trials.length > 0 && (
<Card title="Convergence Plot">
<ConvergencePlot
trials={trials}
objectiveIndex={0}
objectiveName={metadata?.objectives?.[0]?.name || 'Objective'}
direction="minimize"
/>
</Card>
)}
{/* Best Trials Table */}
<Card title="Top 10 Best Trials">
<div className="overflow-x-auto">
<table className="w-full text-sm">
<thead>
<tr className="border-b border-dark-600">
<th className="text-left py-2 px-3 text-dark-400 font-medium">Rank</th>
<th className="text-left py-2 px-3 text-dark-400 font-medium">Trial</th>
<th className="text-left py-2 px-3 text-dark-400 font-medium">Objective</th>
<th className="text-left py-2 px-3 text-dark-400 font-medium">Source</th>
{Object.keys(trials[0]?.params || {}).slice(0, 3).map(p => (
<th key={p} className="text-left py-2 px-3 text-dark-400 font-medium">{p}</th>
))}
</tr>
</thead>
<tbody>
{[...trials]
.sort((a, b) => (a.values[0] ?? Infinity) - (b.values[0] ?? Infinity))
.slice(0, 10)
.map((trial, idx) => (
<tr key={trial.trial_number} className="border-b border-dark-700">
<td className="py-2 px-3">
<span className={`inline-flex w-6 h-6 items-center justify-center rounded-full text-xs font-bold ${
idx === 0 ? 'bg-yellow-500/20 text-yellow-400' :
idx === 1 ? 'bg-gray-400/20 text-gray-300' :
idx === 2 ? 'bg-orange-700/20 text-orange-400' :
'bg-dark-600 text-dark-400'
}`}>
{idx + 1}
</span>
</td>
<td className="py-2 px-3 font-mono text-white">#{trial.trial_number}</td>
<td className="py-2 px-3 font-mono text-green-400">{trial.values[0]?.toExponential(4)}</td>
<td className="py-2 px-3">
<span className={`px-2 py-0.5 rounded text-xs ${
trial.source === 'NN' ? 'bg-purple-500/20 text-purple-400' : 'bg-blue-500/20 text-blue-400'
}`}>
{trial.source}
</span>
</td>
{Object.keys(trials[0]?.params || {}).slice(0, 3).map(p => (
<td key={p} className="py-2 px-3 font-mono text-dark-300">
{trial.params[p]?.toFixed(4)}
</td>
))}
</tr>
))}
</tbody>
</table>
</div>
</Card>
</div>
)}
{/* Parameters Tab */}
{activeTab === 'parameters' && (
<div className="space-y-6">
{/* Parameter Importance */}
{trials.length > 0 && metadata?.design_variables && (
<Card title="Parameter Importance">
<ParameterImportanceChart
trials={trials}
designVariables={metadata.design_variables}
objectiveIndex={0}
objectiveName={metadata?.objectives?.[0]?.name || 'Objective'}
/>
</Card>
)}
{/* Parallel Coordinates */}
{trials.length > 0 && metadata && (
<Card title="Parallel Coordinates">
<ParallelCoordinatesPlot
paretoData={trials}
objectives={metadata.objectives || []}
designVariables={metadata.design_variables || []}
paretoFront={paretoFront}
/>
</Card>
)}
</div>
)}
{/* Pareto Tab */}
{activeTab === 'pareto' && isMultiObjective && (
<div className="space-y-6">
{/* Pareto Metrics */}
<div className="grid grid-cols-3 gap-4">
<Card className="p-4">
<div className="text-xs text-dark-400 uppercase mb-1">Pareto Solutions</div>
<div className="text-2xl font-bold text-primary-400">{paretoFront.length}</div>
</Card>
<Card className="p-4">
<div className="text-xs text-dark-400 uppercase mb-1">Objectives</div>
<div className="text-2xl font-bold text-white">{metadata?.objectives?.length || 0}</div>
</Card>
<Card className="p-4">
<div className="text-xs text-dark-400 uppercase mb-1">Dominated Ratio</div>
<div className="text-2xl font-bold text-white">
{trials.length > 0 ? ((1 - paretoFront.length / trials.length) * 100).toFixed(1) : 0}%
</div>
</Card>
</div>
{/* Pareto Front Plot */}
{paretoFront.length > 0 && (
<Card title="Pareto Front">
<ParetoPlot
paretoData={paretoFront}
objectives={metadata?.objectives || []}
allTrials={trials}
/>
</Card>
)}
{/* Pareto Solutions Table */}
<Card title="Pareto-Optimal Solutions">
<div className="overflow-x-auto">
<table className="w-full text-sm">
<thead>
<tr className="border-b border-dark-600">
<th className="text-left py-2 px-3 text-dark-400 font-medium">Trial</th>
{metadata?.objectives?.map(obj => (
<th key={obj.name} className="text-left py-2 px-3 text-dark-400 font-medium">{obj.name}</th>
))}
</tr>
</thead>
<tbody>
{paretoFront.slice(0, 20).map((sol, idx) => (
<tr key={idx} className="border-b border-dark-700">
<td className="py-2 px-3 font-mono text-white">#{sol.trial_number}</td>
{sol.values?.map((v: number, i: number) => (
<td key={i} className="py-2 px-3 font-mono text-primary-400">{v?.toExponential(4)}</td>
))}
</tr>
))}
</tbody>
</table>
</div>
</Card>
</div>
)}
{/* Correlations Tab */}
{activeTab === 'correlations' && (
<div className="space-y-6">
{/* Correlation Analysis */}
{trials.length > 2 && (
<Card title="Parameter-Objective Correlation Analysis">
<CorrelationTable trials={trials} objectiveName={metadata?.objectives?.[0]?.name || 'Objective'} />
</Card>
)}
{/* Correlation Interpretation Guide */}
<Card title="Interpreting Correlations">
<div className="grid grid-cols-2 md:grid-cols-4 gap-4 text-sm">
<div className="p-3 bg-blue-500/10 rounded-lg border border-blue-500/30">
<div className="text-blue-400 font-semibold mb-1">Strong Positive (0.7 to 1.0)</div>
<p className="text-dark-400 text-xs">Increasing parameter increases objective</p>
</div>
<div className="p-3 bg-blue-500/5 rounded-lg border border-blue-500/20">
<div className="text-blue-300 font-semibold mb-1">Moderate Positive (0.3 to 0.7)</div>
<p className="text-dark-400 text-xs">Some positive relationship</p>
</div>
<div className="p-3 bg-red-500/5 rounded-lg border border-red-500/20">
<div className="text-red-300 font-semibold mb-1">Moderate Negative (-0.7 to -0.3)</div>
<p className="text-dark-400 text-xs">Some negative relationship</p>
</div>
<div className="p-3 bg-red-500/10 rounded-lg border border-red-500/30">
<div className="text-red-400 font-semibold mb-1">Strong Negative (-1.0 to -0.7)</div>
<p className="text-dark-400 text-xs">Increasing parameter decreases objective</p>
</div>
</div>
</Card>
{/* Top Correlations Table */}
{trials.length > 2 && (
<Card title="Strongest Parameter Correlations with Objective">
<CorrelationTable trials={trials} objectiveName={metadata?.objectives?.[0]?.name || 'Objective'} />
</Card>
)}
</div>
)}
{/* Constraints Tab */}
{activeTab === 'constraints' && stats && (
<div className="space-y-6">
<div className="grid grid-cols-3 gap-4">
<Card className="p-4">
<div className="text-xs text-dark-400 uppercase mb-1">Feasible Trials</div>
<div className="text-2xl font-bold text-green-400">{stats.feasible}</div>
</Card>
<Card className="p-4">
<div className="text-xs text-dark-400 uppercase mb-1">Infeasible Trials</div>
<div className="text-2xl font-bold text-red-400">{stats.total - stats.feasible}</div>
</Card>
<Card className="p-4">
<div className="text-xs text-dark-400 uppercase mb-1">Feasibility Rate</div>
<div className="text-2xl font-bold text-primary-400">{stats.feasibilityRate.toFixed(1)}%</div>
</Card>
</div>
{/* Feasibility Summary */}
<Card title="Feasibility Analysis">
<div className="p-4">
<div className="flex items-center gap-4 mb-4">
<div className="flex-1 bg-dark-700 rounded-full h-4 overflow-hidden">
<div
className="h-full bg-green-500 transition-all duration-500"
style={{ width: `${stats.feasibilityRate}%` }}
/>
</div>
<span className="text-lg font-bold text-green-400">{stats.feasibilityRate.toFixed(1)}%</span>
</div>
<p className="text-dark-400 text-sm">
{stats.feasible} of {stats.total} trials satisfy all constraints
</p>
</div>
</Card>
{/* Infeasible Trials List */}
{stats.total - stats.feasible > 0 && (
<Card title="Recent Infeasible Trials">
<div className="overflow-x-auto max-h-64">
<table className="w-full text-sm">
<thead className="sticky top-0 bg-dark-800">
<tr className="border-b border-dark-600">
<th className="text-left py-2 px-3 text-dark-400 font-medium">Trial</th>
<th className="text-left py-2 px-3 text-dark-400 font-medium">Objective</th>
<th className="text-left py-2 px-3 text-dark-400 font-medium">Source</th>
</tr>
</thead>
<tbody>
{trials
.filter(t => !t.constraint_satisfied)
.slice(-20)
.reverse()
.map(trial => (
<tr key={trial.trial_number} className="border-b border-dark-700">
<td className="py-2 px-3 font-mono text-white">#{trial.trial_number}</td>
<td className="py-2 px-3 font-mono text-red-400">{trial.values[0]?.toExponential(4) || 'N/A'}</td>
<td className="py-2 px-3">
<span className={`px-2 py-0.5 rounded text-xs ${
trial.source === 'NN' ? 'bg-purple-500/20 text-purple-400' : 'bg-blue-500/20 text-blue-400'
}`}>
{trial.source}
</span>
</td>
</tr>
))}
</tbody>
</table>
</div>
</Card>
)}
</div>
)}
{/* Surrogate Tab */}
{activeTab === 'surrogate' && stats && (
<div className="space-y-6">
<div className="grid grid-cols-4 gap-4">
<Card className="p-4">
<div className="text-xs text-dark-400 uppercase mb-1">FEA Evaluations</div>
<div className="text-2xl font-bold text-blue-400">{stats.feaTrials}</div>
</Card>
<Card className="p-4">
<div className="text-xs text-dark-400 uppercase mb-1">NN Predictions</div>
<div className="text-2xl font-bold text-purple-400">{stats.nnTrials}</div>
</Card>
<Card className="p-4">
<div className="text-xs text-dark-400 uppercase mb-1">NN Ratio</div>
<div className="text-2xl font-bold text-green-400">
{stats.nnTrials > 0 ? `${((stats.nnTrials / stats.total) * 100).toFixed(0)}%` : '0%'}
</div>
</Card>
<Card className="p-4">
<div className="text-xs text-dark-400 uppercase mb-1">Speedup Factor</div>
<div className="text-2xl font-bold text-primary-400">
{stats.feaTrials > 0 ? `${(stats.total / stats.feaTrials).toFixed(1)}x` : '1.0x'}
</div>
</Card>
</div>
{/* Surrogate Performance Summary */}
<Card title="Surrogate Model Performance">
<div className="grid grid-cols-2 gap-6 p-4">
<div>
<h4 className="text-sm font-semibold text-dark-300 mb-3">Trial Distribution</h4>
<div className="space-y-2">
<div className="flex items-center gap-3">
<div className="w-3 h-3 bg-blue-500 rounded-full"></div>
<span className="text-dark-200">FEA: {stats.feaTrials} trials</span>
<span className="text-dark-400 ml-auto">
{((stats.feaTrials / stats.total) * 100).toFixed(0)}%
</span>
</div>
<div className="flex items-center gap-3">
<div className="w-3 h-3 bg-purple-500 rounded-full"></div>
<span className="text-dark-200">NN: {stats.nnTrials} trials</span>
<span className="text-dark-400 ml-auto">
{((stats.nnTrials / stats.total) * 100).toFixed(0)}%
</span>
</div>
</div>
</div>
<div>
<h4 className="text-sm font-semibold text-dark-300 mb-3">Efficiency Gains</h4>
<div className="text-center p-4 bg-dark-750 rounded-lg">
<div className="text-3xl font-bold text-primary-400">
{stats.feaTrials > 0 ? `${(stats.total / stats.feaTrials).toFixed(1)}x` : '1.0x'}
</div>
<div className="text-xs text-dark-400 mt-1">Effective Speedup</div>
</div>
</div>
</div>
</Card>
</div>
)}
{/* Runs Tab */}
{activeTab === 'runs' && runs.length > 0 && (
<div className="space-y-6">
<Card title="Optimization Runs Comparison">
<p className="text-dark-400 text-sm mb-4">
Compare different optimization runs within this study. Studies with adaptive optimization
may have multiple runs (e.g., initial FEA exploration, NN-accelerated iterations).
</p>
<div className="overflow-x-auto">
<table className="w-full text-sm">
<thead>
<tr className="border-b border-dark-600">
<th className="text-left py-2 px-3 text-dark-400 font-medium">Run</th>
<th className="text-left py-2 px-3 text-dark-400 font-medium">Source</th>
<th className="text-left py-2 px-3 text-dark-400 font-medium">Trials</th>
<th className="text-left py-2 px-3 text-dark-400 font-medium">Best Value</th>
<th className="text-left py-2 px-3 text-dark-400 font-medium">Avg Value</th>
</tr>
</thead>
<tbody>
{runs.map((run) => (
<tr key={run.run_id} className="border-b border-dark-700">
<td className="py-2 px-3 font-mono text-white">{run.name || `Run ${run.run_id}`}</td>
<td className="py-2 px-3">
<span className={`px-2 py-0.5 rounded text-xs ${
run.source === 'NN' ? 'bg-purple-500/20 text-purple-400' : 'bg-blue-500/20 text-blue-400'
}`}>
{run.source}
</span>
</td>
<td className="py-2 px-3 text-dark-200">{run.trial_count}</td>
<td className="py-2 px-3 font-mono text-green-400">{run.best_value?.toExponential(4) || 'N/A'}</td>
<td className="py-2 px-3 font-mono text-dark-300">{run.avg_value?.toExponential(4) || 'N/A'}</td>
</tr>
))}
</tbody>
</table>
</div>
</Card>
</div>
)}
</>
)}
</div>
);
}
// Helper component for correlation table
function CorrelationTable({ trials, objectiveName }: { trials: TrialData[]; objectiveName: string }) {
const correlations = useMemo(() => {
if (trials.length < 3) return [];
const paramNames = Object.keys(trials[0].params);
const objectives = trials.map(t => t.values[0]).filter(v => v !== undefined && !isNaN(v));
const results: { param: string; correlation: number; absCorr: number }[] = [];
paramNames.forEach(param => {
const paramValues = trials.map(t => t.params[param]).filter(v => v !== undefined && !isNaN(v));
const minLen = Math.min(paramValues.length, objectives.length);
if (minLen < 3) return;
// Calculate Pearson correlation
const x = paramValues.slice(0, minLen);
const y = objectives.slice(0, minLen);
const n = x.length;
const meanX = x.reduce((a, b) => a + b, 0) / n;
const meanY = y.reduce((a, b) => a + b, 0) / n;
let numerator = 0;
let denomX = 0;
let denomY = 0;
for (let i = 0; i < n; i++) {
const dx = x[i] - meanX;
const dy = y[i] - meanY;
numerator += dx * dy;
denomX += dx * dx;
denomY += dy * dy;
}
const denominator = Math.sqrt(denomX) * Math.sqrt(denomY);
const corr = denominator === 0 ? 0 : numerator / denominator;
results.push({ param, correlation: corr, absCorr: Math.abs(corr) });
});
return results.sort((a, b) => b.absCorr - a.absCorr);
}, [trials]);
if (correlations.length === 0) {
return <p className="text-dark-400 text-center py-4">Not enough data for correlation analysis</p>;
}
return (
<table className="w-full text-sm">
<thead>
<tr className="border-b border-dark-600">
<th className="text-left py-2 px-3 text-dark-400 font-medium">Parameter</th>
<th className="text-left py-2 px-3 text-dark-400 font-medium">Correlation with {objectiveName}</th>
<th className="text-left py-2 px-3 text-dark-400 font-medium">Strength</th>
</tr>
</thead>
<tbody>
{correlations.slice(0, 10).map(({ param, correlation, absCorr }) => (
<tr key={param} className="border-b border-dark-700">
<td className="py-2 px-3 font-mono text-white">{param}</td>
<td className="py-2 px-3">
<div className="flex items-center gap-2">
<div className="w-24 h-2 bg-dark-700 rounded-full overflow-hidden">
<div
className={`h-full rounded-full ${correlation > 0 ? 'bg-blue-500' : 'bg-red-500'}`}
style={{ width: `${absCorr * 100}%`, marginLeft: correlation < 0 ? 'auto' : 0 }}
/>
</div>
<span className={`font-mono ${
absCorr > 0.7 ? 'text-white font-bold' :
absCorr > 0.3 ? 'text-dark-200' : 'text-dark-400'
}`}>
{correlation > 0 ? '+' : ''}{correlation.toFixed(3)}
</span>
</div>
</td>
<td className="py-2 px-3">
<span className={`px-2 py-0.5 rounded text-xs ${
absCorr > 0.7 ? 'bg-primary-500/20 text-primary-400' :
absCorr > 0.3 ? 'bg-yellow-500/20 text-yellow-400' :
'bg-dark-600 text-dark-400'
}`}>
{absCorr > 0.7 ? 'Strong' : absCorr > 0.3 ? 'Moderate' : 'Weak'}
</span>
</td>
</tr>
))}
</tbody>
</table>
);
}