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>
186 lines
6.3 KiB
TypeScript
186 lines
6.3 KiB
TypeScript
import { useState, useEffect } from 'react';
|
|
import { Activity, Clock, Cpu, Zap, CheckCircle } from 'lucide-react';
|
|
|
|
interface CurrentTrialProps {
|
|
studyId: string | null;
|
|
totalTrials: number;
|
|
completedTrials: number;
|
|
isRunning: boolean;
|
|
lastTrialTime?: number; // ms for last trial
|
|
}
|
|
|
|
type TrialPhase = 'idle' | 'sampling' | 'evaluating' | 'extracting' | 'complete';
|
|
|
|
export function CurrentTrialPanel({
|
|
studyId,
|
|
totalTrials,
|
|
completedTrials,
|
|
isRunning,
|
|
lastTrialTime
|
|
}: CurrentTrialProps) {
|
|
const [elapsedTime, setElapsedTime] = useState(0);
|
|
const [phase, setPhase] = useState<TrialPhase>('idle');
|
|
|
|
// Simulate phase progression when running
|
|
useEffect(() => {
|
|
if (!isRunning) {
|
|
setPhase('idle');
|
|
setElapsedTime(0);
|
|
return;
|
|
}
|
|
|
|
setPhase('sampling');
|
|
const interval = setInterval(() => {
|
|
setElapsedTime(prev => {
|
|
const newTime = prev + 1;
|
|
// Simulate phase transitions based on typical timing
|
|
if (newTime < 2) setPhase('sampling');
|
|
else if (newTime < 5) setPhase('evaluating');
|
|
else setPhase('extracting');
|
|
return newTime;
|
|
});
|
|
}, 1000);
|
|
|
|
return () => clearInterval(interval);
|
|
}, [isRunning, completedTrials]);
|
|
|
|
// Reset elapsed time when a new trial completes
|
|
useEffect(() => {
|
|
if (isRunning) {
|
|
setElapsedTime(0);
|
|
setPhase('sampling');
|
|
}
|
|
}, [completedTrials, isRunning]);
|
|
|
|
// Calculate ETA
|
|
const calculateETA = () => {
|
|
if (!isRunning || completedTrials === 0 || !lastTrialTime) return null;
|
|
|
|
const remainingTrials = totalTrials - completedTrials;
|
|
const avgTimePerTrial = lastTrialTime / 1000; // convert to seconds
|
|
const etaSeconds = remainingTrials * avgTimePerTrial;
|
|
|
|
if (etaSeconds < 60) return `~${Math.round(etaSeconds)}s`;
|
|
if (etaSeconds < 3600) return `~${Math.round(etaSeconds / 60)}m`;
|
|
return `~${(etaSeconds / 3600).toFixed(1)}h`;
|
|
};
|
|
|
|
const progressPercent = totalTrials > 0 ? (completedTrials / totalTrials) * 100 : 0;
|
|
const eta = calculateETA();
|
|
|
|
const getPhaseInfo = () => {
|
|
switch (phase) {
|
|
case 'sampling':
|
|
return { label: 'Sampling', color: 'text-blue-400', bgColor: 'bg-blue-500/20', icon: Zap };
|
|
case 'evaluating':
|
|
return { label: 'FEA Solving', color: 'text-yellow-400', bgColor: 'bg-yellow-500/20', icon: Cpu };
|
|
case 'extracting':
|
|
return { label: 'Extracting', color: 'text-purple-400', bgColor: 'bg-purple-500/20', icon: Activity };
|
|
case 'complete':
|
|
return { label: 'Complete', color: 'text-green-400', bgColor: 'bg-green-500/20', icon: CheckCircle };
|
|
default:
|
|
return { label: 'Idle', color: 'text-dark-400', bgColor: 'bg-dark-600', icon: Clock };
|
|
}
|
|
};
|
|
|
|
const phaseInfo = getPhaseInfo();
|
|
const PhaseIcon = phaseInfo.icon;
|
|
|
|
if (!studyId) return null;
|
|
|
|
return (
|
|
<div className="bg-dark-750 rounded-lg border border-dark-600 p-4">
|
|
{/* Header Row */}
|
|
<div className="flex items-center justify-between mb-3">
|
|
<div className="flex items-center gap-2">
|
|
<Activity className={`w-5 h-5 ${isRunning ? 'text-green-400 animate-pulse' : 'text-dark-400'}`} />
|
|
<span className="font-semibold text-white">
|
|
{isRunning ? `Trial #${completedTrials + 1}` : 'Optimization Status'}
|
|
</span>
|
|
</div>
|
|
{isRunning && (
|
|
<span className={`flex items-center gap-1.5 px-2 py-1 rounded-full text-xs font-medium ${phaseInfo.bgColor} ${phaseInfo.color}`}>
|
|
<PhaseIcon className="w-3 h-3" />
|
|
{phaseInfo.label}
|
|
</span>
|
|
)}
|
|
</div>
|
|
|
|
{/* Progress Bar */}
|
|
<div className="mb-3">
|
|
<div className="flex items-center justify-between text-xs mb-1">
|
|
<span className="text-dark-400">Progress</span>
|
|
<span className="text-white font-medium">
|
|
{completedTrials} / {totalTrials} trials
|
|
</span>
|
|
</div>
|
|
<div className="h-2 bg-dark-600 rounded-full overflow-hidden">
|
|
<div
|
|
className={`h-full rounded-full transition-all duration-500 ${
|
|
isRunning ? 'bg-gradient-to-r from-primary-600 to-primary-400' : 'bg-primary-500'
|
|
}`}
|
|
style={{ width: `${progressPercent}%` }}
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Stats Row */}
|
|
<div className="grid grid-cols-3 gap-3">
|
|
{/* Elapsed Time */}
|
|
<div className="text-center">
|
|
<div className={`text-lg font-mono ${isRunning ? 'text-white' : 'text-dark-400'}`}>
|
|
{isRunning ? `${elapsedTime}s` : '--'}
|
|
</div>
|
|
<div className="text-xs text-dark-400">Elapsed</div>
|
|
</div>
|
|
|
|
{/* Completion */}
|
|
<div className="text-center border-x border-dark-600">
|
|
<div className="text-lg font-mono text-primary-400">
|
|
{progressPercent.toFixed(1)}%
|
|
</div>
|
|
<div className="text-xs text-dark-400">Complete</div>
|
|
</div>
|
|
|
|
{/* ETA */}
|
|
<div className="text-center">
|
|
<div className={`text-lg font-mono ${eta ? 'text-blue-400' : 'text-dark-400'}`}>
|
|
{eta || '--'}
|
|
</div>
|
|
<div className="text-xs text-dark-400">ETA</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Running indicator */}
|
|
{isRunning && (
|
|
<div className="mt-3 pt-3 border-t border-dark-600">
|
|
<div className="flex items-center justify-center gap-2 text-xs text-green-400">
|
|
<span className="w-2 h-2 bg-green-500 rounded-full animate-pulse" />
|
|
Optimization in progress...
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* Paused/Stopped indicator */}
|
|
{!isRunning && completedTrials > 0 && completedTrials < totalTrials && (
|
|
<div className="mt-3 pt-3 border-t border-dark-600">
|
|
<div className="flex items-center justify-center gap-2 text-xs text-orange-400">
|
|
<span className="w-2 h-2 bg-orange-500 rounded-full" />
|
|
Optimization paused
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* Completed indicator */}
|
|
{!isRunning && completedTrials >= totalTrials && totalTrials > 0 && (
|
|
<div className="mt-3 pt-3 border-t border-dark-600">
|
|
<div className="flex items-center justify-center gap-2 text-xs text-blue-400">
|
|
<CheckCircle className="w-3 h-3" />
|
|
Optimization complete
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|