/** * CodeEditorPanel - Monaco editor for custom extractor Python code * * Features: * - Python syntax highlighting * - Auto-completion for common patterns * - Error display * - Claude AI code generation with streaming support * - Preview of extracted outputs * - Code snippets library */ import { useState, useCallback, useRef, useEffect } from 'react'; import Editor, { OnMount, OnChange } from '@monaco-editor/react'; import { Play, Wand2, Copy, Check, AlertCircle, RefreshCw, X, ChevronDown, ChevronRight, FileCode, Sparkles, Square, BookOpen, FlaskConical, } from 'lucide-react'; // Monaco editor types type Monaco = Parameters[1]; type EditorInstance = Parameters[0]; /** Streaming generation callbacks */ export interface StreamingCallbacks { onToken: (token: string) => void; onComplete: (code: string, outputs: string[]) => void; onError: (error: string) => void; } /** Request format for streaming generation */ export interface StreamingGenerationRequest { prompt: string; study_id?: string; existing_code?: string; output_names?: string[]; } interface CodeEditorPanelProps { /** Initial code content */ initialCode?: string; /** Callback when code changes */ onChange?: (code: string) => void; /** Callback when user requests Claude generation (non-streaming) */ onRequestGeneration?: (prompt: string) => Promise; /** Callback for streaming generation (preferred over onRequestGeneration) */ onRequestStreamingGeneration?: ( request: StreamingGenerationRequest, callbacks: StreamingCallbacks ) => AbortController; /** Whether the panel is read-only */ readOnly?: boolean; /** Extractor name for context */ extractorName?: string; /** Output variable names */ outputs?: string[]; /** Optional height (default: 300px) */ height?: number | string; /** Show/hide header */ showHeader?: boolean; /** Callback when running code (validation) */ onRun?: (code: string) => Promise<{ success: boolean; error?: string; outputs?: Record }>; /** Callback for live testing against OP2 file */ onTest?: (code: string) => Promise<{ success: boolean; error?: string; outputs?: Record; execution_time_ms?: number }>; /** Close button callback */ onClose?: () => void; /** Study ID for context in generation */ studyId?: string; } // Default Python template for custom extractors const DEFAULT_EXTRACTOR_TEMPLATE = `""" Custom Extractor Function This function is called after FEA simulation completes. It receives the results and should return extracted values. Available inputs: - op2_path: Path to the .op2 results file - fem_path: Path to the .fem file - params: Dict of current design variable values - subcase_id: Current subcase being analyzed (optional) Return a dict with your extracted values. """ from pyNastran.op2.op2 import OP2 import numpy as np def extract(op2_path: str, fem_path: str, params: dict, subcase_id: int = 1) -> dict: """ Extract physics from FEA results. Args: op2_path: Path to OP2 results file fem_path: Path to FEM file params: Current design variable values subcase_id: Subcase ID to analyze Returns: Dict with extracted values, e.g. {'max_stress': 150.5, 'mass': 2.3} """ # Load OP2 results op2 = OP2() op2.read_op2(op2_path) # Example: Extract max displacement if subcase_id in op2.displacements: disp = op2.displacements[subcase_id] # Get magnitude of displacement vectors magnitudes = np.sqrt(np.sum(disp.data[0, :, 1:4]**2, axis=1)) max_disp = float(np.max(magnitudes)) else: max_disp = 0.0 return { 'max_displacement': max_disp, # Add more outputs as needed } `; // Code snippets library interface CodeSnippet { id: string; name: string; category: string; description: string; code: string; } const CODE_SNIPPETS: CodeSnippet[] = [ { id: 'displacement', name: 'Max Displacement', category: 'Displacement', description: 'Extract maximum displacement magnitude from results', code: `"""Extract maximum displacement magnitude""" from pyNastran.op2.op2 import OP2 import numpy as np def extract(op2_path: str, fem_path: str, params: dict, subcase_id: int = 1) -> dict: op2 = OP2() op2.read_op2(op2_path) if subcase_id in op2.displacements: disp = op2.displacements[subcase_id] # Displacement data: [time, node, component] where component 1-3 are x,y,z magnitudes = np.sqrt(np.sum(disp.data[0, :, 1:4]**2, axis=1)) max_disp = float(np.max(magnitudes)) else: max_disp = 0.0 return {'max_displacement': max_disp} `, }, { id: 'stress_vonmises', name: 'Von Mises Stress', category: 'Stress', description: 'Extract maximum von Mises stress from shell elements', code: `"""Extract maximum von Mises stress from shell elements""" from pyNastran.op2.op2 import OP2 import numpy as np def extract(op2_path: str, fem_path: str, params: dict, subcase_id: int = 1) -> dict: op2 = OP2() op2.read_op2(op2_path) max_stress = 0.0 # Check CQUAD4 elements if subcase_id in op2.cquad4_stress: stress = op2.cquad4_stress[subcase_id] # Von Mises is typically in the last column vm_stress = stress.data[0, :, -1] # [time, element, component] max_stress = max(max_stress, float(np.max(np.abs(vm_stress)))) # Check CTRIA3 elements if subcase_id in op2.ctria3_stress: stress = op2.ctria3_stress[subcase_id] vm_stress = stress.data[0, :, -1] max_stress = max(max_stress, float(np.max(np.abs(vm_stress)))) return {'max_vonmises': max_stress} `, }, { id: 'frequency', name: 'Natural Frequency', category: 'Modal', description: 'Extract first natural frequency from modal analysis', code: `"""Extract natural frequencies from modal analysis""" from pyNastran.op2.op2 import OP2 def extract(op2_path: str, fem_path: str, params: dict, subcase_id: int = 1) -> dict: op2 = OP2() op2.read_op2(op2_path) freq_1 = 0.0 freq_2 = 0.0 freq_3 = 0.0 if subcase_id in op2.eigenvalues: eig = op2.eigenvalues[subcase_id] freqs = eig.radians / (2 * 3.14159) # Convert to Hz if len(freqs) >= 1: freq_1 = float(freqs[0]) if len(freqs) >= 2: freq_2 = float(freqs[1]) if len(freqs) >= 3: freq_3 = float(freqs[2]) return { 'freq_1': freq_1, 'freq_2': freq_2, 'freq_3': freq_3, } `, }, { id: 'mass_grid', name: 'Grid Point Mass', category: 'Mass', description: 'Extract total mass from grid point weight generator', code: `"""Extract mass from grid point weight generator""" from pyNastran.op2.op2 import OP2 def extract(op2_path: str, fem_path: str, params: dict, subcase_id: int = 1) -> dict: op2 = OP2() op2.read_op2(op2_path) total_mass = 0.0 if hasattr(op2, 'grid_point_weight') and op2.grid_point_weight: gpw = op2.grid_point_weight # Mass is typically M[0,0] in the mass matrix if hasattr(gpw, 'mass') and len(gpw.mass) > 0: total_mass = float(gpw.mass[0]) return {'total_mass': total_mass} `, }, { id: 'strain_energy', name: 'Strain Energy', category: 'Energy', description: 'Extract total strain energy from elements', code: `"""Extract strain energy from elements""" from pyNastran.op2.op2 import OP2 import numpy as np def extract(op2_path: str, fem_path: str, params: dict, subcase_id: int = 1) -> dict: op2 = OP2() op2.read_op2(op2_path) total_energy = 0.0 # Sum strain energy from all element types for key in dir(op2): if 'strain_energy' in key.lower(): result = getattr(op2, key) if isinstance(result, dict) and subcase_id in result: se = result[subcase_id] if hasattr(se, 'data'): total_energy += float(np.sum(se.data)) return {'strain_energy': total_energy} `, }, { id: 'reaction_force', name: 'Reaction Forces', category: 'Force', description: 'Extract reaction forces at constrained nodes', code: `"""Extract reaction forces at single point constraints""" from pyNastran.op2.op2 import OP2 import numpy as np def extract(op2_path: str, fem_path: str, params: dict, subcase_id: int = 1) -> dict: op2 = OP2() op2.read_op2(op2_path) max_reaction = 0.0 total_reaction_z = 0.0 if subcase_id in op2.spc_forces: spc = op2.spc_forces[subcase_id] # SPC forces: [time, node, component] where 1-3 are Fx,Fy,Fz forces = spc.data[0, :, 1:4] # Get Fx, Fy, Fz magnitudes = np.sqrt(np.sum(forces**2, axis=1)) max_reaction = float(np.max(magnitudes)) total_reaction_z = float(np.sum(forces[:, 2])) # Sum of Fz return { 'max_reaction': max_reaction, 'total_reaction_z': total_reaction_z, } `, }, ]; export function CodeEditorPanel({ initialCode = DEFAULT_EXTRACTOR_TEMPLATE, onChange, onRequestGeneration, onRequestStreamingGeneration, readOnly = false, extractorName = 'custom_extractor', outputs = [], height = 400, showHeader = true, onRun, onTest, onClose, studyId, }: CodeEditorPanelProps) { const [code, setCode] = useState(initialCode); const [streamingCode, setStreamingCode] = useState(''); // Partial code during streaming const [isGenerating, setIsGenerating] = useState(false); const [isRunning, setIsRunning] = useState(false); const [isTesting, setIsTesting] = useState(false); const [error, setError] = useState(null); const [runResult, setRunResult] = useState<{ success: boolean; outputs?: Record } | null>(null); const [testResult, setTestResult] = useState<{ success: boolean; outputs?: Record; execution_time_ms?: number } | null>(null); const [copied, setCopied] = useState(false); const [showPromptInput, setShowPromptInput] = useState(false); const [generationPrompt, setGenerationPrompt] = useState(''); const [showOutputs, setShowOutputs] = useState(true); const [showSnippets, setShowSnippets] = useState(false); const editorRef = useRef(null); const monacoRef = useRef(null); const abortControllerRef = useRef(null); // Cleanup abort controller on unmount useEffect(() => { return () => { abortControllerRef.current?.abort(); }; }, []); // Handle editor mount const handleEditorMount: OnMount = (editor, monaco) => { editorRef.current = editor; monacoRef.current = monaco; // Configure Python language monaco.languages.registerCompletionItemProvider('python', { provideCompletionItems: (model: Parameters[0], position: { lineNumber: number; column: number }) => { const word = model.getWordUntilPosition(position); const range = { startLineNumber: position.lineNumber, endLineNumber: position.lineNumber, startColumn: word.startColumn, endColumn: word.endColumn, }; const suggestions = [ { label: 'op2.read_op2', kind: monaco.languages.CompletionItemKind.Method, insertText: 'op2.read_op2(op2_path)', documentation: 'Read OP2 results file', range, }, { label: 'op2.displacements', kind: monaco.languages.CompletionItemKind.Property, insertText: 'op2.displacements[subcase_id]', documentation: 'Access displacement results for a subcase', range, }, { label: 'op2.eigenvectors', kind: monaco.languages.CompletionItemKind.Property, insertText: 'op2.eigenvectors[subcase_id]', documentation: 'Access eigenvector results for modal analysis', range, }, { label: 'np.max', kind: monaco.languages.CompletionItemKind.Function, insertText: 'np.max(${1:array})', insertTextRules: monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet, documentation: 'Get maximum value from array', range, }, { label: 'np.sqrt', kind: monaco.languages.CompletionItemKind.Function, insertText: 'np.sqrt(${1:array})', insertTextRules: monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet, documentation: 'Square root of array elements', range, }, { label: 'extract_function', kind: monaco.languages.CompletionItemKind.Snippet, insertText: `def extract(op2_path: str, fem_path: str, params: dict, subcase_id: int = 1) -> dict: """Extract physics from FEA results.""" op2 = OP2() op2.read_op2(op2_path) # Your extraction logic here return { '\${1:output_name}': \${2:value}, }`, insertTextRules: monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet, documentation: 'Insert a complete extract function template', range, }, ]; return { suggestions }; }, }); }; // Handle code change const handleCodeChange: OnChange = (value) => { const newCode = value || ''; setCode(newCode); setError(null); setRunResult(null); onChange?.(newCode); }; // Copy code to clipboard const handleCopy = useCallback(() => { navigator.clipboard.writeText(code); setCopied(true); setTimeout(() => setCopied(false), 2000); }, [code]); // Request Claude generation (with streaming support) const handleGenerate = useCallback(async () => { if ((!onRequestGeneration && !onRequestStreamingGeneration) || !generationPrompt.trim()) return; setIsGenerating(true); setError(null); setStreamingCode(''); // Prefer streaming if available if (onRequestStreamingGeneration) { abortControllerRef.current = onRequestStreamingGeneration( { prompt: generationPrompt, study_id: studyId, existing_code: code !== DEFAULT_EXTRACTOR_TEMPLATE ? code : undefined, output_names: outputs, }, { onToken: (token) => { setStreamingCode(prev => prev + token); }, onComplete: (generatedCode, _outputs) => { setCode(generatedCode); setStreamingCode(''); onChange?.(generatedCode); setShowPromptInput(false); setGenerationPrompt(''); setIsGenerating(false); }, onError: (errorMsg) => { setError(errorMsg); setStreamingCode(''); setIsGenerating(false); }, } ); } else if (onRequestGeneration) { // Fallback to non-streaming try { const generatedCode = await onRequestGeneration(generationPrompt); setCode(generatedCode); onChange?.(generatedCode); setShowPromptInput(false); setGenerationPrompt(''); } catch (err) { setError(err instanceof Error ? err.message : 'Generation failed'); } finally { setIsGenerating(false); } } }, [onRequestGeneration, onRequestStreamingGeneration, generationPrompt, onChange, code, outputs, studyId]); // Cancel ongoing generation const handleCancelGeneration = useCallback(() => { abortControllerRef.current?.abort(); abortControllerRef.current = null; setIsGenerating(false); setStreamingCode(''); }, []); // Run/validate code const handleRun = useCallback(async () => { if (!onRun) return; setIsRunning(true); setError(null); setRunResult(null); try { const result = await onRun(code); setRunResult(result); if (!result.success && result.error) { setError(result.error); } } catch (err) { setError(err instanceof Error ? err.message : 'Validation failed'); } finally { setIsRunning(false); } }, [code, onRun]); // Test code against real OP2 file const handleTest = useCallback(async () => { if (!onTest) return; setIsTesting(true); setError(null); setTestResult(null); try { const result = await onTest(code); setTestResult(result); if (!result.success && result.error) { setError(result.error); } } catch (err) { setError(err instanceof Error ? err.message : 'Test failed'); } finally { setIsTesting(false); } }, [code, onTest]); return (
{/* Header */} {showHeader && (
{extractorName} .py
{/* Snippets Button */} {/* Claude Generate Button */} {(onRequestGeneration || onRequestStreamingGeneration) && ( )} {/* Copy Button */} {/* Run Button */} {onRun && ( )} {/* Test Button - Live Preview */} {onTest && ( )} {/* Close Button */} {onClose && ( )}
)} {/* Claude Prompt Input */} {showPromptInput && (
Generate with Claude