diff --git a/atomizer-dashboard/frontend/src/components/canvas/ConnectionStatusIndicator.tsx b/atomizer-dashboard/frontend/src/components/canvas/ConnectionStatusIndicator.tsx new file mode 100644 index 00000000..0d64b376 --- /dev/null +++ b/atomizer-dashboard/frontend/src/components/canvas/ConnectionStatusIndicator.tsx @@ -0,0 +1,49 @@ +/** + * ConnectionStatusIndicator - Visual indicator for WebSocket connection status. + */ + +import { ConnectionStatus } from '../../hooks/useSpecWebSocket'; + +interface ConnectionStatusIndicatorProps { + status: ConnectionStatus; + className?: string; +} + +/** + * Visual indicator for WebSocket connection status. + * Can be used in the canvas UI to show sync state. + */ +export function ConnectionStatusIndicator({ + status, + className = '', +}: ConnectionStatusIndicatorProps) { + const statusConfig = { + disconnected: { + color: 'bg-gray-500', + label: 'Disconnected', + }, + connecting: { + color: 'bg-yellow-500 animate-pulse', + label: 'Connecting...', + }, + connected: { + color: 'bg-green-500', + label: 'Connected', + }, + reconnecting: { + color: 'bg-yellow-500 animate-pulse', + label: 'Reconnecting...', + }, + }; + + const config = statusConfig[status]; + + return ( +
+
+ {config.label} +
+ ); +} + +export default ConnectionStatusIndicator; diff --git a/atomizer-dashboard/frontend/src/components/canvas/nodes/CustomExtractorNode.tsx b/atomizer-dashboard/frontend/src/components/canvas/nodes/CustomExtractorNode.tsx new file mode 100644 index 00000000..f21e794c --- /dev/null +++ b/atomizer-dashboard/frontend/src/components/canvas/nodes/CustomExtractorNode.tsx @@ -0,0 +1,58 @@ +/** + * CustomExtractorNode - Canvas node for custom Python extractors + * + * Displays custom extractors defined with inline Python code. + * Visually distinct from builtin extractors with a code icon. + * + * P3.11: Custom extractor UI component + */ + +import { memo } from 'react'; +import { NodeProps } from 'reactflow'; +import { Code2 } from 'lucide-react'; +import { BaseNode } from './BaseNode'; + +export interface CustomExtractorNodeData { + type: 'customExtractor'; + label: string; + configured: boolean; + extractorId?: string; + extractorName?: string; + functionName?: string; + functionSource?: string; + outputs?: Array<{ name: string; units?: string }>; + dependencies?: string[]; +} + +function CustomExtractorNodeComponent(props: NodeProps) { + const { data } = props; + + // Show validation status + const hasCode = !!data.functionSource?.trim(); + const hasOutputs = (data.outputs?.length ?? 0) > 0; + const isConfigured = hasCode && hasOutputs; + + return ( + } + iconColor={isConfigured ? 'text-violet-400' : 'text-dark-500'} + > +
+ + {data.extractorName || data.functionName || 'Custom Extractor'} + + {!isConfigured && ( + Needs configuration + )} + {isConfigured && data.outputs && ( + + {data.outputs.length} output{data.outputs.length !== 1 ? 's' : ''} + + )} +
+
+ ); +} + +export const CustomExtractorNode = memo(CustomExtractorNodeComponent); diff --git a/atomizer-dashboard/frontend/src/components/canvas/panels/CustomExtractorPanel.tsx b/atomizer-dashboard/frontend/src/components/canvas/panels/CustomExtractorPanel.tsx new file mode 100644 index 00000000..fe8318a3 --- /dev/null +++ b/atomizer-dashboard/frontend/src/components/canvas/panels/CustomExtractorPanel.tsx @@ -0,0 +1,360 @@ +/** + * CustomExtractorPanel - Panel for editing custom Python extractors + * + * Provides a code editor for writing custom extraction functions, + * output definitions, and validation. + * + * P3.12: Custom extractor UI component + */ + +import { useState, useCallback } from 'react'; +import { X, Play, AlertCircle, CheckCircle, Plus, Trash2, HelpCircle } from 'lucide-react'; + +interface CustomExtractorOutput { + name: string; + units?: string; + description?: string; +} + +interface CustomExtractorPanelProps { + isOpen: boolean; + onClose: () => void; + initialName?: string; + initialFunctionName?: string; + initialSource?: string; + initialOutputs?: CustomExtractorOutput[]; + initialDependencies?: string[]; + onSave: (data: { + name: string; + functionName: string; + source: string; + outputs: CustomExtractorOutput[]; + dependencies: string[]; + }) => void; +} + +// Common styling classes +const inputClass = + 'w-full px-3 py-2 bg-dark-800 border border-dark-600 text-white placeholder-dark-400 rounded-lg focus:border-primary-500 focus:outline-none transition-colors'; +const labelClass = 'block text-sm font-medium text-dark-300 mb-1'; + +// Default extractor template +const DEFAULT_SOURCE = `def extract(op2_path, bdf_path=None, params=None, working_dir=None): + """ + Custom extractor function. + + Args: + op2_path: Path to the OP2 results file + bdf_path: Optional path to the BDF model file + params: Dictionary of current design parameters + working_dir: Path to the current trial directory + + Returns: + Dictionary of output_name -> value + OR a single float value + OR a list/tuple of values (mapped to outputs in order) + """ + import numpy as np + from pyNastran.op2.op2 import OP2 + + # Load OP2 results + op2 = OP2(op2_path, debug=False) + + # Example: compute custom metric + # ... your extraction logic here ... + + result = 0.0 + + return {"custom_output": result} +`; + +export function CustomExtractorPanel({ + isOpen, + onClose, + initialName = '', + initialFunctionName = 'extract', + initialSource = DEFAULT_SOURCE, + initialOutputs = [{ name: 'custom_output', units: '' }], + initialDependencies = [], + onSave, +}: CustomExtractorPanelProps) { + const [name, setName] = useState(initialName); + const [functionName, setFunctionName] = useState(initialFunctionName); + const [source, setSource] = useState(initialSource); + const [outputs, setOutputs] = useState(initialOutputs); + const [dependencies] = useState(initialDependencies); + const [validation, setValidation] = useState<{ + valid: boolean; + errors: string[]; + } | null>(null); + const [isValidating, setIsValidating] = useState(false); + const [showHelp, setShowHelp] = useState(false); + + // Add a new output + const addOutput = useCallback(() => { + setOutputs((prev) => [...prev, { name: '', units: '' }]); + }, []); + + // Remove an output + const removeOutput = useCallback((index: number) => { + setOutputs((prev) => prev.filter((_, i) => i !== index)); + }, []); + + // Update an output + const updateOutput = useCallback( + (index: number, field: keyof CustomExtractorOutput, value: string) => { + setOutputs((prev) => + prev.map((out, i) => (i === index ? { ...out, [field]: value } : out)) + ); + }, + [] + ); + + // Validate the code + const validateCode = useCallback(async () => { + setIsValidating(true); + setValidation(null); + + try { + const response = await fetch('/api/spec/validate-extractor', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + function_name: functionName, + source: source, + }), + }); + + const result = await response.json(); + setValidation({ + valid: result.valid, + errors: result.errors || [], + }); + } catch (error) { + setValidation({ + valid: false, + errors: ['Failed to validate: ' + (error instanceof Error ? error.message : 'Unknown error')], + }); + } finally { + setIsValidating(false); + } + }, [functionName, source]); + + // Handle save + const handleSave = useCallback(() => { + // Filter out empty outputs + const validOutputs = outputs.filter((o) => o.name.trim()); + + if (!name.trim()) { + setValidation({ valid: false, errors: ['Name is required'] }); + return; + } + + if (validOutputs.length === 0) { + setValidation({ valid: false, errors: ['At least one output is required'] }); + return; + } + + onSave({ + name: name.trim(), + functionName: functionName.trim() || 'extract', + source, + outputs: validOutputs, + dependencies: dependencies.filter((d) => d.trim()), + }); + onClose(); + }, [name, functionName, source, outputs, dependencies, onSave, onClose]); + + if (!isOpen) return null; + + return ( +
+
+ {/* Header */} +
+

Custom Extractor

+
+ + +
+
+ + {/* Content */} +
+ {/* Help Section */} + {showHelp && ( +
+

How Custom Extractors Work

+
    +
  • • Your function receives the path to OP2 results and optional BDF/params
  • +
  • • Use pyNastran, numpy, scipy for data extraction and analysis
  • +
  • • Return a dictionary mapping output names to numeric values
  • +
  • • Outputs can be used as objectives or constraints in optimization
  • +
  • • Code runs in a sandboxed environment (no file I/O beyond OP2/BDF)
  • +
+
+ )} + +
+ {/* Left Column - Basic Info & Outputs */} +
+ {/* Name */} +
+ + setName(e.target.value)} + placeholder="My Custom Extractor" + className={inputClass} + /> +
+ + {/* Function Name */} +
+ + setFunctionName(e.target.value)} + placeholder="extract" + className={`${inputClass} font-mono`} + /> +

+ Name of the Python function in your code +

+
+ + {/* Outputs */} +
+ +
+ {outputs.map((output, index) => ( +
+ updateOutput(index, 'name', e.target.value)} + placeholder="output_name" + className={`${inputClass} font-mono flex-1`} + /> + updateOutput(index, 'units', e.target.value)} + placeholder="units" + className={`${inputClass} w-24`} + /> + +
+ ))} + +
+
+ + {/* Validation Status */} + {validation && ( +
+
+ {validation.valid ? ( + + ) : ( + + )} + + {validation.valid ? 'Code is valid' : 'Validation failed'} + +
+ {validation.errors.length > 0 && ( +
    + {validation.errors.map((err, i) => ( +
  • • {err}
  • + ))} +
+ )} +
+ )} +
+ + {/* Right Column - Code Editor */} +
+
+ + +
+