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:
375
atomizer-dashboard/frontend/src/hooks/usePanelStore.ts
Normal file
375
atomizer-dashboard/frontend/src/hooks/usePanelStore.ts
Normal file
@@ -0,0 +1,375 @@
|
||||
/**
|
||||
* usePanelStore - Centralized state management for canvas panels
|
||||
*
|
||||
* This store manages the visibility and state of all panels in the canvas view.
|
||||
* Panels persist their state even when the user clicks elsewhere on the canvas.
|
||||
*
|
||||
* Panel Types:
|
||||
* - introspection: Model introspection results (floating, draggable)
|
||||
* - validation: Spec validation errors/warnings (floating)
|
||||
* - results: Trial results details (floating)
|
||||
* - error: Error display with recovery options (floating)
|
||||
*/
|
||||
|
||||
import { create } from 'zustand';
|
||||
import { persist } from 'zustand/middleware';
|
||||
|
||||
// ============================================================================
|
||||
// Types
|
||||
// ============================================================================
|
||||
|
||||
export interface IntrospectionData {
|
||||
filePath: string;
|
||||
studyId?: string;
|
||||
selectedFile?: string;
|
||||
result?: Record<string, unknown>;
|
||||
isLoading?: boolean;
|
||||
error?: string | null;
|
||||
}
|
||||
|
||||
export interface ValidationError {
|
||||
code: string;
|
||||
severity: 'error' | 'warning';
|
||||
path: string;
|
||||
message: string;
|
||||
suggestion?: string;
|
||||
nodeId?: string;
|
||||
}
|
||||
|
||||
export interface ValidationData {
|
||||
valid: boolean;
|
||||
errors: ValidationError[];
|
||||
warnings: ValidationError[];
|
||||
checkedAt: number;
|
||||
}
|
||||
|
||||
export interface OptimizationError {
|
||||
type: 'nx_crash' | 'solver_fail' | 'extractor_error' | 'config_error' | 'system_error' | 'unknown';
|
||||
trial?: number;
|
||||
message: string;
|
||||
details?: string;
|
||||
recoverable: boolean;
|
||||
suggestions: string[];
|
||||
timestamp: number;
|
||||
}
|
||||
|
||||
export interface TrialResultData {
|
||||
trialNumber: number;
|
||||
params: Record<string, number>;
|
||||
objectives: Record<string, number>;
|
||||
constraints?: Record<string, { value: number; feasible: boolean }>;
|
||||
isFeasible: boolean;
|
||||
isBest: boolean;
|
||||
timestamp: number;
|
||||
}
|
||||
|
||||
export interface PanelPosition {
|
||||
x: number;
|
||||
y: number;
|
||||
}
|
||||
|
||||
export interface PanelState {
|
||||
open: boolean;
|
||||
position?: PanelPosition;
|
||||
minimized?: boolean;
|
||||
}
|
||||
|
||||
export interface IntrospectionPanelState extends PanelState {
|
||||
data?: IntrospectionData;
|
||||
}
|
||||
|
||||
export interface ValidationPanelState extends PanelState {
|
||||
data?: ValidationData;
|
||||
}
|
||||
|
||||
export interface ErrorPanelState extends PanelState {
|
||||
errors: OptimizationError[];
|
||||
}
|
||||
|
||||
export interface ResultsPanelState extends PanelState {
|
||||
data?: TrialResultData;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Store Interface
|
||||
// ============================================================================
|
||||
|
||||
interface PanelStore {
|
||||
// Panel states
|
||||
introspection: IntrospectionPanelState;
|
||||
validation: ValidationPanelState;
|
||||
error: ErrorPanelState;
|
||||
results: ResultsPanelState;
|
||||
|
||||
// Generic panel actions
|
||||
openPanel: (panel: 'introspection' | 'validation' | 'error' | 'results') => void;
|
||||
closePanel: (panel: 'introspection' | 'validation' | 'error' | 'results') => void;
|
||||
togglePanel: (panel: 'introspection' | 'validation' | 'error' | 'results') => void;
|
||||
minimizePanel: (panel: 'introspection' | 'validation' | 'error' | 'results') => void;
|
||||
setPanelPosition: (panel: 'introspection' | 'validation' | 'error' | 'results', position: PanelPosition) => void;
|
||||
|
||||
// Introspection-specific actions
|
||||
setIntrospectionData: (data: IntrospectionData) => void;
|
||||
updateIntrospectionResult: (result: Record<string, unknown>) => void;
|
||||
setIntrospectionLoading: (loading: boolean) => void;
|
||||
setIntrospectionError: (error: string | null) => void;
|
||||
setIntrospectionFile: (fileName: string) => void;
|
||||
|
||||
// Validation-specific actions
|
||||
setValidationData: (data: ValidationData) => void;
|
||||
clearValidation: () => void;
|
||||
|
||||
// Error-specific actions
|
||||
addError: (error: OptimizationError) => void;
|
||||
clearErrors: () => void;
|
||||
dismissError: (timestamp: number) => void;
|
||||
|
||||
// Results-specific actions
|
||||
setTrialResult: (data: TrialResultData) => void;
|
||||
clearTrialResult: () => void;
|
||||
|
||||
// Utility
|
||||
closeAllPanels: () => void;
|
||||
hasOpenPanels: () => boolean;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Default States
|
||||
// ============================================================================
|
||||
|
||||
const defaultIntrospection: IntrospectionPanelState = {
|
||||
open: false,
|
||||
position: { x: 100, y: 100 },
|
||||
minimized: false,
|
||||
data: undefined,
|
||||
};
|
||||
|
||||
const defaultValidation: ValidationPanelState = {
|
||||
open: false,
|
||||
position: { x: 150, y: 150 },
|
||||
minimized: false,
|
||||
data: undefined,
|
||||
};
|
||||
|
||||
const defaultError: ErrorPanelState = {
|
||||
open: false,
|
||||
position: { x: 200, y: 100 },
|
||||
minimized: false,
|
||||
errors: [],
|
||||
};
|
||||
|
||||
const defaultResults: ResultsPanelState = {
|
||||
open: false,
|
||||
position: { x: 250, y: 150 },
|
||||
minimized: false,
|
||||
data: undefined,
|
||||
};
|
||||
|
||||
// ============================================================================
|
||||
// Store Implementation
|
||||
// ============================================================================
|
||||
|
||||
export const usePanelStore = create<PanelStore>()(
|
||||
persist(
|
||||
(set, get) => ({
|
||||
// Initial states
|
||||
introspection: defaultIntrospection,
|
||||
validation: defaultValidation,
|
||||
error: defaultError,
|
||||
results: defaultResults,
|
||||
|
||||
// Generic panel actions
|
||||
openPanel: (panel) => set((state) => ({
|
||||
[panel]: { ...state[panel], open: true, minimized: false }
|
||||
})),
|
||||
|
||||
closePanel: (panel) => set((state) => ({
|
||||
[panel]: { ...state[panel], open: false }
|
||||
})),
|
||||
|
||||
togglePanel: (panel) => set((state) => ({
|
||||
[panel]: { ...state[panel], open: !state[panel].open, minimized: false }
|
||||
})),
|
||||
|
||||
minimizePanel: (panel) => set((state) => ({
|
||||
[panel]: { ...state[panel], minimized: !state[panel].minimized }
|
||||
})),
|
||||
|
||||
setPanelPosition: (panel, position) => set((state) => ({
|
||||
[panel]: { ...state[panel], position }
|
||||
})),
|
||||
|
||||
// Introspection actions
|
||||
setIntrospectionData: (data) => set((state) => ({
|
||||
introspection: {
|
||||
...state.introspection,
|
||||
open: true,
|
||||
data
|
||||
}
|
||||
})),
|
||||
|
||||
updateIntrospectionResult: (result) => set((state) => ({
|
||||
introspection: {
|
||||
...state.introspection,
|
||||
data: state.introspection.data
|
||||
? { ...state.introspection.data, result, isLoading: false, error: null }
|
||||
: undefined
|
||||
}
|
||||
})),
|
||||
|
||||
setIntrospectionLoading: (loading) => set((state) => ({
|
||||
introspection: {
|
||||
...state.introspection,
|
||||
data: state.introspection.data
|
||||
? { ...state.introspection.data, isLoading: loading }
|
||||
: undefined
|
||||
}
|
||||
})),
|
||||
|
||||
setIntrospectionError: (error) => set((state) => ({
|
||||
introspection: {
|
||||
...state.introspection,
|
||||
data: state.introspection.data
|
||||
? { ...state.introspection.data, error, isLoading: false }
|
||||
: undefined
|
||||
}
|
||||
})),
|
||||
|
||||
setIntrospectionFile: (fileName) => set((state) => ({
|
||||
introspection: {
|
||||
...state.introspection,
|
||||
data: state.introspection.data
|
||||
? { ...state.introspection.data, selectedFile: fileName }
|
||||
: undefined
|
||||
}
|
||||
})),
|
||||
|
||||
// Validation actions
|
||||
setValidationData: (data) => set((state) => ({
|
||||
validation: {
|
||||
...state.validation,
|
||||
open: true,
|
||||
data
|
||||
}
|
||||
})),
|
||||
|
||||
clearValidation: () => set((state) => ({
|
||||
validation: {
|
||||
...state.validation,
|
||||
data: undefined
|
||||
}
|
||||
})),
|
||||
|
||||
// Error actions
|
||||
addError: (error) => set((state) => ({
|
||||
error: {
|
||||
...state.error,
|
||||
open: true,
|
||||
errors: [...state.error.errors, error]
|
||||
}
|
||||
})),
|
||||
|
||||
clearErrors: () => set((state) => ({
|
||||
error: {
|
||||
...state.error,
|
||||
errors: [],
|
||||
open: false
|
||||
}
|
||||
})),
|
||||
|
||||
dismissError: (timestamp) => set((state) => {
|
||||
const newErrors = state.error.errors.filter(e => e.timestamp !== timestamp);
|
||||
return {
|
||||
error: {
|
||||
...state.error,
|
||||
errors: newErrors,
|
||||
open: newErrors.length > 0
|
||||
}
|
||||
};
|
||||
}),
|
||||
|
||||
// Results actions
|
||||
setTrialResult: (data) => set((state) => ({
|
||||
results: {
|
||||
...state.results,
|
||||
open: true,
|
||||
data
|
||||
}
|
||||
})),
|
||||
|
||||
clearTrialResult: () => set((state) => ({
|
||||
results: {
|
||||
...state.results,
|
||||
data: undefined,
|
||||
open: false
|
||||
}
|
||||
})),
|
||||
|
||||
// Utility
|
||||
closeAllPanels: () => set({
|
||||
introspection: { ...get().introspection, open: false },
|
||||
validation: { ...get().validation, open: false },
|
||||
error: { ...get().error, open: false },
|
||||
results: { ...get().results, open: false },
|
||||
}),
|
||||
|
||||
hasOpenPanels: () => {
|
||||
const state = get();
|
||||
return state.introspection.open ||
|
||||
state.validation.open ||
|
||||
state.error.open ||
|
||||
state.results.open;
|
||||
},
|
||||
}),
|
||||
{
|
||||
name: 'atomizer-panel-store',
|
||||
// Only persist certain fields (not loading states or errors)
|
||||
partialize: (state) => ({
|
||||
introspection: {
|
||||
position: state.introspection.position,
|
||||
// Don't persist open state - start fresh each session
|
||||
},
|
||||
validation: {
|
||||
position: state.validation.position,
|
||||
},
|
||||
error: {
|
||||
position: state.error.position,
|
||||
},
|
||||
results: {
|
||||
position: state.results.position,
|
||||
},
|
||||
}),
|
||||
}
|
||||
)
|
||||
);
|
||||
|
||||
// ============================================================================
|
||||
// Selector Hooks (for convenience)
|
||||
// ============================================================================
|
||||
|
||||
export const useIntrospectionPanel = () => usePanelStore((state) => state.introspection);
|
||||
export const useValidationPanel = () => usePanelStore((state) => state.validation);
|
||||
export const useErrorPanel = () => usePanelStore((state) => state.error);
|
||||
export const useResultsPanel = () => usePanelStore((state) => state.results);
|
||||
|
||||
// Actions
|
||||
export const usePanelActions = () => usePanelStore((state) => ({
|
||||
openPanel: state.openPanel,
|
||||
closePanel: state.closePanel,
|
||||
togglePanel: state.togglePanel,
|
||||
minimizePanel: state.minimizePanel,
|
||||
setPanelPosition: state.setPanelPosition,
|
||||
setIntrospectionData: state.setIntrospectionData,
|
||||
updateIntrospectionResult: state.updateIntrospectionResult,
|
||||
setIntrospectionLoading: state.setIntrospectionLoading,
|
||||
setIntrospectionError: state.setIntrospectionError,
|
||||
setIntrospectionFile: state.setIntrospectionFile,
|
||||
setValidationData: state.setValidationData,
|
||||
clearValidation: state.clearValidation,
|
||||
addError: state.addError,
|
||||
clearErrors: state.clearErrors,
|
||||
dismissError: state.dismissError,
|
||||
setTrialResult: state.setTrialResult,
|
||||
clearTrialResult: state.clearTrialResult,
|
||||
closeAllPanels: state.closeAllPanels,
|
||||
}));
|
||||
Reference in New Issue
Block a user