/** * 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; 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; objectives: Record; constraints?: Record; 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) => 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()( 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, }));