Files
Atomizer/atomizer-dashboard/frontend/src/components/canvas/panels/ResultsPanel.tsx
Anto01 c224b16ac3 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
2026-01-21 21:35:31 -05:00

180 lines
6.2 KiB
TypeScript

/**
* 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;