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
376 lines
11 KiB
TypeScript
376 lines
11 KiB
TypeScript
/**
|
|
* 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,
|
|
}));
|