feat: Add panel management, validation, and error handling to canvas
Phase 1 - Panel Management System: - Create usePanelStore.ts for centralized panel state management - Add PanelContainer.tsx for draggable floating panels - Create FloatingIntrospectionPanel.tsx (persistent, doesn't disappear on node click) - Create ResultsPanel.tsx for trial result details - Refactor NodeConfigPanelV2 to use panel store for introspection - Integrate PanelContainer into CanvasView Phase 2 - Pre-run Validation: - Create specValidator.ts with comprehensive validation rules - Add ValidationPanel (enhanced version with error navigation) - Add Validate button to SpecRenderer with status indicator - Block run if validation fails - Check for: design vars, objectives, extractors, bounds, connections Phase 3 - Error Handling & Recovery: - Create ErrorPanel.tsx for displaying optimization errors - Add error classification (nx_crash, solver_fail, extractor_error, etc.) - Add recovery suggestions based on error type - Update status endpoint to return error info - Add _get_study_error_info helper to check error_status.json and DB - Integrate error detection into status polling Documentation: - Add CANVAS_ROBUSTNESS_PLAN.md with full implementation plan
This commit is contained in:
@@ -0,0 +1,179 @@
|
||||
/**
|
||||
* ResultsPanel - Shows detailed trial results
|
||||
*
|
||||
* Displays the parameters, objectives, and constraints for a specific trial.
|
||||
* Can be opened by clicking on result badges on nodes.
|
||||
*/
|
||||
|
||||
import {
|
||||
X,
|
||||
Minimize2,
|
||||
Maximize2,
|
||||
CheckCircle,
|
||||
XCircle,
|
||||
Trophy,
|
||||
SlidersHorizontal,
|
||||
Target,
|
||||
AlertTriangle,
|
||||
Clock,
|
||||
} from 'lucide-react';
|
||||
import { useResultsPanel, usePanelStore } from '../../../hooks/usePanelStore';
|
||||
|
||||
interface ResultsPanelProps {
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
export function ResultsPanel({ onClose }: ResultsPanelProps) {
|
||||
const panel = useResultsPanel();
|
||||
const { minimizePanel } = usePanelStore();
|
||||
const data = panel.data;
|
||||
|
||||
if (!panel.open || !data) return null;
|
||||
|
||||
const timestamp = new Date(data.timestamp).toLocaleTimeString();
|
||||
|
||||
// Minimized view
|
||||
if (panel.minimized) {
|
||||
return (
|
||||
<div
|
||||
className="bg-dark-850 border border-dark-700 rounded-lg shadow-xl flex items-center gap-2 px-3 py-2 cursor-pointer hover:bg-dark-800 transition-colors"
|
||||
onClick={() => minimizePanel('results')}
|
||||
>
|
||||
<Trophy size={16} className={data.isBest ? 'text-amber-400' : 'text-dark-400'} />
|
||||
<span className="text-sm text-white font-medium">
|
||||
Trial #{data.trialNumber}
|
||||
</span>
|
||||
<Maximize2 size={14} className="text-dark-400" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="bg-dark-850 border border-dark-700 rounded-xl w-80 max-h-[500px] flex flex-col shadow-xl">
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between px-4 py-3 border-b border-dark-700">
|
||||
<div className="flex items-center gap-2">
|
||||
<Trophy size={18} className={data.isBest ? 'text-amber-400' : 'text-dark-400'} />
|
||||
<span className="font-medium text-white">
|
||||
Trial #{data.trialNumber}
|
||||
</span>
|
||||
{data.isBest && (
|
||||
<span className="px-1.5 py-0.5 text-xs bg-amber-500/20 text-amber-400 rounded">
|
||||
Best
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex items-center gap-1">
|
||||
<button
|
||||
onClick={() => minimizePanel('results')}
|
||||
className="p-1.5 text-dark-400 hover:text-white hover:bg-dark-700 rounded transition-colors"
|
||||
title="Minimize"
|
||||
>
|
||||
<Minimize2 size={14} />
|
||||
</button>
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="p-1.5 text-dark-400 hover:text-white hover:bg-dark-700 rounded transition-colors"
|
||||
>
|
||||
<X size={14} />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Content */}
|
||||
<div className="flex-1 overflow-y-auto p-3 space-y-4">
|
||||
{/* Status */}
|
||||
<div className="flex items-center gap-3">
|
||||
{data.isFeasible ? (
|
||||
<div className="flex items-center gap-1.5 text-green-400">
|
||||
<CheckCircle size={16} />
|
||||
<span className="text-sm font-medium">Feasible</span>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex items-center gap-1.5 text-red-400">
|
||||
<XCircle size={16} />
|
||||
<span className="text-sm font-medium">Infeasible</span>
|
||||
</div>
|
||||
)}
|
||||
<div className="flex items-center gap-1.5 text-dark-400 ml-auto">
|
||||
<Clock size={14} />
|
||||
<span className="text-xs">{timestamp}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Parameters */}
|
||||
<div>
|
||||
<h4 className="text-xs font-medium text-dark-400 uppercase tracking-wide mb-2 flex items-center gap-1.5">
|
||||
<SlidersHorizontal size={12} />
|
||||
Parameters
|
||||
</h4>
|
||||
<div className="space-y-1">
|
||||
{Object.entries(data.params).map(([name, value]) => (
|
||||
<div key={name} className="flex justify-between p-2 bg-dark-800 rounded text-sm">
|
||||
<span className="text-dark-300">{name}</span>
|
||||
<span className="text-white font-mono">{formatValue(value)}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Objectives */}
|
||||
<div>
|
||||
<h4 className="text-xs font-medium text-dark-400 uppercase tracking-wide mb-2 flex items-center gap-1.5">
|
||||
<Target size={12} />
|
||||
Objectives
|
||||
</h4>
|
||||
<div className="space-y-1">
|
||||
{Object.entries(data.objectives).map(([name, value]) => (
|
||||
<div key={name} className="flex justify-between p-2 bg-dark-800 rounded text-sm">
|
||||
<span className="text-dark-300">{name}</span>
|
||||
<span className="text-primary-400 font-mono">{formatValue(value)}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Constraints (if any) */}
|
||||
{data.constraints && Object.keys(data.constraints).length > 0 && (
|
||||
<div>
|
||||
<h4 className="text-xs font-medium text-dark-400 uppercase tracking-wide mb-2 flex items-center gap-1.5">
|
||||
<AlertTriangle size={12} />
|
||||
Constraints
|
||||
</h4>
|
||||
<div className="space-y-1">
|
||||
{Object.entries(data.constraints).map(([name, constraint]) => (
|
||||
<div
|
||||
key={name}
|
||||
className={`flex justify-between p-2 rounded text-sm ${
|
||||
constraint.feasible ? 'bg-dark-800' : 'bg-red-500/10 border border-red-500/20'
|
||||
}`}
|
||||
>
|
||||
<span className="text-dark-300 flex items-center gap-1.5">
|
||||
{constraint.feasible ? (
|
||||
<CheckCircle size={12} className="text-green-400" />
|
||||
) : (
|
||||
<XCircle size={12} className="text-red-400" />
|
||||
)}
|
||||
{name}
|
||||
</span>
|
||||
<span className={`font-mono ${constraint.feasible ? 'text-white' : 'text-red-400'}`}>
|
||||
{formatValue(constraint.value)}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function formatValue(value: number): string {
|
||||
if (Math.abs(value) < 0.001 || Math.abs(value) >= 10000) {
|
||||
return value.toExponential(3);
|
||||
}
|
||||
return value.toFixed(4).replace(/\.?0+$/, '');
|
||||
}
|
||||
|
||||
export default ResultsPanel;
|
||||
Reference in New Issue
Block a user