feat(canvas): Studio Enhancement Phase 3 & 4 - undo/redo and Monaco editor

Phase 3 - Undo/Redo System:
- Create generic useUndoRedo hook with configurable options
- Add localStorage persistence for per-study history (max 30 steps)
- Create useSpecUndoRedo hook integrating with useSpecStore
- Add useUndoRedoKeyboard hook for Ctrl+Z/Ctrl+Y shortcuts
- Add undo/redo buttons to canvas header with tooltips
- Debounced history recording (1s delay after changes)

Phase 4 - Monaco Code Editor:
- Create CodeEditorPanel component with Monaco editor
- Add Python syntax highlighting and auto-completion
- Include pyNastran/OP2 specific completions
- Add Claude AI code generation integration (placeholder)
- Include code validation/run functionality
- Show output variables preview section
- Add copy-to-clipboard and generation prompt UI

Dependencies:
- Add @monaco-editor/react package

Technical:
- All TypeScript checks pass
- All 15 unit tests pass
- Production build successful
This commit is contained in:
2026-01-20 11:58:21 -05:00
parent c4a3cff91a
commit ffd41e3a60
6 changed files with 2926 additions and 62 deletions

View File

@@ -1,6 +1,6 @@
import { useState, useEffect, useCallback } from 'react';
import { useNavigate, useParams, useSearchParams } from 'react-router-dom';
import { ClipboardList, Download, Trash2, Layers, Home, ChevronRight, Save, RefreshCw, Zap, MessageSquare, X, Folder, SlidersHorizontal } from 'lucide-react';
import { ClipboardList, Download, Trash2, Layers, Home, ChevronRight, Save, RefreshCw, Zap, MessageSquare, X, Folder, SlidersHorizontal, Undo2, Redo2 } from 'lucide-react';
import { AtomizerCanvas } from '../components/canvas/AtomizerCanvas';
import { SpecRenderer } from '../components/canvas/SpecRenderer';
import { NodePalette } from '../components/canvas/palette/NodePalette';
@@ -12,6 +12,7 @@ import { NodeConfigPanelV2 } from '../components/canvas/panels/NodeConfigPanelV2
import { ChatPanel } from '../components/canvas/panels/ChatPanel';
import { useCanvasStore } from '../hooks/useCanvasStore';
import { useSpecStore, useSpec, useSpecLoading, useSpecIsDirty, useSelectedNodeId } from '../hooks/useSpecStore';
import { useSpecUndoRedo, useUndoRedoKeyboard } from '../hooks/useSpecUndoRedo';
import { useStudy } from '../context/StudyContext';
import { useChat } from '../hooks/useChat';
import { CanvasTemplate } from '../lib/canvas/templates';
@@ -54,6 +55,13 @@ export function CanvasView() {
const { setSelectedStudy, studies } = useStudy();
const { clearSpec, setSpecFromWebSocket } = useSpecStore();
// Undo/Redo for spec mode
const undoRedo = useSpecUndoRedo();
const { undo, redo, canUndo, canRedo, historyLength } = undoRedo;
// Enable keyboard shortcuts for undo/redo (Ctrl+Z, Ctrl+Y)
useUndoRedoKeyboard(undoRedo);
// Active study ID comes ONLY from URL - don't auto-load from context
// This ensures /canvas shows empty canvas, /canvas/{id} shows the study
const activeStudyId = urlStudyId;
@@ -318,6 +326,39 @@ export function CanvasView() {
</button>
)}
{/* Undo/Redo Buttons (spec mode only) */}
{useSpecMode && activeStudyId && (
<>
<div className="w-px h-6 bg-dark-600" />
<div className="flex items-center gap-1">
<button
onClick={undo}
disabled={!canUndo}
className={`p-1.5 rounded-lg transition-colors ${
canUndo
? 'text-dark-200 hover:bg-dark-700 hover:text-white'
: 'text-dark-600 cursor-not-allowed'
}`}
title={`Undo (Ctrl+Z)${historyLength > 0 ? ` - ${historyLength} steps` : ''}`}
>
<Undo2 size={16} />
</button>
<button
onClick={redo}
disabled={!canRedo}
className={`p-1.5 rounded-lg transition-colors ${
canRedo
? 'text-dark-200 hover:bg-dark-700 hover:text-white'
: 'text-dark-600 cursor-not-allowed'
}`}
title="Redo (Ctrl+Y)"
>
<Redo2 size={16} />
</button>
</div>
</>
)}
<button
onClick={() => setShowTemplates(true)}
className="px-3 py-1.5 bg-primary-600 hover:bg-primary-500 text-white text-sm rounded-lg transition-colors flex items-center gap-1.5"