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
180 lines
6.2 KiB
TypeScript
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;
|