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:
@@ -11,7 +11,7 @@
|
||||
*/
|
||||
|
||||
import { useCallback, useRef, useEffect, useMemo, useState, DragEvent } from 'react';
|
||||
import { Play, Square, Loader2, Eye, EyeOff } from 'lucide-react';
|
||||
import { Play, Square, Loader2, Eye, EyeOff, CheckCircle, AlertCircle } from 'lucide-react';
|
||||
import ReactFlow, {
|
||||
Background,
|
||||
Controls,
|
||||
@@ -38,8 +38,10 @@ import {
|
||||
useSelectedEdgeId,
|
||||
} from '../../hooks/useSpecStore';
|
||||
import { useSpecWebSocket } from '../../hooks/useSpecWebSocket';
|
||||
import { usePanelStore } from '../../hooks/usePanelStore';
|
||||
import { ConnectionStatusIndicator } from './ConnectionStatusIndicator';
|
||||
import { CanvasNodeData } from '../../lib/canvas/schema';
|
||||
import { validateSpec, canRunOptimization } from '../../lib/validation/specValidator';
|
||||
|
||||
// ============================================================================
|
||||
// Drag-Drop Helpers
|
||||
@@ -202,11 +204,18 @@ function SpecRendererInner({
|
||||
const wsStudyId = enableWebSocket ? storeStudyId : null;
|
||||
const { status: wsStatus } = useSpecWebSocket(wsStudyId);
|
||||
|
||||
// Panel store for validation and error panels
|
||||
const { setValidationData, addError, openPanel } = usePanelStore();
|
||||
|
||||
// Optimization execution state
|
||||
const [isRunning, setIsRunning] = useState(false);
|
||||
const [isStarting, setIsStarting] = useState(false);
|
||||
const [bestTrial, setBestTrial] = useState<any>(null);
|
||||
const [showResults, setShowResults] = useState(false);
|
||||
const [validationStatus, setValidationStatus] = useState<'valid' | 'invalid' | 'unchecked'>('unchecked');
|
||||
|
||||
// Track last seen error timestamp to avoid duplicates
|
||||
const lastErrorTimestamp = useRef<number>(0);
|
||||
|
||||
// Poll optimization status
|
||||
useEffect(() => {
|
||||
@@ -226,19 +235,119 @@ function SpecRendererInner({
|
||||
setShowResults(true);
|
||||
}
|
||||
}
|
||||
|
||||
// Handle errors from the optimization process
|
||||
if (data.error && data.error_timestamp && data.error_timestamp > lastErrorTimestamp.current) {
|
||||
lastErrorTimestamp.current = data.error_timestamp;
|
||||
|
||||
// Classify the error based on the message
|
||||
let errorType: 'nx_crash' | 'solver_fail' | 'extractor_error' | 'config_error' | 'system_error' | 'unknown' = 'unknown';
|
||||
const errorMsg = data.error.toLowerCase();
|
||||
|
||||
if (errorMsg.includes('nx') || errorMsg.includes('siemens') || errorMsg.includes('journal')) {
|
||||
errorType = 'nx_crash';
|
||||
} else if (errorMsg.includes('solver') || errorMsg.includes('nastran') || errorMsg.includes('convergence')) {
|
||||
errorType = 'solver_fail';
|
||||
} else if (errorMsg.includes('extractor') || errorMsg.includes('extract') || errorMsg.includes('op2')) {
|
||||
errorType = 'extractor_error';
|
||||
} else if (errorMsg.includes('config') || errorMsg.includes('spec') || errorMsg.includes('parameter')) {
|
||||
errorType = 'config_error';
|
||||
} else if (errorMsg.includes('system') || errorMsg.includes('permission') || errorMsg.includes('disk')) {
|
||||
errorType = 'system_error';
|
||||
}
|
||||
|
||||
// Generate suggestions based on error type
|
||||
const suggestions: string[] = [];
|
||||
switch (errorType) {
|
||||
case 'nx_crash':
|
||||
suggestions.push('Check if NX is running and licensed');
|
||||
suggestions.push('Verify the model file is not corrupted');
|
||||
suggestions.push('Try closing and reopening NX');
|
||||
break;
|
||||
case 'solver_fail':
|
||||
suggestions.push('Check the mesh quality in the FEM file');
|
||||
suggestions.push('Verify boundary conditions are properly defined');
|
||||
suggestions.push('Review the solver settings');
|
||||
break;
|
||||
case 'extractor_error':
|
||||
suggestions.push('Verify the OP2 file was created successfully');
|
||||
suggestions.push('Check if the extractor type matches the analysis');
|
||||
suggestions.push('For custom extractors, review the code for errors');
|
||||
break;
|
||||
case 'config_error':
|
||||
suggestions.push('Run validation to check the spec');
|
||||
suggestions.push('Verify all design variables have valid bounds');
|
||||
break;
|
||||
default:
|
||||
suggestions.push('Check the optimization logs for more details');
|
||||
}
|
||||
|
||||
addError({
|
||||
type: errorType,
|
||||
trial: data.current_trial,
|
||||
message: data.error,
|
||||
details: data.error_details,
|
||||
recoverable: errorType !== 'config_error',
|
||||
suggestions,
|
||||
timestamp: data.error_timestamp || Date.now(),
|
||||
});
|
||||
}
|
||||
|
||||
// Handle failed status
|
||||
if (data.status === 'failed' && data.error) {
|
||||
setIsRunning(false);
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
// Silent fail on polling
|
||||
// Silent fail on polling - network issues shouldn't spam errors
|
||||
console.debug('Status poll failed:', e);
|
||||
}
|
||||
};
|
||||
|
||||
checkStatus();
|
||||
const interval = setInterval(checkStatus, 3000);
|
||||
return () => clearInterval(interval);
|
||||
}, [studyId]);
|
||||
}, [studyId, addError]);
|
||||
|
||||
// Validate the spec and show results in panel
|
||||
const handleValidate = useCallback(() => {
|
||||
if (!spec) return;
|
||||
|
||||
const result = validateSpec(spec);
|
||||
setValidationData(result);
|
||||
setValidationStatus(result.valid ? 'valid' : 'invalid');
|
||||
|
||||
// Auto-open validation panel if there are issues
|
||||
if (!result.valid || result.warnings.length > 0) {
|
||||
openPanel('validation');
|
||||
}
|
||||
|
||||
return result;
|
||||
}, [spec, setValidationData, openPanel]);
|
||||
|
||||
const handleRun = async () => {
|
||||
if (!studyId) return;
|
||||
if (!studyId || !spec) return;
|
||||
|
||||
// Validate before running
|
||||
const validation = handleValidate();
|
||||
if (!validation || !validation.valid) {
|
||||
// Show validation panel with errors
|
||||
return;
|
||||
}
|
||||
|
||||
// Also do a quick sanity check
|
||||
const { canRun, reason } = canRunOptimization(spec);
|
||||
if (!canRun) {
|
||||
addError({
|
||||
type: 'config_error',
|
||||
message: reason || 'Cannot run optimization',
|
||||
recoverable: false,
|
||||
suggestions: ['Check the validation panel for details'],
|
||||
timestamp: Date.now(),
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
setIsStarting(true);
|
||||
try {
|
||||
const res = await fetch(`/api/optimization/studies/${studyId}/run`, {
|
||||
@@ -251,8 +360,19 @@ function SpecRendererInner({
|
||||
throw new Error(err.detail || 'Failed to start');
|
||||
}
|
||||
setIsRunning(true);
|
||||
setValidationStatus('unchecked'); // Clear validation status when running
|
||||
} catch (e) {
|
||||
setError(e instanceof Error ? e.message : 'Failed to start optimization');
|
||||
const errorMessage = e instanceof Error ? e.message : 'Failed to start optimization';
|
||||
setError(errorMessage);
|
||||
|
||||
// Also add to error panel for persistence
|
||||
addError({
|
||||
type: 'system_error',
|
||||
message: errorMessage,
|
||||
recoverable: true,
|
||||
suggestions: ['Check if the backend is running', 'Verify the study configuration'],
|
||||
timestamp: Date.now(),
|
||||
});
|
||||
} finally {
|
||||
setIsStarting(false);
|
||||
}
|
||||
@@ -261,10 +381,22 @@ function SpecRendererInner({
|
||||
const handleStop = async () => {
|
||||
if (!studyId) return;
|
||||
try {
|
||||
await fetch(`/api/optimization/studies/${studyId}/stop`, { method: 'POST' });
|
||||
const res = await fetch(`/api/optimization/studies/${studyId}/stop`, { method: 'POST' });
|
||||
if (!res.ok) {
|
||||
const err = await res.json().catch(() => ({}));
|
||||
throw new Error(err.detail || 'Failed to stop');
|
||||
}
|
||||
setIsRunning(false);
|
||||
} catch (e) {
|
||||
setError(e instanceof Error ? e.message : 'Failed to stop optimization');
|
||||
const errorMessage = e instanceof Error ? e.message : 'Failed to stop optimization';
|
||||
setError(errorMessage);
|
||||
addError({
|
||||
type: 'system_error',
|
||||
message: errorMessage,
|
||||
recoverable: false,
|
||||
suggestions: ['The optimization may still be running in the background'],
|
||||
timestamp: Date.now(),
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
@@ -637,6 +769,7 @@ function SpecRendererInner({
|
||||
|
||||
{/* Action Buttons */}
|
||||
<div className="absolute bottom-4 right-4 z-10 flex gap-2">
|
||||
{/* Results toggle */}
|
||||
{bestTrial && (
|
||||
<button
|
||||
onClick={() => setShowResults(!showResults)}
|
||||
@@ -652,26 +785,54 @@ function SpecRendererInner({
|
||||
</button>
|
||||
)}
|
||||
|
||||
{/* Validate button - shows validation status */}
|
||||
<button
|
||||
onClick={handleValidate}
|
||||
className={`flex items-center gap-2 px-3 py-2 rounded-lg transition-colors border ${
|
||||
validationStatus === 'valid'
|
||||
? 'bg-green-600/20 text-green-400 border-green-500/50 hover:bg-green-600/30'
|
||||
: validationStatus === 'invalid'
|
||||
? 'bg-red-600/20 text-red-400 border-red-500/50 hover:bg-red-600/30'
|
||||
: 'bg-dark-800 text-dark-300 border-dark-600 hover:text-white hover:border-dark-500'
|
||||
}`}
|
||||
title="Validate spec before running"
|
||||
>
|
||||
{validationStatus === 'valid' ? (
|
||||
<CheckCircle size={16} />
|
||||
) : validationStatus === 'invalid' ? (
|
||||
<AlertCircle size={16} />
|
||||
) : (
|
||||
<CheckCircle size={16} />
|
||||
)}
|
||||
<span className="text-sm font-medium">Validate</span>
|
||||
</button>
|
||||
|
||||
{/* Run/Stop button */}
|
||||
{isRunning ? (
|
||||
<button
|
||||
onClick={handleStop}
|
||||
className="flex items-center gap-2 px-4 py-2 bg-red-600 text-white rounded-lg hover:bg-red-500 shadow-lg transition-colors font-medium"
|
||||
>
|
||||
<Square size={16} fill="currentColor" />
|
||||
Stop Optimization
|
||||
Stop
|
||||
</button>
|
||||
) : (
|
||||
<button
|
||||
onClick={handleRun}
|
||||
disabled={isStarting}
|
||||
className="flex items-center gap-2 px-4 py-2 bg-emerald-600 text-white rounded-lg hover:bg-emerald-500 shadow-lg transition-colors font-medium disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
disabled={isStarting || validationStatus === 'invalid'}
|
||||
className={`flex items-center gap-2 px-4 py-2 rounded-lg shadow-lg transition-colors font-medium ${
|
||||
validationStatus === 'invalid'
|
||||
? 'bg-dark-700 text-dark-400 cursor-not-allowed'
|
||||
: 'bg-emerald-600 text-white hover:bg-emerald-500 disabled:opacity-50 disabled:cursor-not-allowed'
|
||||
}`}
|
||||
title={validationStatus === 'invalid' ? 'Fix validation errors first' : 'Start optimization'}
|
||||
>
|
||||
{isStarting ? (
|
||||
<Loader2 size={16} className="animate-spin" />
|
||||
) : (
|
||||
<Play size={16} fill="currentColor" />
|
||||
)}
|
||||
Run Optimization
|
||||
Run
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user