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>
459 lines
18 KiB
TypeScript
459 lines
18 KiB
TypeScript
import { useState, useEffect, useCallback } from 'react';
|
|
import { Settings, Save, X, AlertTriangle, Check, RotateCcw } from 'lucide-react';
|
|
import { Card } from './common/Card';
|
|
|
|
interface DesignVariable {
|
|
name: string;
|
|
min: number;
|
|
max: number;
|
|
type?: string;
|
|
description?: string;
|
|
}
|
|
|
|
interface Objective {
|
|
name: string;
|
|
direction: 'minimize' | 'maximize';
|
|
description?: string;
|
|
unit?: string;
|
|
}
|
|
|
|
interface OptimizationConfig {
|
|
study_name?: string;
|
|
description?: string;
|
|
design_variables?: DesignVariable[];
|
|
objectives?: Objective[];
|
|
constraints?: any[];
|
|
optimization_settings?: {
|
|
n_trials?: number;
|
|
sampler?: string;
|
|
[key: string]: any;
|
|
};
|
|
[key: string]: any;
|
|
}
|
|
|
|
interface ConfigEditorProps {
|
|
studyId: string;
|
|
onClose: () => void;
|
|
onSaved?: () => void;
|
|
}
|
|
|
|
export function ConfigEditor({ studyId, onClose, onSaved }: ConfigEditorProps) {
|
|
const [config, setConfig] = useState<OptimizationConfig | null>(null);
|
|
const [originalConfig, setOriginalConfig] = useState<OptimizationConfig | null>(null);
|
|
const [loading, setLoading] = useState(true);
|
|
const [saving, setSaving] = useState(false);
|
|
const [error, setError] = useState<string | null>(null);
|
|
const [success, setSuccess] = useState<string | null>(null);
|
|
const [isRunning, setIsRunning] = useState(false);
|
|
const [activeTab, setActiveTab] = useState<'general' | 'variables' | 'objectives' | 'settings' | 'json'>('general');
|
|
const [jsonText, setJsonText] = useState('');
|
|
const [jsonError, setJsonError] = useState<string | null>(null);
|
|
|
|
// Load config
|
|
useEffect(() => {
|
|
const loadConfig = async () => {
|
|
try {
|
|
setLoading(true);
|
|
setError(null);
|
|
|
|
// Check if optimization is running
|
|
const processRes = await fetch(`/api/optimization/studies/${studyId}/process`);
|
|
const processData = await processRes.json();
|
|
setIsRunning(processData.is_running);
|
|
|
|
// Load config
|
|
const configRes = await fetch(`/api/optimization/studies/${studyId}/config`);
|
|
if (!configRes.ok) {
|
|
throw new Error('Failed to load config');
|
|
}
|
|
const configData = await configRes.json();
|
|
setConfig(configData.config);
|
|
setOriginalConfig(JSON.parse(JSON.stringify(configData.config)));
|
|
setJsonText(JSON.stringify(configData.config, null, 2));
|
|
} catch (err) {
|
|
setError(err instanceof Error ? err.message : 'Failed to load config');
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
};
|
|
|
|
loadConfig();
|
|
}, [studyId]);
|
|
|
|
// Handle JSON text changes
|
|
const handleJsonChange = useCallback((text: string) => {
|
|
setJsonText(text);
|
|
setJsonError(null);
|
|
try {
|
|
const parsed = JSON.parse(text);
|
|
setConfig(parsed);
|
|
} catch (err) {
|
|
setJsonError('Invalid JSON');
|
|
}
|
|
}, []);
|
|
|
|
// Save config
|
|
const handleSave = async () => {
|
|
if (!config || isRunning) return;
|
|
|
|
try {
|
|
setSaving(true);
|
|
setError(null);
|
|
setSuccess(null);
|
|
|
|
const res = await fetch(`/api/optimization/studies/${studyId}/config`, {
|
|
method: 'PUT',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ config })
|
|
});
|
|
|
|
if (!res.ok) {
|
|
const data = await res.json();
|
|
throw new Error(data.detail || 'Failed to save config');
|
|
}
|
|
|
|
setSuccess('Configuration saved successfully');
|
|
setOriginalConfig(JSON.parse(JSON.stringify(config)));
|
|
onSaved?.();
|
|
|
|
// Clear success after 3 seconds
|
|
setTimeout(() => setSuccess(null), 3000);
|
|
} catch (err) {
|
|
setError(err instanceof Error ? err.message : 'Failed to save config');
|
|
} finally {
|
|
setSaving(false);
|
|
}
|
|
};
|
|
|
|
// Reset to original
|
|
const handleReset = () => {
|
|
if (originalConfig) {
|
|
setConfig(JSON.parse(JSON.stringify(originalConfig)));
|
|
setJsonText(JSON.stringify(originalConfig, null, 2));
|
|
setJsonError(null);
|
|
}
|
|
};
|
|
|
|
// Check if there are unsaved changes
|
|
const hasChanges = config && originalConfig
|
|
? JSON.stringify(config) !== JSON.stringify(originalConfig)
|
|
: false;
|
|
|
|
// Update a design variable
|
|
const updateDesignVariable = (index: number, field: keyof DesignVariable, value: any) => {
|
|
if (!config?.design_variables) return;
|
|
|
|
const newVars = [...config.design_variables];
|
|
newVars[index] = { ...newVars[index], [field]: value };
|
|
setConfig({ ...config, design_variables: newVars });
|
|
setJsonText(JSON.stringify({ ...config, design_variables: newVars }, null, 2));
|
|
};
|
|
|
|
// Update an objective
|
|
const updateObjective = (index: number, field: keyof Objective, value: any) => {
|
|
if (!config?.objectives) return;
|
|
|
|
const newObjs = [...config.objectives];
|
|
newObjs[index] = { ...newObjs[index], [field]: value };
|
|
setConfig({ ...config, objectives: newObjs });
|
|
setJsonText(JSON.stringify({ ...config, objectives: newObjs }, null, 2));
|
|
};
|
|
|
|
// Update optimization settings
|
|
const updateSettings = (field: string, value: any) => {
|
|
if (!config) return;
|
|
|
|
const newSettings = { ...config.optimization_settings, [field]: value };
|
|
setConfig({ ...config, optimization_settings: newSettings });
|
|
setJsonText(JSON.stringify({ ...config, optimization_settings: newSettings }, null, 2));
|
|
};
|
|
|
|
if (loading) {
|
|
return (
|
|
<div className="fixed inset-0 bg-black/70 flex items-center justify-center z-50">
|
|
<Card className="w-full max-w-4xl max-h-[90vh] p-6">
|
|
<div className="flex items-center justify-center py-12">
|
|
<div className="animate-spin w-8 h-8 border-2 border-primary-500 border-t-transparent rounded-full"></div>
|
|
</div>
|
|
</Card>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<div className="fixed inset-0 bg-black/70 flex items-center justify-center z-50 p-4">
|
|
<Card className="w-full max-w-4xl max-h-[90vh] flex flex-col">
|
|
{/* Header */}
|
|
<div className="flex items-center justify-between p-4 border-b border-dark-600">
|
|
<div className="flex items-center gap-3">
|
|
<Settings className="w-5 h-5 text-primary-400" />
|
|
<h2 className="text-lg font-semibold text-white">Edit Configuration</h2>
|
|
{hasChanges && (
|
|
<span className="px-2 py-0.5 bg-yellow-500/20 text-yellow-400 text-xs rounded">
|
|
Unsaved changes
|
|
</span>
|
|
)}
|
|
</div>
|
|
<button
|
|
onClick={onClose}
|
|
className="p-2 text-dark-400 hover:text-white transition-colors"
|
|
>
|
|
<X className="w-5 h-5" />
|
|
</button>
|
|
</div>
|
|
|
|
{/* Warning if running */}
|
|
{isRunning && (
|
|
<div className="m-4 p-3 bg-red-500/10 border border-red-500/30 rounded-lg flex items-center gap-3">
|
|
<AlertTriangle className="w-5 h-5 text-red-400" />
|
|
<span className="text-red-400 text-sm">
|
|
Optimization is running. Stop it before editing configuration.
|
|
</span>
|
|
</div>
|
|
)}
|
|
|
|
{/* Tabs */}
|
|
<div className="flex border-b border-dark-600 px-4">
|
|
{(['general', 'variables', 'objectives', 'settings', 'json'] as const).map(tab => (
|
|
<button
|
|
key={tab}
|
|
onClick={() => setActiveTab(tab)}
|
|
className={`px-4 py-2 text-sm font-medium transition-colors ${
|
|
activeTab === tab
|
|
? 'text-primary-400 border-b-2 border-primary-400 -mb-[2px]'
|
|
: 'text-dark-400 hover:text-white'
|
|
}`}
|
|
>
|
|
{tab.charAt(0).toUpperCase() + tab.slice(1)}
|
|
</button>
|
|
))}
|
|
</div>
|
|
|
|
{/* Content */}
|
|
<div className="flex-1 overflow-y-auto p-4">
|
|
{error && (
|
|
<div className="mb-4 p-3 bg-red-500/10 border border-red-500/30 rounded-lg text-red-400 text-sm">
|
|
{error}
|
|
</div>
|
|
)}
|
|
|
|
{success && (
|
|
<div className="mb-4 p-3 bg-green-500/10 border border-green-500/30 rounded-lg text-green-400 text-sm flex items-center gap-2">
|
|
<Check className="w-4 h-4" />
|
|
{success}
|
|
</div>
|
|
)}
|
|
|
|
{config && (
|
|
<>
|
|
{/* General Tab */}
|
|
{activeTab === 'general' && (
|
|
<div className="space-y-4">
|
|
<div>
|
|
<label className="block text-sm font-medium text-dark-300 mb-1">
|
|
Study Name
|
|
</label>
|
|
<input
|
|
type="text"
|
|
value={config.study_name || ''}
|
|
onChange={(e) => {
|
|
setConfig({ ...config, study_name: e.target.value });
|
|
setJsonText(JSON.stringify({ ...config, study_name: e.target.value }, null, 2));
|
|
}}
|
|
disabled={isRunning}
|
|
className="w-full px-3 py-2 bg-dark-700 border border-dark-600 rounded-lg text-white disabled:opacity-50"
|
|
/>
|
|
</div>
|
|
<div>
|
|
<label className="block text-sm font-medium text-dark-300 mb-1">
|
|
Description
|
|
</label>
|
|
<textarea
|
|
value={config.description || ''}
|
|
onChange={(e) => {
|
|
setConfig({ ...config, description: e.target.value });
|
|
setJsonText(JSON.stringify({ ...config, description: e.target.value }, null, 2));
|
|
}}
|
|
disabled={isRunning}
|
|
rows={3}
|
|
className="w-full px-3 py-2 bg-dark-700 border border-dark-600 rounded-lg text-white disabled:opacity-50"
|
|
/>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* Design Variables Tab */}
|
|
{activeTab === 'variables' && (
|
|
<div className="space-y-4">
|
|
<p className="text-dark-400 text-sm mb-4">
|
|
Edit design variable bounds. These control the parameter search space.
|
|
</p>
|
|
{config.design_variables?.map((dv, index) => (
|
|
<div key={dv.name} className="p-4 bg-dark-750 rounded-lg">
|
|
<div className="font-medium text-white mb-3">{dv.name}</div>
|
|
<div className="grid grid-cols-2 gap-4">
|
|
<div>
|
|
<label className="block text-xs text-dark-400 mb-1">Min</label>
|
|
<input
|
|
type="number"
|
|
value={dv.min}
|
|
onChange={(e) => updateDesignVariable(index, 'min', parseFloat(e.target.value))}
|
|
disabled={isRunning}
|
|
step="any"
|
|
className="w-full px-3 py-2 bg-dark-700 border border-dark-600 rounded-lg text-white disabled:opacity-50"
|
|
/>
|
|
</div>
|
|
<div>
|
|
<label className="block text-xs text-dark-400 mb-1">Max</label>
|
|
<input
|
|
type="number"
|
|
value={dv.max}
|
|
onChange={(e) => updateDesignVariable(index, 'max', parseFloat(e.target.value))}
|
|
disabled={isRunning}
|
|
step="any"
|
|
className="w-full px-3 py-2 bg-dark-700 border border-dark-600 rounded-lg text-white disabled:opacity-50"
|
|
/>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
)}
|
|
|
|
{/* Objectives Tab */}
|
|
{activeTab === 'objectives' && (
|
|
<div className="space-y-4">
|
|
<p className="text-dark-400 text-sm mb-4">
|
|
Configure optimization objectives and their directions.
|
|
</p>
|
|
{config.objectives?.map((obj, index) => (
|
|
<div key={obj.name} className="p-4 bg-dark-750 rounded-lg">
|
|
<div className="font-medium text-white mb-3">{obj.name}</div>
|
|
<div className="grid grid-cols-2 gap-4">
|
|
<div>
|
|
<label className="block text-xs text-dark-400 mb-1">Direction</label>
|
|
<select
|
|
value={obj.direction}
|
|
onChange={(e) => updateObjective(index, 'direction', e.target.value)}
|
|
disabled={isRunning}
|
|
className="w-full px-3 py-2 bg-dark-700 border border-dark-600 rounded-lg text-white disabled:opacity-50"
|
|
>
|
|
<option value="minimize">Minimize</option>
|
|
<option value="maximize">Maximize</option>
|
|
</select>
|
|
</div>
|
|
<div>
|
|
<label className="block text-xs text-dark-400 mb-1">Unit</label>
|
|
<input
|
|
type="text"
|
|
value={obj.unit || ''}
|
|
onChange={(e) => updateObjective(index, 'unit', e.target.value)}
|
|
disabled={isRunning}
|
|
className="w-full px-3 py-2 bg-dark-700 border border-dark-600 rounded-lg text-white disabled:opacity-50"
|
|
/>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
)}
|
|
|
|
{/* Settings Tab */}
|
|
{activeTab === 'settings' && (
|
|
<div className="space-y-4">
|
|
<p className="text-dark-400 text-sm mb-4">
|
|
Optimization algorithm settings.
|
|
</p>
|
|
<div className="grid grid-cols-2 gap-4">
|
|
<div>
|
|
<label className="block text-sm font-medium text-dark-300 mb-1">
|
|
Number of Trials
|
|
</label>
|
|
<input
|
|
type="number"
|
|
value={config.optimization_settings?.n_trials || 100}
|
|
onChange={(e) => updateSettings('n_trials', parseInt(e.target.value))}
|
|
disabled={isRunning}
|
|
min={1}
|
|
className="w-full px-3 py-2 bg-dark-700 border border-dark-600 rounded-lg text-white disabled:opacity-50"
|
|
/>
|
|
</div>
|
|
<div>
|
|
<label className="block text-sm font-medium text-dark-300 mb-1">
|
|
Sampler
|
|
</label>
|
|
<select
|
|
value={config.optimization_settings?.sampler || 'TPE'}
|
|
onChange={(e) => updateSettings('sampler', e.target.value)}
|
|
disabled={isRunning}
|
|
className="w-full px-3 py-2 bg-dark-700 border border-dark-600 rounded-lg text-white disabled:opacity-50"
|
|
>
|
|
<option value="TPE">TPE (Tree-structured Parzen Estimator)</option>
|
|
<option value="CMA-ES">CMA-ES (Evolution Strategy)</option>
|
|
<option value="NSGA-II">NSGA-II (Multi-objective)</option>
|
|
<option value="Random">Random</option>
|
|
<option value="QMC">QMC (Quasi-Monte Carlo)</option>
|
|
</select>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* JSON Tab */}
|
|
{activeTab === 'json' && (
|
|
<div className="space-y-2">
|
|
<p className="text-dark-400 text-sm">
|
|
Edit the raw JSON configuration. Be careful with syntax.
|
|
</p>
|
|
{jsonError && (
|
|
<div className="p-2 bg-red-500/10 border border-red-500/30 rounded text-red-400 text-xs">
|
|
{jsonError}
|
|
</div>
|
|
)}
|
|
<textarea
|
|
value={jsonText}
|
|
onChange={(e) => handleJsonChange(e.target.value)}
|
|
disabled={isRunning}
|
|
spellCheck={false}
|
|
className="w-full h-96 px-3 py-2 bg-dark-700 border border-dark-600 rounded-lg text-white font-mono text-sm disabled:opacity-50"
|
|
/>
|
|
</div>
|
|
)}
|
|
</>
|
|
)}
|
|
</div>
|
|
|
|
{/* Footer */}
|
|
<div className="flex items-center justify-between p-4 border-t border-dark-600">
|
|
<button
|
|
onClick={handleReset}
|
|
disabled={!hasChanges || isRunning}
|
|
className="flex items-center gap-2 px-4 py-2 text-dark-400 hover:text-white disabled:opacity-50 transition-colors"
|
|
>
|
|
<RotateCcw className="w-4 h-4" />
|
|
Reset
|
|
</button>
|
|
<div className="flex gap-2">
|
|
<button
|
|
onClick={onClose}
|
|
className="px-4 py-2 bg-dark-700 text-white rounded-lg hover:bg-dark-600 transition-colors"
|
|
>
|
|
Cancel
|
|
</button>
|
|
<button
|
|
onClick={handleSave}
|
|
disabled={saving || !hasChanges || isRunning || !!jsonError}
|
|
className="flex items-center gap-2 px-4 py-2 bg-primary-500 text-white rounded-lg hover:bg-primary-600 disabled:opacity-50 transition-colors"
|
|
>
|
|
<Save className="w-4 h-4" />
|
|
{saving ? 'Saving...' : 'Save Changes'}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</Card>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
export default ConfigEditor;
|