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:
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user