Files
Atomizer/docs/archive/plans/CANVAS_V3_PLAN.md
Anto01 a3f18dc377 chore: Project cleanup and Canvas UX improvements (Phase 7-9)
## Cleanup (v0.5.0)
- Delete 102+ orphaned MCP session temp files
- Remove build artifacts (htmlcov, dist, __pycache__)
- Archive superseded plan docs (RALPH_LOOP V2/V3, CANVAS V3, etc.)
- Move debug/analysis scripts from tests/ to tools/analysis/
- Archive redundant NX journals to archive/nx_journals/
- Archive monolithic PROTOCOL.md to docs/archive/
- Update .gitignore with missing patterns
- Clean old study files (optimization_log_old.txt, run_optimization_old.py)

## Canvas UX (Phases 7-9)
- Phase 7: Resizable panels with localStorage persistence
  - Left sidebar: 200-400px, Right panel: 280-600px
  - New useResizablePanel hook and ResizeHandle component
- Phase 8: Enable all palette items
  - All 8 node types now draggable
  - Singleton logic for model/solver/algorithm/surrogate
- Phase 9: Solver configuration
  - Add SolverEngine type (nxnastran, mscnastran, python, etc.)
  - Add NastranSolutionType (SOL101-SOL200)
  - Engine/solution dropdowns in config panel
  - Python script path support

## Documentation
- Update CHANGELOG.md with recent versions
- Update docs/00_INDEX.md
- Create examples/README.md
- Add docs/plans/CANVAS_UX_IMPROVEMENTS.md
2026-01-24 15:17:34 -05:00

16 KiB

Canvas V3 - Comprehensive Fix & Enhancement Plan

Created: January 16, 2026 Status: Planning Priority: High


Problem Analysis

Critical Bugs (Must Fix)

Issue Impact Root Cause (Likely)
Atomizer Assistant broken High - core feature unusable WebSocket connection or chat hook error
Cannot delete connections High - workflow editing blocked Missing edge selection/delete handler
Drag & drop positioning wrong Medium - UX frustration Position calculation not accounting for scroll/zoom

Data Loading Issues

Issue Impact Root Cause (Likely)
Auto-connect missing High - manual work required loadFromConfig creates nodes but not edges
Missing elements (OPD extractor) High - incomplete workflows Incomplete parsing of optimization_config.json
Constraints not shown High - incomplete workflows Constraints not parsed from config
Algorithm not pre-selected Medium - extra clicks Algorithm node not created from config

UI/UX Issues

Issue Impact Root Cause (Likely)
Interface too small Medium - wasted screen space Fixed dimensions, not responsive
Insufficient contrast Medium - accessibility White text on light blue background
Font size too small Low - readability Hardcoded small font sizes

Feature Requests

Feature Value Complexity
Auto-complete with Claude High - smart assistance Medium
Templates/guidelines Medium - onboarding Low
Canvas ↔ Assistant integration High - conversational control High

Implementation Strategy

Phased Approach

Phase 1: Critical Fixes       (2 hours)
    ↓
Phase 2: Data Loading         (3 hours)
    ↓
Phase 3: UI/UX Polish         (2 hours)
    ↓
Phase 4: Claude Integration   (3 hours)
    ↓
Phase 5: Testing & Commit     (1 hour)

Phase 1: Critical Bug Fixes

1.1 Fix Atomizer Assistant Error

Investigation Steps:

  1. Check ChatPanel.tsx for error handling
  2. Check useCanvasChat.ts hook for connection issues
  3. Verify WebSocket endpoint /api/chat/ is working
  4. Check if useChat.ts base hook has errors

Likely Fix:

  • Add error boundary around chat component
  • Add null checks for WebSocket connection
  • Provide fallback UI when chat unavailable

1.2 Enable Connection Deletion

Current State: Edges can't be selected or deleted

Implementation:

// In AtomizerCanvas.tsx
<ReactFlow
  edgesUpdatable={true}
  edgesFocusable={true}
  deleteKeyCode={['Backspace', 'Delete']}
  onEdgeClick={(event, edge) => selectEdge(edge.id)}
/>

Store Update:

// In useCanvasStore.ts
deleteEdge: (edgeId: string) => {
  set((state) => ({
    edges: state.edges.filter((e) => e.id !== edgeId),
  }));
},

1.3 Fix Drag & Drop Positioning

Problem: New nodes appear at wrong position (not where dropped)

Fix in AtomizerCanvas.tsx:

const onDrop = useCallback((event: DragEvent) => {
  event.preventDefault();

  if (!reactFlowInstance.current || !reactFlowWrapper.current) return;

  const type = event.dataTransfer.getData('application/reactflow') as NodeType;
  if (!type) return;

  // Get correct position accounting for viewport transform
  const bounds = reactFlowWrapper.current.getBoundingClientRect();
  const position = reactFlowInstance.current.screenToFlowPosition({
    x: event.clientX - bounds.left,
    y: event.clientY - bounds.top,
  });

  addNode(type, position);
}, [addNode]);

Phase 2: Data Loading Improvements

2.1 Enhanced loadFromConfig Function

Goal: When loading a study, create ALL nodes AND edges automatically.

Current Problems:

  • Nodes created but not connected
  • Some extractors/constraints missing
  • Algorithm not created

New Implementation Strategy:

loadFromConfig: (config: OptimizationConfig) => {
  const nodes: Node[] = [];
  const edges: Edge[] = [];

  // Layout constants
  const COLS = { model: 50, dvar: 50, solver: 250, extractor: 450, obj: 650, algo: 850 };
  const ROW_HEIGHT = 100;
  const START_Y = 100;

  // Track IDs for connections
  const nodeIds = {
    model: '',
    solver: '',
    dvars: [] as string[],
    extractors: {} as Record<string, string>,  // extractor_id -> node_id
    objectives: [] as string[],
    constraints: [] as string[],
    algorithm: '',
  };

  // 1. Create Model Node
  if (config.nx_model) {
    const id = `model_${Date.now()}`;
    nodeIds.model = id;
    nodes.push({
      id,
      type: 'model',
      position: { x: COLS.model, y: START_Y },
      data: {
        type: 'model',
        label: 'Model',
        configured: true,
        filePath: config.nx_model.sim_path || config.nx_model.prt_path,
        fileType: config.nx_model.sim_path ? 'sim' : 'prt',
      },
    });
  }

  // 2. Create Solver Node
  if (config.solver) {
    const id = `solver_${Date.now()}`;
    nodeIds.solver = id;
    nodes.push({
      id,
      type: 'solver',
      position: { x: COLS.solver, y: START_Y },
      data: {
        type: 'solver',
        label: 'Solver',
        configured: true,
        solverType: `SOL${config.solver.solution_type}`,
      },
    });

    // Connect Model → Solver
    if (nodeIds.model) {
      edges.push({
        id: `e_model_solver`,
        source: nodeIds.model,
        target: nodeIds.solver,
        animated: true,
      });
    }
  }

  // 3. Create Design Variables (connected to Model)
  config.design_variables?.forEach((dv, i) => {
    const id = `dvar_${i}_${Date.now()}`;
    nodeIds.dvars.push(id);
    nodes.push({
      id,
      type: 'designVar',
      position: { x: COLS.dvar, y: START_Y + 150 + i * ROW_HEIGHT },
      data: {
        type: 'designVar',
        label: dv.name,
        configured: true,
        expressionName: dv.nx_expression || dv.name,
        minValue: dv.lower_bound,
        maxValue: dv.upper_bound,
        unit: dv.unit,
      },
    });

    // Connect DVar → Model (or keep disconnected, they're inputs)
  });

  // 4. Create Extractors from objectives AND constraints
  const allExtractors = new Set<string>();
  config.objectives?.forEach(obj => allExtractors.add(obj.extractor_id));
  config.constraints?.forEach(con => {
    if (con.extractor_id) allExtractors.add(con.extractor_id);
  });

  let extractorRow = 0;
  allExtractors.forEach((extractorId) => {
    const id = `extractor_${extractorId}_${Date.now()}`;
    nodeIds.extractors[extractorId] = id;

    // Find extractor config
    const objWithExt = config.objectives?.find(o => o.extractor_id === extractorId);
    const conWithExt = config.constraints?.find(c => c.extractor_id === extractorId);

    nodes.push({
      id,
      type: 'extractor',
      position: { x: COLS.extractor, y: START_Y + extractorRow * ROW_HEIGHT },
      data: {
        type: 'extractor',
        label: extractorId,
        configured: true,
        extractorId: extractorId,
        extractorName: objWithExt?.name || conWithExt?.name || extractorId,
      },
    });
    extractorRow++;

    // Connect Solver → Extractor
    if (nodeIds.solver) {
      edges.push({
        id: `e_solver_${extractorId}`,
        source: nodeIds.solver,
        target: id,
      });
    }
  });

  // 5. Create Objectives (connected to Extractors)
  config.objectives?.forEach((obj, i) => {
    const id = `obj_${i}_${Date.now()}`;
    nodeIds.objectives.push(id);
    nodes.push({
      id,
      type: 'objective',
      position: { x: COLS.obj, y: START_Y + i * ROW_HEIGHT },
      data: {
        type: 'objective',
        label: obj.name,
        configured: true,
        name: obj.name,
        direction: obj.direction,
        weight: obj.weight,
      },
    });

    // Connect Extractor → Objective
    const extractorNodeId = nodeIds.extractors[obj.extractor_id];
    if (extractorNodeId) {
      edges.push({
        id: `e_ext_obj_${i}`,
        source: extractorNodeId,
        target: id,
      });
    }
  });

  // 6. Create Constraints (connected to Extractors)
  config.constraints?.forEach((con, i) => {
    const id = `con_${i}_${Date.now()}`;
    nodeIds.constraints.push(id);

    const objY = START_Y + (config.objectives?.length || 0) * ROW_HEIGHT;
    nodes.push({
      id,
      type: 'constraint',
      position: { x: COLS.obj, y: objY + i * ROW_HEIGHT },
      data: {
        type: 'constraint',
        label: con.name,
        configured: true,
        name: con.name,
        operator: con.type === 'upper' ? '<=' : con.type === 'lower' ? '>=' : '==',
        value: con.upper_bound ?? con.lower_bound ?? con.target,
      },
    });

    // Connect Extractor → Constraint
    if (con.extractor_id) {
      const extractorNodeId = nodeIds.extractors[con.extractor_id];
      if (extractorNodeId) {
        edges.push({
          id: `e_ext_con_${i}`,
          source: extractorNodeId,
          target: id,
        });
      }
    }
  });

  // 7. Create Algorithm Node
  if (config.optimization) {
    const id = `algo_${Date.now()}`;
    nodeIds.algorithm = id;
    nodes.push({
      id,
      type: 'algorithm',
      position: { x: COLS.algo, y: START_Y },
      data: {
        type: 'algorithm',
        label: 'Algorithm',
        configured: true,
        method: config.optimization.sampler || 'TPE',
        maxTrials: config.optimization.n_trials || 100,
      },
    });

    // Connect Objectives → Algorithm
    nodeIds.objectives.forEach((objId, i) => {
      edges.push({
        id: `e_obj_algo_${i}`,
        source: objId,
        target: id,
      });
    });
  }

  // 8. Create Surrogate Node (if enabled)
  if (config.surrogate?.enabled) {
    const id = `surrogate_${Date.now()}`;
    nodes.push({
      id,
      type: 'surrogate',
      position: { x: COLS.algo, y: START_Y + 150 },
      data: {
        type: 'surrogate',
        label: 'Surrogate',
        configured: true,
        enabled: true,
        modelType: config.surrogate.type || 'MLP',
        minTrials: config.surrogate.min_trials || 20,
      },
    });
  }

  // Apply to store
  set({
    nodes,
    edges,
    selectedNode: null,
    validation: { valid: false, errors: [], warnings: [] },
  });
};

2.2 Parse Full Config Structure

Ensure we handle:

  • nx_model.sim_path / prt_path / fem_path
  • solver.solution_type
  • design_variables[] with all fields
  • objectives[] with extractor_id, name, direction, weight
  • constraints[] with extractor_id, type, upper_bound, lower_bound
  • optimization.sampler, n_trials
  • surrogate.enabled, type, min_trials
  • post_processing[] (for future)

Phase 3: UI/UX Polish

3.1 Responsive Full-Screen Canvas

CanvasView.tsx:

export function CanvasView() {
  return (
    <div className="h-screen w-screen flex flex-col overflow-hidden">
      {/* Minimal header */}
      <header className="flex-shrink-0 h-10 bg-dark-900 border-b border-dark-700 px-4 flex items-center justify-between">
        <span className="text-sm font-medium text-white">Canvas Builder</span>
        <div className="flex gap-2">
          <button>Templates</button>
          <button>Import</button>
          <button>Clear</button>
        </div>
      </header>

      {/* Canvas fills remaining space */}
      <main className="flex-1 min-h-0">
        <AtomizerCanvas />
      </main>
    </div>
  );
}

3.2 Fix Contrast Issues

Problem: White text on light blue is hard to read

Solution: Use darker backgrounds or different text colors

/* Node backgrounds */
.bg-dark-850 /* #0A1525 - dark enough for white text */

/* Avoid light blue backgrounds with white text */
/* If using blue, use dark blue (#1e3a5f) or switch to light text */

/* Specific fixes */
.node-header {
  background: #0F1E32; /* dark-800 */
  color: #FFFFFF;
}

.node-content {
  background: #0A1525; /* dark-850 */
  color: #E2E8F0; /* light gray */
}

/* Badge/pill text */
.badge-primary {
  background: rgba(0, 212, 230, 0.15); /* primary with low opacity */
  color: #00D4E6; /* primary-400 */
  border: 1px solid rgba(0, 212, 230, 0.3);
}

3.3 Increase Font Sizes

Current vs New:

Element Current New
Node label 12px 14px
Node detail 10px 12px
Palette item 12px 14px
Panel headers 14px 16px
Config labels 10px 12px

Phase 4: Claude Integration

4.1 Fix Chat Panel Connection

Error Handling:

function ChatPanel({ onClose }: ChatPanelProps) {
  const { messages, isConnected, isThinking, error } = useCanvasChat();

  if (error) {
    return (
      <div className="flex flex-col items-center justify-center h-full p-4">
        <AlertCircle className="text-red-400 mb-2" size={24} />
        <p className="text-red-400 text-sm text-center">{error}</p>
        <button
          onClick={reconnect}
          className="mt-4 px-3 py-1.5 bg-dark-700 rounded text-sm"
        >
          Retry Connection
        </button>
      </div>
    );
  }

  // ... rest of component
}

4.2 Auto-Complete with Claude

Concept: A button that sends current canvas state to Claude and asks for suggestions.

UI:

  • Button: "Complete with Claude" next to Validate
  • Opens chat panel with Claude's analysis
  • Claude suggests: missing nodes, connections, configuration improvements

Implementation:

// In useCanvasChat.ts
const autoCompleteWithClaude = useCallback(async () => {
  const intent = toIntent();
  const message = `Analyze this Canvas workflow and suggest what's missing or could be improved:

\`\`\`json
${JSON.stringify(intent, null, 2)}
\`\`\`

Please:
1. Identify any missing required components
2. Suggest extractors that should be added based on the objectives
3. Recommend connections that should be made
4. Propose any configuration improvements

Be specific and actionable.`;

  await sendMessage(message);
}, [toIntent, sendMessage]);

4.3 Canvas ↔ Assistant Integration (Future)

Vision: Claude can modify the canvas through conversation.

Commands:

  • "Add a displacement extractor"
  • "Connect the mass extractor to objective 1"
  • "Set the algorithm to CMA-ES with 200 trials"
  • "Load the bracket_v3 study"

Implementation Approach:

  1. Define canvas manipulation actions as Claude tools
  2. Parse Claude responses for action intents
  3. Execute actions via store methods

MCP Tools (New):

  • canvas_add_node - Add a node of specified type
  • canvas_remove_node - Remove a node by ID
  • canvas_connect - Connect two nodes
  • canvas_disconnect - Remove a connection
  • canvas_configure - Update node configuration
  • canvas_load_study - Load a study into canvas

Phase 5: Testing & Validation

Test Cases

Test Steps Expected
Load study with connections Import → Select study → Load All nodes + edges appear
Delete connection Click edge → Press Delete Edge removed
Drag & drop position Drag node to specific spot Node appears at drop location
Chat panel opens Click chat icon Panel opens without error
Full screen canvas Open /canvas Canvas fills window
Contrast readable View all nodes All text legible

Build Verification

cd atomizer-dashboard/frontend
npm run build
# Must pass without errors

Files to Modify

File Changes
useCanvasStore.ts Enhanced loadFromConfig, deleteEdge
AtomizerCanvas.tsx Edge deletion, drag/drop fix, responsive
CanvasView.tsx Full-screen layout
ChatPanel.tsx Error handling, reconnect
useCanvasChat.ts Auto-complete function, error state
BaseNode.tsx Font sizes, contrast
NodePalette.tsx Font sizes
NodeConfigPanel.tsx Font sizes, contrast

Summary

Total Effort: ~11 hours across 5 phases

Priority Order:

  1. Fix Atomizer Assistant (blocking)
  2. Fix connection deletion (blocking editing)
  3. Fix data loading (core functionality)
  4. UI/UX polish (user experience)
  5. Claude integration (enhancement)

Success Criteria:

  • All bugs from user report fixed
  • Loading a study shows ALL elements with connections
  • Canvas is responsive and readable
  • Chat panel works without errors
  • Build passes without errors

Plan created for Ralph Loop autonomous execution.