feat: Add Analysis page, run comparison, notifications, and config editor
Dashboard enhancements:
- Add Analysis page with tabs: Overview, Parameters, Pareto, Correlations, Constraints, Surrogate, Runs
- Add PlotlyCorrelationHeatmap for parameter-objective correlation analysis
- Add PlotlyFeasibilityChart for constraint satisfaction visualization
- Add PlotlySurrogateQuality for FEA vs NN prediction comparison
- Add PlotlyRunComparison for comparing optimization runs within a study
Real-time improvements:
- Replace watchdog file-watching with SQLite database polling for better Windows reliability
- Add DatabasePoller class with 2-second polling interval
- Enhanced WebSocket messages: trial_completed, new_best, pareto_update, progress
Desktop notifications:
- Add useNotifications hook using Web Notifications API
- Add NotificationSettings toggle component
- Notify users when new best solutions are found
Config editor:
- Add PUT /studies/{study_id}/config endpoint with auto-backup
- Add ConfigEditor modal with tabs: General, Variables, Objectives, Settings, JSON
- Prevents editing while optimization is running
Enhanced Pareto visualization:
- Add dark mode styling with transparent backgrounds
- Add stats bar showing Pareto, FEA, NN, and infeasible counts
- Add Pareto front connecting line for 2D view
- Add table showing top 10 Pareto-optimal solutions
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
757
atomizer-dashboard/frontend/src/pages/Analysis.tsx
Normal file
757
atomizer-dashboard/frontend/src/pages/Analysis.tsx
Normal file
@@ -0,0 +1,757 @@
|
||||
import { useState, useEffect, lazy, Suspense, 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';
|
||||
|
||||
// Lazy load charts
|
||||
const PlotlyParetoPlot = lazy(() => import('../components/plotly/PlotlyParetoPlot').then(m => ({ default: m.PlotlyParetoPlot })));
|
||||
const PlotlyParallelCoordinates = lazy(() => import('../components/plotly/PlotlyParallelCoordinates').then(m => ({ default: m.PlotlyParallelCoordinates })));
|
||||
const PlotlyParameterImportance = lazy(() => import('../components/plotly/PlotlyParameterImportance').then(m => ({ default: m.PlotlyParameterImportance })));
|
||||
const PlotlyConvergencePlot = lazy(() => import('../components/plotly/PlotlyConvergencePlot').then(m => ({ default: m.PlotlyConvergencePlot })));
|
||||
const PlotlyCorrelationHeatmap = lazy(() => import('../components/plotly/PlotlyCorrelationHeatmap').then(m => ({ default: m.PlotlyCorrelationHeatmap })));
|
||||
const PlotlyFeasibilityChart = lazy(() => import('../components/plotly/PlotlyFeasibilityChart').then(m => ({ default: m.PlotlyFeasibilityChart })));
|
||||
const PlotlySurrogateQuality = lazy(() => import('../components/plotly/PlotlySurrogateQuality').then(m => ({ default: m.PlotlySurrogateQuality })));
|
||||
const PlotlyRunComparison = lazy(() => import('../components/plotly/PlotlyRunComparison').then(m => ({ default: m.PlotlyRunComparison })));
|
||||
|
||||
const ChartLoading = () => (
|
||||
<div className="flex items-center justify-center h-64 text-dark-400">
|
||||
<div className="animate-pulse">Loading chart...</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 [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);
|
||||
try {
|
||||
// Load trial history
|
||||
const historyRes = await fetch(`/api/optimization/studies/${selectedStudy.id}/history?limit=500`);
|
||||
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
|
||||
const metadataRes = await fetch(`/api/optimization/studies/${selectedStudy.id}/metadata`);
|
||||
const metadataData = await metadataRes.json();
|
||||
setMetadata(metadataData);
|
||||
|
||||
// Load Pareto front
|
||||
const paretoRes = await fetch(`/api/optimization/studies/${selectedStudy.id}/pareto-front`);
|
||||
const paretoData = await paretoRes.json();
|
||||
if (paretoData.is_multi_objective && paretoData.pareto_front) {
|
||||
setParetoFront(paretoData.pareto_front);
|
||||
}
|
||||
|
||||
// Load runs data for comparison
|
||||
const runsRes = await fetch(`/api/optimization/studies/${selectedStudy.id}/runs`);
|
||||
const runsData = await runsRes.json();
|
||||
if (runsData.runs) {
|
||||
setRuns(runsData.runs);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Failed to load analysis data:', err);
|
||||
} 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 max-w-[2400px] mx-auto px-4">
|
||||
{/* 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>
|
||||
) : (
|
||||
<>
|
||||
{/* 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">
|
||||
<Suspense fallback={<ChartLoading />}>
|
||||
<PlotlyConvergencePlot
|
||||
trials={trials}
|
||||
objectiveIndex={0}
|
||||
objectiveName={metadata?.objectives?.[0]?.name || 'Objective'}
|
||||
direction="minimize"
|
||||
height={350}
|
||||
/>
|
||||
</Suspense>
|
||||
</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">
|
||||
<Suspense fallback={<ChartLoading />}>
|
||||
<PlotlyParameterImportance
|
||||
trials={trials}
|
||||
designVariables={metadata.design_variables}
|
||||
objectiveIndex={0}
|
||||
objectiveName={metadata?.objectives?.[0]?.name || 'Objective'}
|
||||
height={400}
|
||||
/>
|
||||
</Suspense>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{/* Parallel Coordinates */}
|
||||
{trials.length > 0 && metadata && (
|
||||
<Card title="Parallel Coordinates">
|
||||
<Suspense fallback={<ChartLoading />}>
|
||||
<PlotlyParallelCoordinates
|
||||
trials={trials}
|
||||
objectives={metadata.objectives || []}
|
||||
designVariables={metadata.design_variables || []}
|
||||
paretoFront={paretoFront}
|
||||
height={450}
|
||||
/>
|
||||
</Suspense>
|
||||
</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">
|
||||
<Suspense fallback={<ChartLoading />}>
|
||||
<PlotlyParetoPlot
|
||||
trials={trials}
|
||||
paretoFront={paretoFront}
|
||||
objectives={metadata?.objectives || []}
|
||||
height={500}
|
||||
/>
|
||||
</Suspense>
|
||||
</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 Heatmap */}
|
||||
{trials.length > 2 && (
|
||||
<Card title="Parameter-Objective Correlation Matrix">
|
||||
<Suspense fallback={<ChartLoading />}>
|
||||
<PlotlyCorrelationHeatmap
|
||||
trials={trials}
|
||||
objectiveName={metadata?.objectives?.[0]?.name || 'Objective'}
|
||||
height={Math.min(500, 100 + Object.keys(trials[0]?.params || {}).length * 40)}
|
||||
/>
|
||||
</Suspense>
|
||||
</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 Over Time Chart */}
|
||||
<Card title="Feasibility Rate Over Time">
|
||||
<Suspense fallback={<ChartLoading />}>
|
||||
<PlotlyFeasibilityChart trials={trials} height={350} />
|
||||
</Suspense>
|
||||
</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 Quality Charts */}
|
||||
<Card title="Surrogate Model Analysis">
|
||||
<Suspense fallback={<ChartLoading />}>
|
||||
<PlotlySurrogateQuality trials={trials} height={400} />
|
||||
</Suspense>
|
||||
</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>
|
||||
<Suspense fallback={<ChartLoading />}>
|
||||
<PlotlyRunComparison runs={runs} height={400} />
|
||||
</Suspense>
|
||||
</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>
|
||||
);
|
||||
}
|
||||
@@ -1,12 +1,15 @@
|
||||
import { useState, useEffect, lazy, Suspense } from 'react';
|
||||
import { useState, useEffect, lazy, Suspense, useRef } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { Terminal } from 'lucide-react';
|
||||
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';
|
||||
@@ -14,6 +17,7 @@ import { ConvergencePlot } from '../components/ConvergencePlot';
|
||||
import { StudyReportViewer } from '../components/StudyReportViewer';
|
||||
import { ConsoleOutput } from '../components/ConsoleOutput';
|
||||
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
|
||||
@@ -31,16 +35,10 @@ const ChartLoading = () => (
|
||||
|
||||
export default function Dashboard() {
|
||||
const navigate = useNavigate();
|
||||
const { selectedStudy, refreshStudies } = useStudy();
|
||||
const { selectedStudy, refreshStudies, isInitialized } = useStudy();
|
||||
const selectedStudyId = selectedStudy?.id || null;
|
||||
|
||||
// Redirect to home if no study selected
|
||||
useEffect(() => {
|
||||
if (!selectedStudy) {
|
||||
navigate('/');
|
||||
}
|
||||
}, [selectedStudy, navigate]);
|
||||
|
||||
// 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);
|
||||
@@ -52,9 +50,9 @@ export default function Dashboard() {
|
||||
const [trialsPage, setTrialsPage] = useState(0);
|
||||
const trialsPerPage = 50; // Limit trials per page for performance
|
||||
|
||||
// Parameter Space axis selection
|
||||
const [paramXIndex, setParamXIndex] = useState(0);
|
||||
const [paramYIndex, setParamYIndex] = useState(1);
|
||||
// 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);
|
||||
@@ -64,9 +62,27 @@ export default function Dashboard() {
|
||||
// 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);
|
||||
@@ -84,8 +100,22 @@ export default function Dashboard() {
|
||||
const trial = msg.data as Trial;
|
||||
setAllTrials(prev => [...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);
|
||||
@@ -162,9 +192,31 @@ export default function Dashboard() {
|
||||
}
|
||||
})
|
||||
.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));
|
||||
}
|
||||
}, [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 trials based on selected sort order
|
||||
useEffect(() => {
|
||||
let sorted = [...allTrials];
|
||||
@@ -223,50 +275,19 @@ export default function Dashboard() {
|
||||
return () => clearInterval(refreshInterval);
|
||||
}, [selectedStudyId]);
|
||||
|
||||
// Sample data for charts when there are too many trials (performance optimization)
|
||||
const MAX_CHART_POINTS = 200; // Reduced for better performance
|
||||
const sampleData = <T,>(data: T[], maxPoints: number): T[] => {
|
||||
if (data.length <= maxPoints) return data;
|
||||
const step = Math.ceil(data.length / maxPoints);
|
||||
return data.filter((_, i) => i % step === 0 || i === data.length - 1);
|
||||
};
|
||||
// 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>
|
||||
);
|
||||
}
|
||||
|
||||
// Prepare chart data with proper null/undefined handling
|
||||
const allValidTrials = allTrials
|
||||
.filter(t => t.objective !== null && t.objective !== undefined)
|
||||
.sort((a, b) => a.trial_number - b.trial_number);
|
||||
|
||||
// Calculate best_so_far for each trial
|
||||
let runningBest = Infinity;
|
||||
const convergenceDataFull: ConvergenceDataPoint[] = allValidTrials.map(trial => {
|
||||
if (trial.objective < runningBest) {
|
||||
runningBest = trial.objective;
|
||||
}
|
||||
return {
|
||||
trial_number: trial.trial_number,
|
||||
objective: trial.objective,
|
||||
best_so_far: runningBest,
|
||||
};
|
||||
});
|
||||
|
||||
// Sample for chart rendering performance
|
||||
const convergenceData = sampleData(convergenceDataFull, MAX_CHART_POINTS);
|
||||
|
||||
const parameterSpaceDataFull: 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,
|
||||
};
|
||||
});
|
||||
|
||||
// Sample for chart rendering performance
|
||||
const parameterSpaceData = sampleData(parameterSpaceDataFull, MAX_CHART_POINTS);
|
||||
// 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);
|
||||
@@ -350,6 +371,21 @@ export default function Dashboard() {
|
||||
<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)}
|
||||
@@ -421,6 +457,27 @@ export default function Dashboard() {
|
||||
<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 */}
|
||||
@@ -795,6 +852,15 @@ export default function Dashboard() {
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
|
||||
{/* Config Editor Modal */}
|
||||
{showConfigEditor && selectedStudyId && (
|
||||
<ConfigEditor
|
||||
studyId={selectedStudyId}
|
||||
onClose={() => setShowConfigEditor(false)}
|
||||
onSaved={() => refreshStudies()}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,38 +1,33 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import React, { useEffect, useState, useMemo } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import {
|
||||
FolderOpen,
|
||||
Play,
|
||||
Pause,
|
||||
CheckCircle,
|
||||
Clock,
|
||||
AlertCircle,
|
||||
ArrowRight,
|
||||
RefreshCw,
|
||||
Zap,
|
||||
FileText,
|
||||
ChevronDown,
|
||||
ChevronUp,
|
||||
Target,
|
||||
Activity
|
||||
Activity,
|
||||
BarChart3,
|
||||
TrendingUp,
|
||||
ArrowRight
|
||||
} from 'lucide-react';
|
||||
import { useStudy } from '../context/StudyContext';
|
||||
import { Study } from '../types';
|
||||
import ReactMarkdown from 'react-markdown';
|
||||
import remarkGfm from 'remark-gfm';
|
||||
import remarkMath from 'remark-math';
|
||||
import rehypeKatex from 'rehype-katex';
|
||||
import 'katex/dist/katex.min.css';
|
||||
import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter';
|
||||
import { oneDark } from 'react-syntax-highlighter/dist/esm/styles/prism';
|
||||
import { apiClient } from '../api/client';
|
||||
import { MarkdownRenderer } from '../components/MarkdownRenderer';
|
||||
|
||||
const Home: React.FC = () => {
|
||||
const { studies, setSelectedStudy, refreshStudies, isLoading } = useStudy();
|
||||
const [selectedPreview, setSelectedPreview] = useState<Study | null>(null);
|
||||
const [readme, setReadme] = useState<string>('');
|
||||
const [readmeLoading, setReadmeLoading] = useState(false);
|
||||
const [showAllStudies, setShowAllStudies] = useState(false);
|
||||
const [sortField, setSortField] = useState<'name' | 'status' | 'trials' | 'bestValue'>('trials');
|
||||
const [sortDir, setSortDir] = useState<'asc' | 'desc'>('desc');
|
||||
const navigate = useNavigate();
|
||||
|
||||
// Load README when a study is selected for preview
|
||||
@@ -49,7 +44,7 @@ const Home: React.FC = () => {
|
||||
try {
|
||||
const response = await apiClient.getStudyReadme(studyId);
|
||||
setReadme(response.content || 'No README found for this study.');
|
||||
} catch (error) {
|
||||
} catch {
|
||||
setReadme('No README found for this study.');
|
||||
} finally {
|
||||
setReadmeLoading(false);
|
||||
@@ -61,78 +56,88 @@ const Home: React.FC = () => {
|
||||
navigate('/dashboard');
|
||||
};
|
||||
|
||||
const handleSort = (field: typeof sortField) => {
|
||||
if (sortField === field) {
|
||||
setSortDir(sortDir === 'asc' ? 'desc' : 'asc');
|
||||
} else {
|
||||
setSortField(field);
|
||||
setSortDir('desc');
|
||||
}
|
||||
};
|
||||
|
||||
// Sort studies
|
||||
const sortedStudies = useMemo(() => {
|
||||
return [...studies].sort((a, b) => {
|
||||
let aVal: any, bVal: any;
|
||||
|
||||
switch (sortField) {
|
||||
case 'name':
|
||||
aVal = (a.name || a.id).toLowerCase();
|
||||
bVal = (b.name || b.id).toLowerCase();
|
||||
break;
|
||||
case 'trials':
|
||||
aVal = a.progress.current;
|
||||
bVal = b.progress.current;
|
||||
break;
|
||||
case 'bestValue':
|
||||
aVal = a.best_value ?? Infinity;
|
||||
bVal = b.best_value ?? Infinity;
|
||||
break;
|
||||
case 'status':
|
||||
const statusOrder = { running: 0, paused: 1, completed: 2, not_started: 3 };
|
||||
aVal = statusOrder[a.status as keyof typeof statusOrder] ?? 4;
|
||||
bVal = statusOrder[b.status as keyof typeof statusOrder] ?? 4;
|
||||
break;
|
||||
default:
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (sortDir === 'asc') {
|
||||
return aVal < bVal ? -1 : aVal > bVal ? 1 : 0;
|
||||
} else {
|
||||
return aVal > bVal ? -1 : aVal < bVal ? 1 : 0;
|
||||
}
|
||||
});
|
||||
}, [studies, sortField, sortDir]);
|
||||
|
||||
// Aggregate stats
|
||||
const aggregateStats = useMemo(() => {
|
||||
const totalStudies = studies.length;
|
||||
const runningStudies = studies.filter(s => s.status === 'running').length;
|
||||
const completedStudies = studies.filter(s => s.status === 'completed').length;
|
||||
const totalTrials = studies.reduce((sum, s) => sum + s.progress.current, 0);
|
||||
const studiesWithValues = studies.filter(s => s.best_value !== null);
|
||||
const bestOverall = studiesWithValues.length > 0
|
||||
? studiesWithValues.reduce((best, curr) =>
|
||||
(curr.best_value! < best.best_value!) ? curr : best
|
||||
)
|
||||
: null;
|
||||
|
||||
return { totalStudies, runningStudies, completedStudies, totalTrials, bestOverall };
|
||||
}, [studies]);
|
||||
|
||||
const getStatusIcon = (status: string) => {
|
||||
switch (status) {
|
||||
case 'running':
|
||||
return <Play className="w-3.5 h-3.5" />;
|
||||
return <Play className="w-4 h-4 text-green-400" />;
|
||||
case 'paused':
|
||||
return <Pause className="w-3.5 h-3.5" />;
|
||||
return <Pause className="w-4 h-4 text-orange-400" />;
|
||||
case 'completed':
|
||||
return <CheckCircle className="w-3.5 h-3.5" />;
|
||||
case 'not_started':
|
||||
return <Clock className="w-3.5 h-3.5" />;
|
||||
return <CheckCircle className="w-4 h-4 text-blue-400" />;
|
||||
default:
|
||||
return <AlertCircle className="w-3.5 h-3.5" />;
|
||||
return <Clock className="w-4 h-4 text-dark-400" />;
|
||||
}
|
||||
};
|
||||
|
||||
const getStatusStyles = (status: string) => {
|
||||
const getStatusColor = (status: string) => {
|
||||
switch (status) {
|
||||
case 'running':
|
||||
return {
|
||||
badge: 'bg-green-500/20 text-green-400 border-green-500/30',
|
||||
card: 'border-green-500/30 hover:border-green-500/50',
|
||||
glow: 'shadow-green-500/10'
|
||||
};
|
||||
case 'paused':
|
||||
return {
|
||||
badge: 'bg-orange-500/20 text-orange-400 border-orange-500/30',
|
||||
card: 'border-orange-500/30 hover:border-orange-500/50',
|
||||
glow: 'shadow-orange-500/10'
|
||||
};
|
||||
case 'completed':
|
||||
return {
|
||||
badge: 'bg-blue-500/20 text-blue-400 border-blue-500/30',
|
||||
card: 'border-blue-500/30 hover:border-blue-500/50',
|
||||
glow: 'shadow-blue-500/10'
|
||||
};
|
||||
case 'not_started':
|
||||
return {
|
||||
badge: 'bg-dark-600 text-dark-400 border-dark-500',
|
||||
card: 'border-dark-600 hover:border-dark-500',
|
||||
glow: ''
|
||||
};
|
||||
default:
|
||||
return {
|
||||
badge: 'bg-yellow-500/20 text-yellow-400 border-yellow-500/30',
|
||||
card: 'border-yellow-500/30 hover:border-yellow-500/50',
|
||||
glow: 'shadow-yellow-500/10'
|
||||
};
|
||||
case 'running': return 'text-green-400 bg-green-500/10';
|
||||
case 'paused': return 'text-orange-400 bg-orange-500/10';
|
||||
case 'completed': return 'text-blue-400 bg-blue-500/10';
|
||||
default: return 'text-dark-400 bg-dark-600';
|
||||
}
|
||||
};
|
||||
|
||||
// Study sort options
|
||||
const [studySort, setStudySort] = useState<'date' | 'running' | 'trials'>('date');
|
||||
|
||||
// Sort studies based on selected sort option
|
||||
const sortedStudies = [...studies].sort((a, b) => {
|
||||
if (studySort === 'running') {
|
||||
// Running first, then by date
|
||||
if (a.status === 'running' && b.status !== 'running') return -1;
|
||||
if (b.status === 'running' && a.status !== 'running') return 1;
|
||||
}
|
||||
if (studySort === 'trials') {
|
||||
// By trial count (most trials first)
|
||||
return b.progress.current - a.progress.current;
|
||||
}
|
||||
// Default: sort by date (newest first)
|
||||
const aDate = a.last_modified || a.created_at || '';
|
||||
const bDate = b.last_modified || b.created_at || '';
|
||||
return bDate.localeCompare(aDate);
|
||||
});
|
||||
|
||||
const displayedStudies = showAllStudies ? sortedStudies : sortedStudies.slice(0, 6);
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-dark-900">
|
||||
{/* Header */}
|
||||
@@ -162,352 +167,254 @@ const Home: React.FC = () => {
|
||||
</header>
|
||||
|
||||
<main className="max-w-[1920px] mx-auto px-6 py-8">
|
||||
{/* Study Selection Section */}
|
||||
<section className="mb-8">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<h2 className="text-lg font-semibold text-white flex items-center gap-2">
|
||||
<FolderOpen className="w-5 h-5 text-primary-400" />
|
||||
Select a Study
|
||||
</h2>
|
||||
<div className="flex items-center gap-4">
|
||||
{/* Sort Controls */}
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-sm text-dark-400">Sort:</span>
|
||||
<div className="flex rounded-lg overflow-hidden border border-dark-600">
|
||||
<button
|
||||
onClick={() => setStudySort('date')}
|
||||
className={`px-3 py-1.5 text-sm transition-colors ${
|
||||
studySort === 'date'
|
||||
? 'bg-primary-500 text-white'
|
||||
: 'bg-dark-700 text-dark-300 hover:bg-dark-600'
|
||||
}`}
|
||||
>
|
||||
Newest
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setStudySort('running')}
|
||||
className={`px-3 py-1.5 text-sm transition-colors ${
|
||||
studySort === 'running'
|
||||
? 'bg-primary-500 text-white'
|
||||
: 'bg-dark-700 text-dark-300 hover:bg-dark-600'
|
||||
}`}
|
||||
>
|
||||
Running
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setStudySort('trials')}
|
||||
className={`px-3 py-1.5 text-sm transition-colors ${
|
||||
studySort === 'trials'
|
||||
? 'bg-primary-500 text-white'
|
||||
: 'bg-dark-700 text-dark-300 hover:bg-dark-600'
|
||||
}`}
|
||||
>
|
||||
Most Trials
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{studies.length > 6 && (
|
||||
<button
|
||||
onClick={() => setShowAllStudies(!showAllStudies)}
|
||||
className="text-sm text-primary-400 hover:text-primary-300 flex items-center gap-1"
|
||||
>
|
||||
{showAllStudies ? (
|
||||
<>Show Less <ChevronUp className="w-4 h-4" /></>
|
||||
) : (
|
||||
<>Show All ({studies.length}) <ChevronDown className="w-4 h-4" /></>
|
||||
)}
|
||||
</button>
|
||||
)}
|
||||
{/* Aggregate Stats Cards */}
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-4 mb-8">
|
||||
<div className="bg-dark-800 rounded-xl p-4 border border-dark-600">
|
||||
<div className="flex items-center gap-2 text-dark-400 text-sm mb-2">
|
||||
<BarChart3 className="w-4 h-4" />
|
||||
Total Studies
|
||||
</div>
|
||||
<div className="text-3xl font-bold text-white">{aggregateStats.totalStudies}</div>
|
||||
</div>
|
||||
|
||||
{isLoading ? (
|
||||
<div className="flex items-center justify-center py-12 text-dark-400">
|
||||
<RefreshCw className="w-6 h-6 animate-spin mr-3" />
|
||||
Loading studies...
|
||||
<div className="bg-dark-800 rounded-xl p-4 border border-dark-600">
|
||||
<div className="flex items-center gap-2 text-green-400 text-sm mb-2">
|
||||
<Play className="w-4 h-4" />
|
||||
Running
|
||||
</div>
|
||||
) : studies.length === 0 ? (
|
||||
<div className="text-center py-12 text-dark-400">
|
||||
<FolderOpen className="w-12 h-12 mx-auto mb-3 opacity-50" />
|
||||
<p>No studies found. Create a new study to get started.</p>
|
||||
<div className="text-3xl font-bold text-green-400">{aggregateStats.runningStudies}</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-dark-800 rounded-xl p-4 border border-dark-600">
|
||||
<div className="flex items-center gap-2 text-dark-400 text-sm mb-2">
|
||||
<Activity className="w-4 h-4" />
|
||||
Total Trials
|
||||
</div>
|
||||
) : (
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||
{displayedStudies.map((study) => {
|
||||
const styles = getStatusStyles(study.status);
|
||||
const isSelected = selectedPreview?.id === study.id;
|
||||
<div className="text-3xl font-bold text-white">{aggregateStats.totalTrials.toLocaleString()}</div>
|
||||
</div>
|
||||
|
||||
return (
|
||||
<div
|
||||
key={study.id}
|
||||
onClick={() => setSelectedPreview(study)}
|
||||
className={`
|
||||
relative p-4 rounded-xl border cursor-pointer transition-all duration-200
|
||||
bg-dark-800 hover:bg-dark-750
|
||||
${styles.card} ${styles.glow}
|
||||
${isSelected ? 'ring-2 ring-primary-500 border-primary-500' : ''}
|
||||
`}
|
||||
>
|
||||
{/* Status Badge */}
|
||||
<div className="flex items-start justify-between mb-3">
|
||||
<div className="flex-1 min-w-0 pr-2">
|
||||
<h3 className="text-white font-medium truncate">{study.name || study.id}</h3>
|
||||
<p className="text-dark-500 text-xs truncate mt-0.5">{study.id}</p>
|
||||
</div>
|
||||
<span className={`flex items-center gap-1.5 px-2 py-1 text-xs font-medium rounded-full border ${styles.badge}`}>
|
||||
{getStatusIcon(study.status)}
|
||||
{study.status}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Stats */}
|
||||
<div className="flex items-center gap-4 text-sm mb-3">
|
||||
<div className="flex items-center gap-1.5 text-dark-400">
|
||||
<Activity className="w-3.5 h-3.5" />
|
||||
<span>{study.progress.current} trials</span>
|
||||
</div>
|
||||
{study.best_value !== null && (
|
||||
<div className="flex items-center gap-1.5 text-primary-400">
|
||||
<Target className="w-3.5 h-3.5" />
|
||||
<span>{study.best_value.toFixed(4)}</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Progress Bar */}
|
||||
<div className="h-1.5 bg-dark-700 rounded-full overflow-hidden">
|
||||
<div
|
||||
className={`h-full rounded-full transition-all duration-500 ${
|
||||
study.status === 'running' ? 'bg-green-500' :
|
||||
study.status === 'completed' ? 'bg-blue-500' : 'bg-primary-500'
|
||||
}`}
|
||||
style={{ width: `${Math.min((study.progress.current / study.progress.total) * 100, 100)}%` }}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Selected Indicator */}
|
||||
{isSelected && (
|
||||
<div className="absolute -bottom-px left-1/2 -translate-x-1/2 w-12 h-1 bg-primary-500 rounded-t-full" />
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
<div className="bg-dark-800 rounded-xl p-4 border border-dark-600">
|
||||
<div className="flex items-center gap-2 text-primary-400 text-sm mb-2">
|
||||
<Target className="w-4 h-4" />
|
||||
Best Overall
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
|
||||
{/* Study Documentation Section */}
|
||||
{selectedPreview && (
|
||||
<section className="animate-in fade-in slide-in-from-bottom-4 duration-300">
|
||||
{/* Documentation Header */}
|
||||
<div className="bg-dark-800 rounded-t-xl border border-dark-600 border-b-0">
|
||||
<div className="px-6 py-4 flex items-center justify-between">
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="w-10 h-10 bg-dark-700 rounded-lg flex items-center justify-center">
|
||||
<FileText className="w-5 h-5 text-primary-400" />
|
||||
</div>
|
||||
<div>
|
||||
<h2 className="text-xl font-semibold text-white">{selectedPreview.name || selectedPreview.id}</h2>
|
||||
<p className="text-dark-400 text-sm">Study Documentation</p>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => handleSelectStudy(selectedPreview)}
|
||||
className="flex items-center gap-2 px-5 py-2.5 bg-primary-600 hover:bg-primary-500
|
||||
text-white rounded-lg transition-all font-medium shadow-lg shadow-primary-500/20
|
||||
hover:shadow-primary-500/30"
|
||||
>
|
||||
Open Dashboard
|
||||
<ArrowRight className="w-4 h-4" />
|
||||
</button>
|
||||
<div className="text-2xl font-bold text-primary-400">
|
||||
{aggregateStats.bestOverall?.best_value !== null && aggregateStats.bestOverall?.best_value !== undefined
|
||||
? aggregateStats.bestOverall.best_value.toExponential(3)
|
||||
: 'N/A'}
|
||||
</div>
|
||||
{aggregateStats.bestOverall && (
|
||||
<div className="text-xs text-dark-400 mt-1 truncate">
|
||||
{aggregateStats.bestOverall.name || aggregateStats.bestOverall.id}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Two-column layout: Table + Preview */}
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||
{/* Study Table */}
|
||||
<div className="bg-dark-800 rounded-xl border border-dark-600 overflow-hidden">
|
||||
<div className="px-6 py-4 border-b border-dark-600 flex items-center justify-between">
|
||||
<h2 className="text-lg font-semibold text-white flex items-center gap-2">
|
||||
<TrendingUp className="w-5 h-5 text-primary-400" />
|
||||
Studies
|
||||
</h2>
|
||||
<span className="text-sm text-dark-400">{studies.length} studies</span>
|
||||
</div>
|
||||
|
||||
{/* README Content */}
|
||||
<div className="bg-dark-850 rounded-b-xl border border-dark-600 border-t-0 overflow-hidden">
|
||||
{readmeLoading ? (
|
||||
<div className="flex items-center justify-center py-16 text-dark-400">
|
||||
<RefreshCw className="w-6 h-6 animate-spin mr-3" />
|
||||
Loading documentation...
|
||||
</div>
|
||||
) : (
|
||||
<div className="p-8 overflow-x-auto">
|
||||
<article className="markdown-body max-w-none">
|
||||
<ReactMarkdown
|
||||
remarkPlugins={[remarkGfm, [remarkMath, { singleDollarTextMath: false }]]}
|
||||
rehypePlugins={[[rehypeKatex, { strict: false, trust: true, output: 'html' }]]}
|
||||
components={{
|
||||
// Custom heading styles
|
||||
h1: ({ children }) => (
|
||||
<h1 className="text-3xl font-bold text-white mb-6 pb-3 border-b border-dark-600">
|
||||
{children}
|
||||
</h1>
|
||||
),
|
||||
h2: ({ children }) => (
|
||||
<h2 className="text-2xl font-semibold text-white mt-10 mb-4 pb-2 border-b border-dark-700">
|
||||
{children}
|
||||
</h2>
|
||||
),
|
||||
h3: ({ children }) => (
|
||||
<h3 className="text-xl font-semibold text-white mt-8 mb-3">
|
||||
{children}
|
||||
</h3>
|
||||
),
|
||||
h4: ({ children }) => (
|
||||
<h4 className="text-lg font-medium text-white mt-6 mb-2">
|
||||
{children}
|
||||
</h4>
|
||||
),
|
||||
// Paragraphs
|
||||
p: ({ children }) => (
|
||||
<p className="text-dark-300 leading-relaxed mb-4">
|
||||
{children}
|
||||
</p>
|
||||
),
|
||||
// Strong/Bold
|
||||
strong: ({ children }) => (
|
||||
<strong className="text-white font-semibold">{children}</strong>
|
||||
),
|
||||
// Links
|
||||
a: ({ href, children }) => (
|
||||
<a
|
||||
href={href}
|
||||
className="text-primary-400 hover:text-primary-300 underline underline-offset-2"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
{children}
|
||||
</a>
|
||||
),
|
||||
// Lists
|
||||
ul: ({ children }) => (
|
||||
<ul className="list-disc list-inside text-dark-300 mb-4 space-y-1.5 ml-2">
|
||||
{children}
|
||||
</ul>
|
||||
),
|
||||
ol: ({ children }) => (
|
||||
<ol className="list-decimal list-inside text-dark-300 mb-4 space-y-1.5 ml-2">
|
||||
{children}
|
||||
</ol>
|
||||
),
|
||||
li: ({ children }) => (
|
||||
<li className="text-dark-300 leading-relaxed">{children}</li>
|
||||
),
|
||||
// Code blocks with syntax highlighting
|
||||
code: ({ inline, className, children, ...props }: any) => {
|
||||
const match = /language-(\w+)/.exec(className || '');
|
||||
const language = match ? match[1] : '';
|
||||
{isLoading ? (
|
||||
<div className="flex items-center justify-center py-16 text-dark-400">
|
||||
<RefreshCw className="w-6 h-6 animate-spin mr-3" />
|
||||
Loading studies...
|
||||
</div>
|
||||
) : studies.length === 0 ? (
|
||||
<div className="text-center py-16 text-dark-400">
|
||||
<BarChart3 className="w-12 h-12 mx-auto mb-4 opacity-50" />
|
||||
<p>No studies found</p>
|
||||
<p className="text-sm mt-1 text-dark-500">Create a new study to get started</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="overflow-x-auto max-h-[500px] overflow-y-auto">
|
||||
<table className="w-full">
|
||||
<thead className="sticky top-0 bg-dark-750 z-10">
|
||||
<tr className="border-b border-dark-600">
|
||||
<th
|
||||
className="text-left py-3 px-4 text-dark-400 font-medium cursor-pointer hover:text-white transition-colors"
|
||||
onClick={() => handleSort('name')}
|
||||
>
|
||||
<div className="flex items-center gap-1">
|
||||
Study Name
|
||||
{sortField === 'name' && (
|
||||
sortDir === 'asc' ? <ChevronUp className="w-4 h-4" /> : <ChevronDown className="w-4 h-4" />
|
||||
)}
|
||||
</div>
|
||||
</th>
|
||||
<th
|
||||
className="text-left py-3 px-4 text-dark-400 font-medium cursor-pointer hover:text-white transition-colors"
|
||||
onClick={() => handleSort('status')}
|
||||
>
|
||||
<div className="flex items-center gap-1">
|
||||
Status
|
||||
{sortField === 'status' && (
|
||||
sortDir === 'asc' ? <ChevronUp className="w-4 h-4" /> : <ChevronDown className="w-4 h-4" />
|
||||
)}
|
||||
</div>
|
||||
</th>
|
||||
<th
|
||||
className="text-left py-3 px-4 text-dark-400 font-medium cursor-pointer hover:text-white transition-colors"
|
||||
onClick={() => handleSort('trials')}
|
||||
>
|
||||
<div className="flex items-center gap-1">
|
||||
Progress
|
||||
{sortField === 'trials' && (
|
||||
sortDir === 'asc' ? <ChevronUp className="w-4 h-4" /> : <ChevronDown className="w-4 h-4" />
|
||||
)}
|
||||
</div>
|
||||
</th>
|
||||
<th
|
||||
className="text-left py-3 px-4 text-dark-400 font-medium cursor-pointer hover:text-white transition-colors"
|
||||
onClick={() => handleSort('bestValue')}
|
||||
>
|
||||
<div className="flex items-center gap-1">
|
||||
Best
|
||||
{sortField === 'bestValue' && (
|
||||
sortDir === 'asc' ? <ChevronUp className="w-4 h-4" /> : <ChevronDown className="w-4 h-4" />
|
||||
)}
|
||||
</div>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{sortedStudies.map((study) => {
|
||||
const completionPercent = study.progress.total > 0
|
||||
? Math.round((study.progress.current / study.progress.total) * 100)
|
||||
: 0;
|
||||
|
||||
if (!inline && language) {
|
||||
return (
|
||||
<div className="my-4 rounded-lg overflow-hidden border border-dark-600">
|
||||
<div className="bg-dark-700 px-4 py-2 text-xs text-dark-400 font-mono border-b border-dark-600">
|
||||
{language}
|
||||
</div>
|
||||
<SyntaxHighlighter
|
||||
style={oneDark}
|
||||
language={language}
|
||||
PreTag="div"
|
||||
customStyle={{
|
||||
margin: 0,
|
||||
padding: '1rem',
|
||||
background: '#1a1d23',
|
||||
fontSize: '0.875rem',
|
||||
}}
|
||||
{...props}
|
||||
>
|
||||
{String(children).replace(/\n$/, '')}
|
||||
</SyntaxHighlighter>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (!inline) {
|
||||
return (
|
||||
<pre className="my-4 p-4 bg-dark-700 rounded-lg border border-dark-600 overflow-x-auto">
|
||||
<code className="text-primary-400 text-sm font-mono">{children}</code>
|
||||
</pre>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<code className="px-1.5 py-0.5 bg-dark-700 text-primary-400 rounded text-sm font-mono">
|
||||
{children}
|
||||
</code>
|
||||
);
|
||||
},
|
||||
// Tables
|
||||
table: ({ children }) => (
|
||||
<div className="my-6 overflow-x-auto rounded-lg border border-dark-600">
|
||||
<table className="w-full text-sm">
|
||||
{children}
|
||||
</table>
|
||||
</div>
|
||||
),
|
||||
thead: ({ children }) => (
|
||||
<thead className="bg-dark-700 text-white">
|
||||
{children}
|
||||
</thead>
|
||||
),
|
||||
tbody: ({ children }) => (
|
||||
<tbody className="divide-y divide-dark-600">
|
||||
{children}
|
||||
</tbody>
|
||||
),
|
||||
tr: ({ children }) => (
|
||||
<tr className="hover:bg-dark-750 transition-colors">
|
||||
{children}
|
||||
</tr>
|
||||
),
|
||||
th: ({ children }) => (
|
||||
<th className="px-4 py-3 text-left font-semibold text-white border-b border-dark-600">
|
||||
{children}
|
||||
</th>
|
||||
),
|
||||
td: ({ children }) => (
|
||||
<td className="px-4 py-3 text-dark-300">
|
||||
{children}
|
||||
return (
|
||||
<tr
|
||||
key={study.id}
|
||||
onClick={() => setSelectedPreview(study)}
|
||||
className={`border-b border-dark-700 hover:bg-dark-750 transition-colors cursor-pointer ${
|
||||
selectedPreview?.id === study.id ? 'bg-primary-900/20' : ''
|
||||
}`}
|
||||
>
|
||||
<td className="py-3 px-4">
|
||||
<div className="flex flex-col">
|
||||
<span className="text-white font-medium truncate max-w-[200px]">
|
||||
{study.name || study.id}
|
||||
</span>
|
||||
{study.name && (
|
||||
<span className="text-xs text-dark-500 truncate max-w-[200px]">{study.id}</span>
|
||||
)}
|
||||
</div>
|
||||
</td>
|
||||
),
|
||||
// Blockquotes
|
||||
blockquote: ({ children }) => (
|
||||
<blockquote className="my-4 pl-4 border-l-4 border-primary-500 bg-dark-750 py-3 pr-4 rounded-r-lg">
|
||||
{children}
|
||||
</blockquote>
|
||||
),
|
||||
// Horizontal rules
|
||||
hr: () => (
|
||||
<hr className="my-8 border-dark-600" />
|
||||
),
|
||||
// Images
|
||||
img: ({ src, alt }) => (
|
||||
<img
|
||||
src={src}
|
||||
alt={alt}
|
||||
className="my-4 rounded-lg max-w-full h-auto border border-dark-600"
|
||||
/>
|
||||
),
|
||||
}}
|
||||
>
|
||||
{readme}
|
||||
</ReactMarkdown>
|
||||
</article>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</section>
|
||||
)}
|
||||
<td className="py-3 px-4">
|
||||
<span className={`inline-flex items-center gap-1.5 px-2 py-1 rounded-full text-xs font-medium ${getStatusColor(study.status)}`}>
|
||||
{getStatusIcon(study.status)}
|
||||
{study.status}
|
||||
</span>
|
||||
</td>
|
||||
<td className="py-3 px-4">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="flex-1 h-2 bg-dark-600 rounded-full overflow-hidden max-w-[80px]">
|
||||
<div
|
||||
className={`h-full transition-all ${
|
||||
completionPercent >= 100 ? 'bg-green-500' :
|
||||
completionPercent >= 50 ? 'bg-primary-500' :
|
||||
'bg-yellow-500'
|
||||
}`}
|
||||
style={{ width: `${Math.min(completionPercent, 100)}%` }}
|
||||
/>
|
||||
</div>
|
||||
<span className="text-dark-400 text-sm font-mono w-16">
|
||||
{study.progress.current}/{study.progress.total}
|
||||
</span>
|
||||
</div>
|
||||
</td>
|
||||
<td className="py-3 px-4">
|
||||
<span className={`font-mono text-sm ${study.best_value !== null ? 'text-primary-400' : 'text-dark-500'}`}>
|
||||
{study.best_value !== null ? study.best_value.toExponential(3) : 'N/A'}
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Empty State when no study selected */}
|
||||
{!selectedPreview && studies.length > 0 && (
|
||||
<section className="flex items-center justify-center py-16 text-dark-400">
|
||||
<div className="text-center">
|
||||
<FileText className="w-16 h-16 mx-auto mb-4 opacity-30" />
|
||||
<p className="text-lg">Select a study to view its documentation</p>
|
||||
<p className="text-sm mt-1 text-dark-500">Click on any study card above</p>
|
||||
</div>
|
||||
</section>
|
||||
)}
|
||||
{/* Study Preview */}
|
||||
<div className="bg-dark-800 rounded-xl border border-dark-600 overflow-hidden flex flex-col">
|
||||
{selectedPreview ? (
|
||||
<>
|
||||
{/* Preview Header */}
|
||||
<div className="px-6 py-4 border-b border-dark-600 flex items-center justify-between">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-10 h-10 bg-dark-700 rounded-lg flex items-center justify-center">
|
||||
<FileText className="w-5 h-5 text-primary-400" />
|
||||
</div>
|
||||
<div className="min-w-0">
|
||||
<h2 className="text-lg font-semibold text-white truncate">
|
||||
{selectedPreview.name || selectedPreview.id}
|
||||
</h2>
|
||||
<p className="text-dark-400 text-sm">Study Documentation</p>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => handleSelectStudy(selectedPreview)}
|
||||
className="flex items-center gap-2 px-4 py-2 bg-primary-600 hover:bg-primary-500
|
||||
text-white rounded-lg transition-all font-medium shadow-lg shadow-primary-500/20
|
||||
hover:shadow-primary-500/30 whitespace-nowrap"
|
||||
>
|
||||
Open
|
||||
<ArrowRight className="w-4 h-4" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Study Quick Stats */}
|
||||
<div className="px-6 py-3 border-b border-dark-600 flex items-center gap-6 text-sm">
|
||||
<div className="flex items-center gap-2">
|
||||
{getStatusIcon(selectedPreview.status)}
|
||||
<span className="text-dark-300 capitalize">{selectedPreview.status}</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 text-dark-400">
|
||||
<Activity className="w-4 h-4" />
|
||||
<span>{selectedPreview.progress.current} / {selectedPreview.progress.total} trials</span>
|
||||
</div>
|
||||
{selectedPreview.best_value !== null && (
|
||||
<div className="flex items-center gap-2 text-primary-400">
|
||||
<Target className="w-4 h-4" />
|
||||
<span>Best: {selectedPreview.best_value.toExponential(4)}</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* README Content */}
|
||||
<div className="flex-1 overflow-y-auto p-6">
|
||||
{readmeLoading ? (
|
||||
<div className="flex items-center justify-center py-16 text-dark-400">
|
||||
<RefreshCw className="w-6 h-6 animate-spin mr-3" />
|
||||
Loading documentation...
|
||||
</div>
|
||||
) : (
|
||||
<MarkdownRenderer content={readme} />
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<div className="flex-1 flex items-center justify-center text-dark-400">
|
||||
<div className="text-center">
|
||||
<FileText className="w-16 h-16 mx-auto mb-4 opacity-30" />
|
||||
<p className="text-lg">Select a study to preview</p>
|
||||
<p className="text-sm mt-1 text-dark-500">Click on any row in the table</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -10,14 +10,45 @@ import {
|
||||
Loader2,
|
||||
AlertTriangle,
|
||||
CheckCircle,
|
||||
Copy
|
||||
Copy,
|
||||
Trophy,
|
||||
TrendingUp,
|
||||
FileJson,
|
||||
FileSpreadsheet,
|
||||
Settings,
|
||||
ArrowRight,
|
||||
ChevronDown,
|
||||
ChevronUp,
|
||||
Printer
|
||||
} from 'lucide-react';
|
||||
import { apiClient } from '../api/client';
|
||||
import { useStudy } from '../context/StudyContext';
|
||||
import ReactMarkdown from 'react-markdown';
|
||||
import { MarkdownRenderer } from '../components/MarkdownRenderer';
|
||||
|
||||
interface BestSolution {
|
||||
best_trial: {
|
||||
trial_number: number;
|
||||
objective: number;
|
||||
design_variables: Record<string, number>;
|
||||
user_attrs?: Record<string, any>;
|
||||
timestamp?: string;
|
||||
} | null;
|
||||
first_trial: {
|
||||
trial_number: number;
|
||||
objective: number;
|
||||
design_variables: Record<string, number>;
|
||||
} | null;
|
||||
improvements: Record<string, {
|
||||
initial: number;
|
||||
final: number;
|
||||
improvement_pct: number;
|
||||
absolute_change: number;
|
||||
}>;
|
||||
total_trials: number;
|
||||
}
|
||||
|
||||
export default function Results() {
|
||||
const { selectedStudy } = useStudy();
|
||||
const { selectedStudy, isInitialized } = useStudy();
|
||||
const navigate = useNavigate();
|
||||
const [reportContent, setReportContent] = useState<string | null>(null);
|
||||
const [loading, setLoading] = useState(false);
|
||||
@@ -25,21 +56,37 @@ export default function Results() {
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [copied, setCopied] = useState(false);
|
||||
const [lastGenerated, setLastGenerated] = useState<string | null>(null);
|
||||
const [bestSolution, setBestSolution] = useState<BestSolution | null>(null);
|
||||
const [showAllParams, setShowAllParams] = useState(false);
|
||||
const [exporting, setExporting] = useState<string | null>(null);
|
||||
|
||||
// Redirect if no study selected
|
||||
// Redirect if no study selected (but only after initialization completes)
|
||||
useEffect(() => {
|
||||
if (!selectedStudy) {
|
||||
if (isInitialized && !selectedStudy) {
|
||||
navigate('/');
|
||||
}
|
||||
}, [selectedStudy, navigate]);
|
||||
}, [selectedStudy, navigate, isInitialized]);
|
||||
|
||||
// Load report when study changes
|
||||
// Load report and best solution when study changes
|
||||
useEffect(() => {
|
||||
if (selectedStudy) {
|
||||
loadReport();
|
||||
loadBestSolution();
|
||||
}
|
||||
}, [selectedStudy]);
|
||||
|
||||
// Show loading state while initializing (must be after all hooks)
|
||||
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>
|
||||
);
|
||||
}
|
||||
|
||||
const loadReport = async () => {
|
||||
if (!selectedStudy) return;
|
||||
|
||||
@@ -52,7 +99,7 @@ export default function Results() {
|
||||
if (data.generated_at) {
|
||||
setLastGenerated(data.generated_at);
|
||||
}
|
||||
} catch (err: any) {
|
||||
} catch {
|
||||
// No report yet - show placeholder
|
||||
setReportContent(null);
|
||||
} finally {
|
||||
@@ -60,6 +107,17 @@ export default function Results() {
|
||||
}
|
||||
};
|
||||
|
||||
const loadBestSolution = async () => {
|
||||
if (!selectedStudy) return;
|
||||
|
||||
try {
|
||||
const data = await apiClient.getBestSolution(selectedStudy.id);
|
||||
setBestSolution(data);
|
||||
} catch {
|
||||
setBestSolution(null);
|
||||
}
|
||||
};
|
||||
|
||||
const handleGenerate = async () => {
|
||||
if (!selectedStudy) return;
|
||||
|
||||
@@ -101,17 +159,148 @@ export default function Results() {
|
||||
URL.revokeObjectURL(url);
|
||||
};
|
||||
|
||||
const handlePrintPDF = () => {
|
||||
if (!reportContent || !selectedStudy) return;
|
||||
|
||||
// Create a printable version of the report
|
||||
const printWindow = window.open('', '_blank');
|
||||
if (!printWindow) {
|
||||
setError('Pop-up blocked. Please allow pop-ups to print PDF.');
|
||||
return;
|
||||
}
|
||||
|
||||
printWindow.document.write(`
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>${selectedStudy.name} - Optimization Report</title>
|
||||
<style>
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
padding: 40px;
|
||||
color: #1a1a1a;
|
||||
line-height: 1.6;
|
||||
}
|
||||
h1 { color: #2563eb; border-bottom: 2px solid #2563eb; padding-bottom: 10px; }
|
||||
h2 { color: #1e40af; margin-top: 30px; }
|
||||
h3 { color: #3730a3; }
|
||||
table { border-collapse: collapse; width: 100%; margin: 20px 0; }
|
||||
th, td { border: 1px solid #ddd; padding: 8px 12px; text-align: left; }
|
||||
th { background: #f3f4f6; font-weight: 600; }
|
||||
tr:nth-child(even) { background: #f9fafb; }
|
||||
code { background: #f3f4f6; padding: 2px 6px; border-radius: 4px; font-family: 'Monaco', monospace; }
|
||||
pre { background: #1e1e1e; color: #d4d4d4; padding: 16px; border-radius: 8px; overflow-x: auto; }
|
||||
pre code { background: transparent; padding: 0; }
|
||||
blockquote { border-left: 4px solid #2563eb; margin: 20px 0; padding: 10px 20px; background: #eff6ff; }
|
||||
.header-info { color: #666; margin-bottom: 30px; }
|
||||
@media print {
|
||||
body { padding: 20px; }
|
||||
pre { white-space: pre-wrap; word-wrap: break-word; }
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="header-info">
|
||||
<strong>Study:</strong> ${selectedStudy.name}<br>
|
||||
<strong>Generated:</strong> ${new Date().toLocaleString()}<br>
|
||||
<strong>Trials:</strong> ${selectedStudy.progress.current} / ${selectedStudy.progress.total}
|
||||
</div>
|
||||
${convertMarkdownToHTML(reportContent)}
|
||||
</body>
|
||||
</html>
|
||||
`);
|
||||
printWindow.document.close();
|
||||
|
||||
// Wait for content to load then print
|
||||
printWindow.onload = () => {
|
||||
printWindow.print();
|
||||
};
|
||||
};
|
||||
|
||||
// Simple markdown to HTML converter for print
|
||||
const convertMarkdownToHTML = (md: string): string => {
|
||||
return md
|
||||
// Headers
|
||||
.replace(/^### (.*$)/gm, '<h3>$1</h3>')
|
||||
.replace(/^## (.*$)/gm, '<h2>$1</h2>')
|
||||
.replace(/^# (.*$)/gm, '<h1>$1</h1>')
|
||||
// Bold and italic
|
||||
.replace(/\*\*\*(.*?)\*\*\*/g, '<strong><em>$1</em></strong>')
|
||||
.replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>')
|
||||
.replace(/\*(.*?)\*/g, '<em>$1</em>')
|
||||
// Code blocks
|
||||
.replace(/```(\w*)\n([\s\S]*?)```/g, '<pre><code>$2</code></pre>')
|
||||
.replace(/`([^`]+)`/g, '<code>$1</code>')
|
||||
// Lists
|
||||
.replace(/^\s*[-*]\s+(.*)$/gm, '<li>$1</li>')
|
||||
.replace(/(<li>.*<\/li>)\n(?!<li>)/g, '</ul>$1\n')
|
||||
.replace(/(?<!<\/ul>)(<li>)/g, '<ul>$1')
|
||||
// Blockquotes
|
||||
.replace(/^>\s*(.*)$/gm, '<blockquote>$1</blockquote>')
|
||||
// Horizontal rules
|
||||
.replace(/^---$/gm, '<hr>')
|
||||
// Paragraphs
|
||||
.replace(/\n\n/g, '</p><p>')
|
||||
.replace(/^(.+)$/gm, (match) => {
|
||||
if (match.startsWith('<')) return match;
|
||||
return match;
|
||||
});
|
||||
};
|
||||
|
||||
const handleExport = async (format: 'csv' | 'json' | 'config') => {
|
||||
if (!selectedStudy) return;
|
||||
|
||||
setExporting(format);
|
||||
try {
|
||||
const data = await apiClient.exportData(selectedStudy.id, format);
|
||||
|
||||
if (data.filename && data.content) {
|
||||
const blob = new Blob([data.content], { type: data.content_type || 'text/plain' });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = data.filename;
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
document.body.removeChild(a);
|
||||
URL.revokeObjectURL(url);
|
||||
} else if (format === 'json' && data.trials) {
|
||||
// Direct JSON response
|
||||
const blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = `${selectedStudy.id}_data.json`;
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
document.body.removeChild(a);
|
||||
URL.revokeObjectURL(url);
|
||||
}
|
||||
} catch (err: any) {
|
||||
setError(err.message || `Failed to export ${format}`);
|
||||
} finally {
|
||||
setExporting(null);
|
||||
}
|
||||
};
|
||||
|
||||
if (!selectedStudy) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const paramEntries = bestSolution?.best_trial?.design_variables
|
||||
? Object.entries(bestSolution.best_trial.design_variables)
|
||||
: [];
|
||||
const visibleParams = showAllParams ? paramEntries : paramEntries.slice(0, 6);
|
||||
|
||||
return (
|
||||
<div className="h-full flex flex-col">
|
||||
<div className="h-full flex flex-col max-w-[2400px] mx-auto px-4">
|
||||
{/* Header */}
|
||||
<header className="mb-6 flex items-center justify-between">
|
||||
<header className="mb-6 flex items-center justify-between border-b border-dark-600 pb-4">
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold text-white">Optimization Report</h1>
|
||||
<p className="text-dark-400 mt-1">{selectedStudy.name}</p>
|
||||
<h1 className="text-2xl font-bold text-primary-400">Results</h1>
|
||||
<p className="text-dark-400 text-sm">{selectedStudy.name}</p>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<Button
|
||||
@@ -153,7 +342,156 @@ export default function Results() {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Main Content */}
|
||||
{/* Best Solution Card */}
|
||||
{bestSolution?.best_trial && (
|
||||
<Card className="mb-6">
|
||||
<div className="flex items-center gap-3 mb-4">
|
||||
<div className="w-10 h-10 rounded-full bg-yellow-500/20 flex items-center justify-center">
|
||||
<Trophy className="w-5 h-5 text-yellow-400" />
|
||||
</div>
|
||||
<div>
|
||||
<h2 className="text-lg font-semibold text-white">Best Solution</h2>
|
||||
<p className="text-sm text-dark-400">Trial #{bestSolution.best_trial.trial_number} of {bestSolution.total_trials}</p>
|
||||
</div>
|
||||
{bestSolution.improvements.objective && (
|
||||
<div className="ml-auto flex items-center gap-2 px-4 py-2 bg-green-900/20 rounded-lg border border-green-800/30">
|
||||
<TrendingUp className="w-5 h-5 text-green-400" />
|
||||
<span className="text-green-400 font-bold text-lg">
|
||||
{bestSolution.improvements.objective.improvement_pct > 0 ? '+' : ''}
|
||||
{bestSolution.improvements.objective.improvement_pct.toFixed(1)}%
|
||||
</span>
|
||||
<span className="text-dark-400 text-sm">improvement</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Objective Value */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4 mb-4">
|
||||
<div className="bg-dark-700 rounded-lg p-4">
|
||||
<div className="text-xs text-dark-400 uppercase mb-1">Best Objective</div>
|
||||
<div className="text-2xl font-bold text-primary-400">
|
||||
{bestSolution.best_trial.objective.toExponential(4)}
|
||||
</div>
|
||||
</div>
|
||||
{bestSolution.first_trial && (
|
||||
<div className="bg-dark-700 rounded-lg p-4">
|
||||
<div className="text-xs text-dark-400 uppercase mb-1">Initial Value</div>
|
||||
<div className="text-2xl font-bold text-dark-300">
|
||||
{bestSolution.first_trial.objective.toExponential(4)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{bestSolution.improvements.objective && (
|
||||
<div className="bg-dark-700 rounded-lg p-4">
|
||||
<div className="text-xs text-dark-400 uppercase mb-1">Absolute Change</div>
|
||||
<div className="text-2xl font-bold text-green-400 flex items-center gap-2">
|
||||
<ArrowRight className="w-5 h-5" />
|
||||
{bestSolution.improvements.objective.absolute_change.toExponential(4)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Design Variables */}
|
||||
<div className="border-t border-dark-600 pt-4">
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<h3 className="text-sm font-medium text-dark-300">Optimal Design Variables</h3>
|
||||
{paramEntries.length > 6 && (
|
||||
<button
|
||||
onClick={() => setShowAllParams(!showAllParams)}
|
||||
className="text-xs text-primary-400 hover:text-primary-300 flex items-center gap-1"
|
||||
>
|
||||
{showAllParams ? (
|
||||
<>Show Less <ChevronUp className="w-3 h-3" /></>
|
||||
) : (
|
||||
<>Show All ({paramEntries.length}) <ChevronDown className="w-3 h-3" /></>
|
||||
)}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
<div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-6 gap-3">
|
||||
{visibleParams.map(([name, value]) => (
|
||||
<div key={name} className="bg-dark-800 rounded px-3 py-2">
|
||||
<div className="text-xs text-dark-400 truncate" title={name}>{name}</div>
|
||||
<div className="text-sm font-mono text-white">
|
||||
{typeof value === 'number' ? value.toFixed(4) : value}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{/* Export Options */}
|
||||
<Card className="mb-6">
|
||||
<h3 className="text-lg font-semibold text-white mb-4 flex items-center gap-2">
|
||||
<Download className="w-5 h-5 text-primary-400" />
|
||||
Export Data
|
||||
</h3>
|
||||
<div className="grid grid-cols-2 md:grid-cols-5 gap-4">
|
||||
<button
|
||||
onClick={() => handleExport('csv')}
|
||||
disabled={exporting !== null}
|
||||
className="flex items-center gap-3 p-4 bg-dark-700 hover:bg-dark-600 rounded-lg border border-dark-600 hover:border-dark-500 transition-colors disabled:opacity-50"
|
||||
>
|
||||
<FileSpreadsheet className="w-8 h-8 text-green-400" />
|
||||
<div className="text-left">
|
||||
<div className="text-sm font-medium text-white">CSV</div>
|
||||
<div className="text-xs text-dark-400">Spreadsheet</div>
|
||||
</div>
|
||||
{exporting === 'csv' && <Loader2 className="w-4 h-4 animate-spin ml-auto" />}
|
||||
</button>
|
||||
<button
|
||||
onClick={() => handleExport('json')}
|
||||
disabled={exporting !== null}
|
||||
className="flex items-center gap-3 p-4 bg-dark-700 hover:bg-dark-600 rounded-lg border border-dark-600 hover:border-dark-500 transition-colors disabled:opacity-50"
|
||||
>
|
||||
<FileJson className="w-8 h-8 text-blue-400" />
|
||||
<div className="text-left">
|
||||
<div className="text-sm font-medium text-white">JSON</div>
|
||||
<div className="text-xs text-dark-400">Full data</div>
|
||||
</div>
|
||||
{exporting === 'json' && <Loader2 className="w-4 h-4 animate-spin ml-auto" />}
|
||||
</button>
|
||||
<button
|
||||
onClick={() => handleExport('config')}
|
||||
disabled={exporting !== null}
|
||||
className="flex items-center gap-3 p-4 bg-dark-700 hover:bg-dark-600 rounded-lg border border-dark-600 hover:border-dark-500 transition-colors disabled:opacity-50"
|
||||
>
|
||||
<Settings className="w-8 h-8 text-purple-400" />
|
||||
<div className="text-left">
|
||||
<div className="text-sm font-medium text-white">Config</div>
|
||||
<div className="text-xs text-dark-400">Settings</div>
|
||||
</div>
|
||||
{exporting === 'config' && <Loader2 className="w-4 h-4 animate-spin ml-auto" />}
|
||||
</button>
|
||||
<button
|
||||
onClick={handleDownload}
|
||||
disabled={!reportContent}
|
||||
className="flex items-center gap-3 p-4 bg-dark-700 hover:bg-dark-600 rounded-lg border border-dark-600 hover:border-dark-500 transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
>
|
||||
<FileText className="w-8 h-8 text-orange-400" />
|
||||
<div className="text-left">
|
||||
<div className="text-sm font-medium text-white">Report</div>
|
||||
<div className="text-xs text-dark-400">Markdown</div>
|
||||
</div>
|
||||
</button>
|
||||
<button
|
||||
onClick={handlePrintPDF}
|
||||
disabled={!reportContent}
|
||||
className="flex items-center gap-3 p-4 bg-dark-700 hover:bg-dark-600 rounded-lg border border-dark-600 hover:border-dark-500 transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
>
|
||||
<Printer className="w-8 h-8 text-red-400" />
|
||||
<div className="text-left">
|
||||
<div className="text-sm font-medium text-white">PDF</div>
|
||||
<div className="text-xs text-dark-400">Print report</div>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
{/* Main Content - Report */}
|
||||
<div className="flex-1 min-h-0">
|
||||
<Card className="h-full overflow-hidden flex flex-col">
|
||||
<div className="flex items-center justify-between border-b border-dark-600 pb-4 mb-4">
|
||||
@@ -175,18 +513,8 @@ export default function Results() {
|
||||
<span>Loading report...</span>
|
||||
</div>
|
||||
) : reportContent ? (
|
||||
<div className="prose prose-invert prose-sm max-w-none
|
||||
prose-headings:text-white prose-headings:font-semibold
|
||||
prose-p:text-dark-300 prose-strong:text-white
|
||||
prose-code:text-primary-400 prose-code:bg-dark-700 prose-code:px-1 prose-code:rounded
|
||||
prose-pre:bg-dark-700 prose-pre:border prose-pre:border-dark-600
|
||||
prose-a:text-primary-400 prose-a:no-underline hover:prose-a:underline
|
||||
prose-ul:text-dark-300 prose-ol:text-dark-300
|
||||
prose-li:text-dark-300
|
||||
prose-table:border-collapse prose-th:border prose-th:border-dark-600 prose-th:p-2 prose-th:bg-dark-700
|
||||
prose-td:border prose-td:border-dark-600 prose-td:p-2
|
||||
prose-hr:border-dark-600">
|
||||
<ReactMarkdown>{reportContent}</ReactMarkdown>
|
||||
<div className="p-2">
|
||||
<MarkdownRenderer content={reportContent} />
|
||||
</div>
|
||||
) : (
|
||||
<div className="h-full flex flex-col items-center justify-center text-dark-400">
|
||||
|
||||
780
atomizer-dashboard/frontend/src/pages/Setup.tsx
Normal file
780
atomizer-dashboard/frontend/src/pages/Setup.tsx
Normal file
@@ -0,0 +1,780 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import {
|
||||
Settings,
|
||||
Target,
|
||||
Sliders,
|
||||
AlertTriangle,
|
||||
Cpu,
|
||||
Box,
|
||||
Layers,
|
||||
Play,
|
||||
Download,
|
||||
RefreshCw,
|
||||
ChevronDown,
|
||||
ChevronUp,
|
||||
ArrowUp,
|
||||
ArrowDown,
|
||||
CheckCircle,
|
||||
Info,
|
||||
FileBox,
|
||||
FolderOpen,
|
||||
File
|
||||
} from 'lucide-react';
|
||||
import { useStudy } from '../context/StudyContext';
|
||||
import { Card } from '../components/common/Card';
|
||||
import { Button } from '../components/common/Button';
|
||||
import { apiClient, ModelFile } from '../api/client';
|
||||
|
||||
interface StudyConfig {
|
||||
study_name: string;
|
||||
description?: string;
|
||||
objectives: {
|
||||
name: string;
|
||||
direction: 'minimize' | 'maximize';
|
||||
unit?: string;
|
||||
target?: number;
|
||||
weight?: number;
|
||||
}[];
|
||||
design_variables: {
|
||||
name: string;
|
||||
type: 'float' | 'int' | 'categorical';
|
||||
low?: number;
|
||||
high?: number;
|
||||
step?: number;
|
||||
choices?: string[];
|
||||
unit?: string;
|
||||
}[];
|
||||
constraints: {
|
||||
name: string;
|
||||
type: 'le' | 'ge' | 'eq';
|
||||
bound: number;
|
||||
unit?: string;
|
||||
}[];
|
||||
algorithm: {
|
||||
name: string;
|
||||
sampler: string;
|
||||
pruner?: string;
|
||||
n_trials: number;
|
||||
timeout?: number;
|
||||
};
|
||||
fea_model?: {
|
||||
software: string;
|
||||
solver: string;
|
||||
sim_file?: string;
|
||||
mesh_elements?: number;
|
||||
};
|
||||
extractors?: {
|
||||
name: string;
|
||||
type: string;
|
||||
source?: string;
|
||||
}[];
|
||||
}
|
||||
|
||||
export default function Setup() {
|
||||
const navigate = useNavigate();
|
||||
const { selectedStudy, isInitialized } = useStudy();
|
||||
const [config, setConfig] = useState<StudyConfig | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [expandedSections, setExpandedSections] = useState<Set<string>>(
|
||||
new Set(['objectives', 'variables', 'constraints', 'algorithm', 'modelFiles'])
|
||||
);
|
||||
const [modelFiles, setModelFiles] = useState<ModelFile[]>([]);
|
||||
const [modelDir, setModelDir] = useState<string>('');
|
||||
|
||||
// Redirect if no study selected
|
||||
useEffect(() => {
|
||||
if (isInitialized && !selectedStudy) {
|
||||
navigate('/');
|
||||
}
|
||||
}, [selectedStudy, navigate, isInitialized]);
|
||||
|
||||
// Load study configuration
|
||||
useEffect(() => {
|
||||
if (selectedStudy) {
|
||||
loadConfig();
|
||||
loadModelFiles();
|
||||
}
|
||||
}, [selectedStudy]);
|
||||
|
||||
const loadModelFiles = async () => {
|
||||
if (!selectedStudy) return;
|
||||
try {
|
||||
const data = await apiClient.getModelFiles(selectedStudy.id);
|
||||
setModelFiles(data.files);
|
||||
setModelDir(data.model_dir);
|
||||
} catch (err) {
|
||||
console.error('Failed to load model files:', err);
|
||||
}
|
||||
};
|
||||
|
||||
const handleOpenFolder = async () => {
|
||||
if (!selectedStudy) return;
|
||||
try {
|
||||
await apiClient.openFolder(selectedStudy.id, 'model');
|
||||
} catch (err: any) {
|
||||
setError(err.message || 'Failed to open folder');
|
||||
}
|
||||
};
|
||||
|
||||
const loadConfig = async () => {
|
||||
if (!selectedStudy) return;
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
|
||||
try {
|
||||
const response = await apiClient.getStudyConfig(selectedStudy.id);
|
||||
const rawConfig = response.config;
|
||||
|
||||
// Transform backend config format to our StudyConfig format
|
||||
const transformedConfig: StudyConfig = {
|
||||
study_name: rawConfig.study_name || selectedStudy.name || selectedStudy.id,
|
||||
description: rawConfig.description,
|
||||
objectives: (rawConfig.objectives || []).map((obj: any) => ({
|
||||
name: obj.name,
|
||||
direction: obj.direction || 'minimize',
|
||||
unit: obj.unit || obj.units,
|
||||
target: obj.target,
|
||||
weight: obj.weight
|
||||
})),
|
||||
design_variables: (rawConfig.design_variables || []).map((dv: any) => ({
|
||||
name: dv.name,
|
||||
type: dv.type || 'float',
|
||||
low: dv.min ?? dv.low,
|
||||
high: dv.max ?? dv.high,
|
||||
step: dv.step,
|
||||
choices: dv.choices,
|
||||
unit: dv.unit || dv.units
|
||||
})),
|
||||
constraints: (rawConfig.constraints || []).map((c: any) => ({
|
||||
name: c.name,
|
||||
type: c.type || 'le',
|
||||
bound: c.max_value ?? c.min_value ?? c.bound ?? 0,
|
||||
unit: c.unit || c.units
|
||||
})),
|
||||
algorithm: {
|
||||
name: rawConfig.optimizer?.name || rawConfig.algorithm?.name || 'Optuna',
|
||||
sampler: rawConfig.optimization_settings?.sampler || rawConfig.algorithm?.sampler || 'TPESampler',
|
||||
pruner: rawConfig.optimization_settings?.pruner || rawConfig.algorithm?.pruner,
|
||||
n_trials: rawConfig.optimization_settings?.n_trials || rawConfig.trials?.n_trials || selectedStudy.progress.total,
|
||||
timeout: rawConfig.optimization_settings?.timeout
|
||||
},
|
||||
fea_model: rawConfig.fea_model || rawConfig.solver ? {
|
||||
software: rawConfig.fea_model?.software || rawConfig.solver?.type || 'NX Nastran',
|
||||
solver: rawConfig.fea_model?.solver || rawConfig.solver?.name || 'SOL 103',
|
||||
sim_file: rawConfig.sim_file || rawConfig.fea_model?.sim_file,
|
||||
mesh_elements: rawConfig.fea_model?.mesh_elements
|
||||
} : undefined,
|
||||
extractors: rawConfig.extractors
|
||||
};
|
||||
|
||||
setConfig(transformedConfig);
|
||||
} catch (err: any) {
|
||||
// If no config endpoint, create mock from available data
|
||||
setConfig({
|
||||
study_name: selectedStudy.name || selectedStudy.id,
|
||||
objectives: [{ name: 'objective', direction: 'minimize' }],
|
||||
design_variables: [],
|
||||
constraints: [],
|
||||
algorithm: {
|
||||
name: 'Optuna',
|
||||
sampler: 'TPESampler',
|
||||
n_trials: selectedStudy.progress.total
|
||||
}
|
||||
});
|
||||
setError('Configuration loaded with limited data');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const toggleSection = (section: string) => {
|
||||
setExpandedSections(prev => {
|
||||
const next = new Set(prev);
|
||||
if (next.has(section)) {
|
||||
next.delete(section);
|
||||
} else {
|
||||
next.add(section);
|
||||
}
|
||||
return next;
|
||||
});
|
||||
};
|
||||
|
||||
const handleStartOptimization = async () => {
|
||||
if (!selectedStudy) return;
|
||||
try {
|
||||
await apiClient.startOptimization(selectedStudy.id);
|
||||
navigate('/dashboard');
|
||||
} catch (err: any) {
|
||||
setError(err.message || 'Failed to start optimization');
|
||||
}
|
||||
};
|
||||
|
||||
const handleExportConfig = () => {
|
||||
if (!config) return;
|
||||
const blob = new Blob([JSON.stringify(config, null, 2)], { type: 'application/json' });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = `${selectedStudy?.id || 'study'}_config.json`;
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
document.body.removeChild(a);
|
||||
URL.revokeObjectURL(url);
|
||||
};
|
||||
|
||||
// Loading state
|
||||
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>
|
||||
);
|
||||
}
|
||||
|
||||
if (!selectedStudy) return null;
|
||||
|
||||
// Calculate design space size
|
||||
const designSpaceSize = config?.design_variables.reduce((acc, v) => {
|
||||
if (v.type === 'categorical' && v.choices) {
|
||||
return acc * v.choices.length;
|
||||
} else if (v.type === 'int' && v.low !== undefined && v.high !== undefined) {
|
||||
return acc * (v.high - v.low + 1);
|
||||
}
|
||||
return acc * 1000; // Approximate for continuous
|
||||
}, 1) || 0;
|
||||
|
||||
return (
|
||||
<div className="w-full max-w-[2400px] mx-auto px-4">
|
||||
{/* Header */}
|
||||
<header className="mb-6 flex items-center justify-between border-b border-dark-600 pb-4">
|
||||
<div>
|
||||
<div className="flex items-center gap-3">
|
||||
<Settings className="w-8 h-8 text-primary-400" />
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold text-white">{config?.study_name || selectedStudy.name}</h1>
|
||||
<p className="text-dark-400 text-sm">Study Configuration</p>
|
||||
</div>
|
||||
</div>
|
||||
{config?.description && (
|
||||
<p className="text-dark-300 mt-2 max-w-2xl">{config.description}</p>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex items-center gap-3">
|
||||
<Button
|
||||
variant="secondary"
|
||||
icon={<RefreshCw className="w-4 h-4" />}
|
||||
onClick={loadConfig}
|
||||
disabled={loading}
|
||||
>
|
||||
Refresh
|
||||
</Button>
|
||||
<Button
|
||||
variant="secondary"
|
||||
icon={<Download className="w-4 h-4" />}
|
||||
onClick={handleExportConfig}
|
||||
disabled={!config}
|
||||
>
|
||||
Export
|
||||
</Button>
|
||||
{selectedStudy.status === 'not_started' && (
|
||||
<Button
|
||||
variant="primary"
|
||||
icon={<Play className="w-4 h-4" />}
|
||||
onClick={handleStartOptimization}
|
||||
>
|
||||
Start Optimization
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</header>
|
||||
|
||||
{/* Error Message */}
|
||||
{error && (
|
||||
<div className="mb-4 p-3 bg-yellow-900/20 border border-yellow-800/30 rounded-lg">
|
||||
<div className="flex items-center gap-2 text-yellow-400 text-sm">
|
||||
<Info className="w-4 h-4" />
|
||||
<span>{error}</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{loading ? (
|
||||
<div className="flex items-center justify-center py-16">
|
||||
<RefreshCw className="w-8 h-8 animate-spin text-dark-400" />
|
||||
</div>
|
||||
) : (
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||
{/* Left Column */}
|
||||
<div className="space-y-6">
|
||||
{/* Objectives Panel */}
|
||||
<Card className="overflow-hidden">
|
||||
<button
|
||||
onClick={() => toggleSection('objectives')}
|
||||
className="w-full flex items-center justify-between p-4 hover:bg-dark-750 transition-colors"
|
||||
>
|
||||
<div className="flex items-center gap-3">
|
||||
<Target className="w-5 h-5 text-primary-400" />
|
||||
<h2 className="text-lg font-semibold text-white">Objectives</h2>
|
||||
<span className="text-xs bg-dark-600 text-dark-300 px-2 py-0.5 rounded-full">
|
||||
{config?.objectives.length || 0}
|
||||
</span>
|
||||
</div>
|
||||
{expandedSections.has('objectives') ? (
|
||||
<ChevronUp className="w-5 h-5 text-dark-400" />
|
||||
) : (
|
||||
<ChevronDown className="w-5 h-5 text-dark-400" />
|
||||
)}
|
||||
</button>
|
||||
|
||||
{expandedSections.has('objectives') && (
|
||||
<div className="px-4 pb-4 space-y-3">
|
||||
{config?.objectives.map((obj, idx) => (
|
||||
<div
|
||||
key={idx}
|
||||
className="bg-dark-750 rounded-lg p-4 border border-dark-600"
|
||||
>
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<span className="font-medium text-white">{obj.name}</span>
|
||||
<span className={`flex items-center gap-1 text-sm px-2 py-1 rounded ${
|
||||
obj.direction === 'minimize'
|
||||
? 'bg-green-500/10 text-green-400'
|
||||
: 'bg-blue-500/10 text-blue-400'
|
||||
}`}>
|
||||
{obj.direction === 'minimize' ? (
|
||||
<ArrowDown className="w-3 h-3" />
|
||||
) : (
|
||||
<ArrowUp className="w-3 h-3" />
|
||||
)}
|
||||
{obj.direction}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-4 text-sm text-dark-400">
|
||||
{obj.unit && <span>Unit: {obj.unit}</span>}
|
||||
{obj.target !== undefined && (
|
||||
<span>Target: {obj.target}</span>
|
||||
)}
|
||||
{obj.weight !== undefined && (
|
||||
<span>Weight: {obj.weight}</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
{config?.objectives.length === 0 && (
|
||||
<p className="text-dark-500 text-sm italic">No objectives configured</p>
|
||||
)}
|
||||
<div className="text-xs text-dark-500 pt-2">
|
||||
Type: {(config?.objectives.length || 0) > 1 ? 'Multi-Objective' : 'Single-Objective'}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</Card>
|
||||
|
||||
{/* Design Variables Panel */}
|
||||
<Card className="overflow-hidden">
|
||||
<button
|
||||
onClick={() => toggleSection('variables')}
|
||||
className="w-full flex items-center justify-between p-4 hover:bg-dark-750 transition-colors"
|
||||
>
|
||||
<div className="flex items-center gap-3">
|
||||
<Sliders className="w-5 h-5 text-primary-400" />
|
||||
<h2 className="text-lg font-semibold text-white">Design Variables</h2>
|
||||
<span className="text-xs bg-dark-600 text-dark-300 px-2 py-0.5 rounded-full">
|
||||
{config?.design_variables.length || 0}
|
||||
</span>
|
||||
</div>
|
||||
{expandedSections.has('variables') ? (
|
||||
<ChevronUp className="w-5 h-5 text-dark-400" />
|
||||
) : (
|
||||
<ChevronDown className="w-5 h-5 text-dark-400" />
|
||||
)}
|
||||
</button>
|
||||
|
||||
{expandedSections.has('variables') && (
|
||||
<div className="px-4 pb-4 space-y-2">
|
||||
{config?.design_variables.map((v, idx) => (
|
||||
<div
|
||||
key={idx}
|
||||
className="bg-dark-750 rounded-lg p-3 border border-dark-600"
|
||||
>
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="font-medium text-white font-mono text-sm">{v.name}</span>
|
||||
<span className="text-xs bg-dark-600 text-dark-400 px-2 py-0.5 rounded">
|
||||
{v.type}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-4 text-sm text-dark-400 mt-1">
|
||||
{v.low !== undefined && v.high !== undefined && (
|
||||
<span>Range: [{v.low}, {v.high}]</span>
|
||||
)}
|
||||
{v.step && <span>Step: {v.step}</span>}
|
||||
{v.unit && <span>{v.unit}</span>}
|
||||
{v.choices && (
|
||||
<span>Choices: {v.choices.join(', ')}</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
{config?.design_variables.length === 0 && (
|
||||
<p className="text-dark-500 text-sm italic">No design variables configured</p>
|
||||
)}
|
||||
{designSpaceSize > 0 && (
|
||||
<div className="text-xs text-dark-500 pt-2">
|
||||
Design Space: ~{designSpaceSize.toExponential(2)} combinations
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</Card>
|
||||
|
||||
{/* Constraints Panel */}
|
||||
<Card className="overflow-hidden">
|
||||
<button
|
||||
onClick={() => toggleSection('constraints')}
|
||||
className="w-full flex items-center justify-between p-4 hover:bg-dark-750 transition-colors"
|
||||
>
|
||||
<div className="flex items-center gap-3">
|
||||
<AlertTriangle className="w-5 h-5 text-yellow-400" />
|
||||
<h2 className="text-lg font-semibold text-white">Constraints</h2>
|
||||
<span className="text-xs bg-dark-600 text-dark-300 px-2 py-0.5 rounded-full">
|
||||
{config?.constraints.length || 0}
|
||||
</span>
|
||||
</div>
|
||||
{expandedSections.has('constraints') ? (
|
||||
<ChevronUp className="w-5 h-5 text-dark-400" />
|
||||
) : (
|
||||
<ChevronDown className="w-5 h-5 text-dark-400" />
|
||||
)}
|
||||
</button>
|
||||
|
||||
{expandedSections.has('constraints') && (
|
||||
<div className="px-4 pb-4">
|
||||
{(config?.constraints.length || 0) > 0 ? (
|
||||
<table className="w-full text-sm">
|
||||
<thead>
|
||||
<tr className="text-dark-400 text-left">
|
||||
<th className="pb-2">Name</th>
|
||||
<th className="pb-2">Type</th>
|
||||
<th className="pb-2">Bound</th>
|
||||
<th className="pb-2">Unit</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="text-dark-300">
|
||||
{config?.constraints.map((c, idx) => (
|
||||
<tr key={idx} className="border-t border-dark-700">
|
||||
<td className="py-2 font-mono">{c.name}</td>
|
||||
<td className="py-2">
|
||||
{c.type === 'le' ? '≤' : c.type === 'ge' ? '≥' : '='}
|
||||
</td>
|
||||
<td className="py-2">{c.bound}</td>
|
||||
<td className="py-2 text-dark-500">{c.unit || '-'}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
) : (
|
||||
<p className="text-dark-500 text-sm italic">No constraints configured</p>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* Right Column */}
|
||||
<div className="space-y-6">
|
||||
{/* Algorithm Configuration */}
|
||||
<Card className="overflow-hidden">
|
||||
<button
|
||||
onClick={() => toggleSection('algorithm')}
|
||||
className="w-full flex items-center justify-between p-4 hover:bg-dark-750 transition-colors"
|
||||
>
|
||||
<div className="flex items-center gap-3">
|
||||
<Cpu className="w-5 h-5 text-primary-400" />
|
||||
<h2 className="text-lg font-semibold text-white">Algorithm Configuration</h2>
|
||||
</div>
|
||||
{expandedSections.has('algorithm') ? (
|
||||
<ChevronUp className="w-5 h-5 text-dark-400" />
|
||||
) : (
|
||||
<ChevronDown className="w-5 h-5 text-dark-400" />
|
||||
)}
|
||||
</button>
|
||||
|
||||
{expandedSections.has('algorithm') && (
|
||||
<div className="px-4 pb-4 space-y-3">
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="bg-dark-750 rounded-lg p-3 border border-dark-600">
|
||||
<div className="text-xs text-dark-400 uppercase mb-1">Optimizer</div>
|
||||
<div className="text-white font-medium">{config?.algorithm.name || 'Optuna'}</div>
|
||||
</div>
|
||||
<div className="bg-dark-750 rounded-lg p-3 border border-dark-600">
|
||||
<div className="text-xs text-dark-400 uppercase mb-1">Sampler</div>
|
||||
<div className="text-white font-medium">{config?.algorithm.sampler || 'TPE'}</div>
|
||||
</div>
|
||||
<div className="bg-dark-750 rounded-lg p-3 border border-dark-600">
|
||||
<div className="text-xs text-dark-400 uppercase mb-1">Total Trials</div>
|
||||
<div className="text-white font-medium">{config?.algorithm.n_trials || selectedStudy.progress.total}</div>
|
||||
</div>
|
||||
{config?.algorithm.pruner && (
|
||||
<div className="bg-dark-750 rounded-lg p-3 border border-dark-600">
|
||||
<div className="text-xs text-dark-400 uppercase mb-1">Pruner</div>
|
||||
<div className="text-white font-medium">{config.algorithm.pruner}</div>
|
||||
</div>
|
||||
)}
|
||||
{config?.algorithm.timeout && (
|
||||
<div className="bg-dark-750 rounded-lg p-3 border border-dark-600">
|
||||
<div className="text-xs text-dark-400 uppercase mb-1">Timeout</div>
|
||||
<div className="text-white font-medium">{config.algorithm.timeout}s</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</Card>
|
||||
|
||||
{/* FEA Model Info */}
|
||||
{config?.fea_model && (
|
||||
<Card className="overflow-hidden">
|
||||
<button
|
||||
onClick={() => toggleSection('model')}
|
||||
className="w-full flex items-center justify-between p-4 hover:bg-dark-750 transition-colors"
|
||||
>
|
||||
<div className="flex items-center gap-3">
|
||||
<Box className="w-5 h-5 text-primary-400" />
|
||||
<h2 className="text-lg font-semibold text-white">FEA Model</h2>
|
||||
</div>
|
||||
{expandedSections.has('model') ? (
|
||||
<ChevronUp className="w-5 h-5 text-dark-400" />
|
||||
) : (
|
||||
<ChevronDown className="w-5 h-5 text-dark-400" />
|
||||
)}
|
||||
</button>
|
||||
|
||||
{expandedSections.has('model') && (
|
||||
<div className="px-4 pb-4 space-y-3">
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="bg-dark-750 rounded-lg p-3 border border-dark-600">
|
||||
<div className="text-xs text-dark-400 uppercase mb-1">Software</div>
|
||||
<div className="text-white font-medium">{config.fea_model.software}</div>
|
||||
</div>
|
||||
<div className="bg-dark-750 rounded-lg p-3 border border-dark-600">
|
||||
<div className="text-xs text-dark-400 uppercase mb-1">Solver</div>
|
||||
<div className="text-white font-medium">{config.fea_model.solver}</div>
|
||||
</div>
|
||||
{config.fea_model.mesh_elements && (
|
||||
<div className="bg-dark-750 rounded-lg p-3 border border-dark-600">
|
||||
<div className="text-xs text-dark-400 uppercase mb-1">Mesh Elements</div>
|
||||
<div className="text-white font-medium">
|
||||
{config.fea_model.mesh_elements.toLocaleString()}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{config.fea_model.sim_file && (
|
||||
<div className="bg-dark-750 rounded-lg p-3 border border-dark-600 col-span-2">
|
||||
<div className="text-xs text-dark-400 uppercase mb-1">Simulation File</div>
|
||||
<div className="text-white font-mono text-sm truncate">
|
||||
{config.fea_model.sim_file}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{/* NX Model Files */}
|
||||
<Card className="overflow-hidden">
|
||||
<button
|
||||
onClick={() => toggleSection('modelFiles')}
|
||||
className="w-full flex items-center justify-between p-4 hover:bg-dark-750 transition-colors"
|
||||
>
|
||||
<div className="flex items-center gap-3">
|
||||
<FileBox className="w-5 h-5 text-primary-400" />
|
||||
<h2 className="text-lg font-semibold text-white">NX Model Files</h2>
|
||||
<span className="text-xs bg-dark-600 text-dark-300 px-2 py-0.5 rounded-full">
|
||||
{modelFiles.length}
|
||||
</span>
|
||||
</div>
|
||||
{expandedSections.has('modelFiles') ? (
|
||||
<ChevronUp className="w-5 h-5 text-dark-400" />
|
||||
) : (
|
||||
<ChevronDown className="w-5 h-5 text-dark-400" />
|
||||
)}
|
||||
</button>
|
||||
|
||||
{expandedSections.has('modelFiles') && (
|
||||
<div className="px-4 pb-4 space-y-3">
|
||||
{/* Open Folder Button */}
|
||||
<button
|
||||
onClick={handleOpenFolder}
|
||||
className="w-full flex items-center justify-center gap-2 px-4 py-2 bg-dark-700 hover:bg-dark-600 text-dark-200 hover:text-white rounded-lg border border-dark-600 transition-colors"
|
||||
>
|
||||
<FolderOpen className="w-4 h-4" />
|
||||
<span>Open Model Folder</span>
|
||||
</button>
|
||||
|
||||
{/* Model Directory Path */}
|
||||
{modelDir && (
|
||||
<div className="text-xs text-dark-500 font-mono truncate" title={modelDir}>
|
||||
{modelDir}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* File List */}
|
||||
{modelFiles.length > 0 ? (
|
||||
<div className="space-y-2">
|
||||
{modelFiles.map((file, idx) => (
|
||||
<div
|
||||
key={idx}
|
||||
className="bg-dark-750 rounded-lg p-3 border border-dark-600"
|
||||
>
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<File className={`w-4 h-4 ${
|
||||
file.extension === '.prt' ? 'text-blue-400' :
|
||||
file.extension === '.sim' ? 'text-green-400' :
|
||||
file.extension === '.fem' ? 'text-yellow-400' :
|
||||
file.extension === '.bdf' || file.extension === '.dat' ? 'text-orange-400' :
|
||||
file.extension === '.op2' ? 'text-purple-400' :
|
||||
'text-dark-400'
|
||||
}`} />
|
||||
<span className="font-medium text-white text-sm truncate" title={file.name}>
|
||||
{file.name}
|
||||
</span>
|
||||
</div>
|
||||
<span className="text-xs bg-dark-600 text-dark-400 px-2 py-0.5 rounded uppercase">
|
||||
{file.extension.slice(1)}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center justify-between mt-1 text-xs text-dark-500">
|
||||
<span>{file.size_display}</span>
|
||||
<span>{new Date(file.modified).toLocaleDateString()}</span>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<p className="text-dark-500 text-sm italic text-center py-4">
|
||||
No model files found
|
||||
</p>
|
||||
)}
|
||||
|
||||
{/* File Type Legend */}
|
||||
{modelFiles.length > 0 && (
|
||||
<div className="pt-2 border-t border-dark-700">
|
||||
<div className="flex flex-wrap gap-3 text-xs text-dark-500">
|
||||
<span className="flex items-center gap-1"><span className="w-2 h-2 bg-blue-400 rounded-full"></span>.prt = Part</span>
|
||||
<span className="flex items-center gap-1"><span className="w-2 h-2 bg-green-400 rounded-full"></span>.sim = Simulation</span>
|
||||
<span className="flex items-center gap-1"><span className="w-2 h-2 bg-yellow-400 rounded-full"></span>.fem = FEM</span>
|
||||
<span className="flex items-center gap-1"><span className="w-2 h-2 bg-orange-400 rounded-full"></span>.bdf = Nastran</span>
|
||||
<span className="flex items-center gap-1"><span className="w-2 h-2 bg-purple-400 rounded-full"></span>.op2 = Results</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</Card>
|
||||
|
||||
{/* Extractors */}
|
||||
{config?.extractors && config.extractors.length > 0 && (
|
||||
<Card className="overflow-hidden">
|
||||
<button
|
||||
onClick={() => toggleSection('extractors')}
|
||||
className="w-full flex items-center justify-between p-4 hover:bg-dark-750 transition-colors"
|
||||
>
|
||||
<div className="flex items-center gap-3">
|
||||
<Layers className="w-5 h-5 text-primary-400" />
|
||||
<h2 className="text-lg font-semibold text-white">Extractors</h2>
|
||||
<span className="text-xs bg-dark-600 text-dark-300 px-2 py-0.5 rounded-full">
|
||||
{config.extractors.length}
|
||||
</span>
|
||||
</div>
|
||||
{expandedSections.has('extractors') ? (
|
||||
<ChevronUp className="w-5 h-5 text-dark-400" />
|
||||
) : (
|
||||
<ChevronDown className="w-5 h-5 text-dark-400" />
|
||||
)}
|
||||
</button>
|
||||
|
||||
{expandedSections.has('extractors') && (
|
||||
<div className="px-4 pb-4 space-y-2">
|
||||
{config.extractors.map((ext, idx) => (
|
||||
<div
|
||||
key={idx}
|
||||
className="bg-dark-750 rounded-lg p-3 border border-dark-600"
|
||||
>
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="font-medium text-white">{ext.name}</span>
|
||||
<span className="text-xs bg-dark-600 text-dark-400 px-2 py-0.5 rounded">
|
||||
{ext.type}
|
||||
</span>
|
||||
</div>
|
||||
{ext.source && (
|
||||
<div className="text-xs text-dark-500 mt-1 font-mono">{ext.source}</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{/* Study Stats */}
|
||||
<Card title="Current Progress">
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="bg-dark-750 rounded-lg p-4 border border-dark-600 text-center">
|
||||
<div className="text-3xl font-bold text-white">
|
||||
{selectedStudy.progress.current}
|
||||
</div>
|
||||
<div className="text-xs text-dark-400 uppercase mt-1">Trials Completed</div>
|
||||
</div>
|
||||
<div className="bg-dark-750 rounded-lg p-4 border border-dark-600 text-center">
|
||||
<div className="text-3xl font-bold text-primary-400">
|
||||
{selectedStudy.best_value?.toExponential(3) || 'N/A'}
|
||||
</div>
|
||||
<div className="text-xs text-dark-400 uppercase mt-1">Best Value</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Progress Bar */}
|
||||
<div className="mt-4">
|
||||
<div className="flex items-center justify-between text-sm mb-2">
|
||||
<span className="text-dark-400">Progress</span>
|
||||
<span className="text-white">
|
||||
{Math.round((selectedStudy.progress.current / selectedStudy.progress.total) * 100)}%
|
||||
</span>
|
||||
</div>
|
||||
<div className="h-2 bg-dark-700 rounded-full overflow-hidden">
|
||||
<div
|
||||
className="h-full bg-primary-500 rounded-full transition-all"
|
||||
style={{
|
||||
width: `${Math.min((selectedStudy.progress.current / selectedStudy.progress.total) * 100, 100)}%`
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Status Badge */}
|
||||
<div className="mt-4 flex items-center justify-center">
|
||||
<span className={`flex items-center gap-2 px-4 py-2 rounded-full text-sm font-medium ${
|
||||
selectedStudy.status === 'running' ? 'bg-green-500/10 text-green-400' :
|
||||
selectedStudy.status === 'completed' ? 'bg-blue-500/10 text-blue-400' :
|
||||
selectedStudy.status === 'paused' ? 'bg-orange-500/10 text-orange-400' :
|
||||
'bg-dark-600 text-dark-400'
|
||||
}`}>
|
||||
{selectedStudy.status === 'completed' && <CheckCircle className="w-4 h-4" />}
|
||||
{selectedStudy.status === 'running' && <Play className="w-4 h-4" />}
|
||||
<span className="capitalize">{selectedStudy.status.replace('_', ' ')}</span>
|
||||
</span>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user