2026-01-20 11:53:26 -05:00
|
|
|
/**
|
|
|
|
|
* NodeConfigPanelV2 - Configuration panel for AtomizerSpec v2.0 nodes
|
|
|
|
|
*
|
|
|
|
|
* This component uses useSpecStore instead of the legacy useCanvasStore.
|
|
|
|
|
* It renders type-specific configuration forms based on the selected node.
|
2026-01-20 13:08:12 -05:00
|
|
|
*
|
|
|
|
|
* For custom extractors, integrates CodeEditorPanel with Claude AI generation.
|
2026-01-20 11:53:26 -05:00
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
import { useState, useMemo, useCallback } from 'react';
|
2026-01-20 13:08:12 -05:00
|
|
|
import { Microscope, Trash2, X, AlertCircle, Code, FileCode } from 'lucide-react';
|
|
|
|
|
import { CodeEditorPanel } from './CodeEditorPanel';
|
|
|
|
|
import { generateExtractorCode, validateExtractorCode, streamExtractorCode, checkCodeDependencies, testExtractorCode } from '../../../lib/api/claude';
|
2026-01-20 11:53:26 -05:00
|
|
|
import {
|
|
|
|
|
useSpecStore,
|
|
|
|
|
useSpec,
|
|
|
|
|
useSelectedNodeId,
|
|
|
|
|
useSelectedNode,
|
|
|
|
|
} from '../../../hooks/useSpecStore';
|
feat: Add panel management, validation, and error handling to canvas
Phase 1 - Panel Management System:
- Create usePanelStore.ts for centralized panel state management
- Add PanelContainer.tsx for draggable floating panels
- Create FloatingIntrospectionPanel.tsx (persistent, doesn't disappear on node click)
- Create ResultsPanel.tsx for trial result details
- Refactor NodeConfigPanelV2 to use panel store for introspection
- Integrate PanelContainer into CanvasView
Phase 2 - Pre-run Validation:
- Create specValidator.ts with comprehensive validation rules
- Add ValidationPanel (enhanced version with error navigation)
- Add Validate button to SpecRenderer with status indicator
- Block run if validation fails
- Check for: design vars, objectives, extractors, bounds, connections
Phase 3 - Error Handling & Recovery:
- Create ErrorPanel.tsx for displaying optimization errors
- Add error classification (nx_crash, solver_fail, extractor_error, etc.)
- Add recovery suggestions based on error type
- Update status endpoint to return error info
- Add _get_study_error_info helper to check error_status.json and DB
- Integrate error detection into status polling
Documentation:
- Add CANVAS_ROBUSTNESS_PLAN.md with full implementation plan
2026-01-21 21:35:31 -05:00
|
|
|
import { usePanelStore } from '../../../hooks/usePanelStore';
|
2026-01-20 11:53:26 -05:00
|
|
|
import { FileBrowser } from './FileBrowser';
|
|
|
|
|
import {
|
|
|
|
|
DesignVariable,
|
|
|
|
|
Extractor,
|
|
|
|
|
Objective,
|
|
|
|
|
Constraint,
|
|
|
|
|
} from '../../../types/atomizer-spec';
|
|
|
|
|
|
|
|
|
|
// Common input class for dark theme
|
|
|
|
|
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 selectClass = "w-full px-3 py-2 bg-dark-800 border border-dark-600 text-white rounded-lg focus:border-primary-500 focus:outline-none transition-colors";
|
|
|
|
|
const labelClass = "block text-sm font-medium text-dark-300 mb-1";
|
|
|
|
|
|
|
|
|
|
interface NodeConfigPanelV2Props {
|
|
|
|
|
/** Called when panel should close */
|
|
|
|
|
onClose?: () => void;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function NodeConfigPanelV2({ onClose }: NodeConfigPanelV2Props) {
|
|
|
|
|
const spec = useSpec();
|
|
|
|
|
const selectedNodeId = useSelectedNodeId();
|
|
|
|
|
const selectedNode = useSelectedNode();
|
|
|
|
|
const { updateNode, removeNode, clearSelection } = useSpecStore();
|
|
|
|
|
|
|
|
|
|
const [showFileBrowser, setShowFileBrowser] = useState(false);
|
|
|
|
|
const [showIntrospection, setShowIntrospection] = useState(false);
|
|
|
|
|
const [isUpdating, setIsUpdating] = useState(false);
|
|
|
|
|
const [error, setError] = useState<string | null>(null);
|
|
|
|
|
|
|
|
|
|
// Determine node type from ID prefix or from the node itself
|
|
|
|
|
const nodeType = useMemo(() => {
|
|
|
|
|
if (!selectedNodeId) return null;
|
|
|
|
|
|
|
|
|
|
// Synthetic nodes have fixed IDs
|
|
|
|
|
if (selectedNodeId === 'model') return 'model';
|
|
|
|
|
if (selectedNodeId === 'solver') return 'solver';
|
|
|
|
|
if (selectedNodeId === 'algorithm') return 'algorithm';
|
|
|
|
|
if (selectedNodeId === 'surrogate') return 'surrogate';
|
|
|
|
|
|
|
|
|
|
// Real nodes have prefixed IDs
|
|
|
|
|
const prefix = selectedNodeId.split('_')[0];
|
|
|
|
|
switch (prefix) {
|
|
|
|
|
case 'dv': return 'designVar';
|
|
|
|
|
case 'ext': return 'extractor';
|
|
|
|
|
case 'obj': return 'objective';
|
|
|
|
|
case 'con': return 'constraint';
|
|
|
|
|
default: return null;
|
|
|
|
|
}
|
|
|
|
|
}, [selectedNodeId]);
|
|
|
|
|
|
|
|
|
|
// Get label for display
|
|
|
|
|
const nodeLabel = useMemo(() => {
|
|
|
|
|
if (!selectedNodeId || !spec) return 'Node';
|
|
|
|
|
|
|
|
|
|
switch (nodeType) {
|
|
|
|
|
case 'model': return spec.meta.study_name || 'Model';
|
|
|
|
|
case 'solver': return spec.model.sim?.solution_type || 'Solver';
|
|
|
|
|
case 'algorithm': return spec.optimization.algorithm?.type || 'Algorithm';
|
|
|
|
|
case 'surrogate': return 'Neural Surrogate';
|
|
|
|
|
default:
|
|
|
|
|
if (selectedNode) {
|
|
|
|
|
return (selectedNode as any).name || selectedNodeId;
|
|
|
|
|
}
|
|
|
|
|
return selectedNodeId;
|
|
|
|
|
}
|
|
|
|
|
}, [selectedNodeId, selectedNode, nodeType, spec]);
|
|
|
|
|
|
|
|
|
|
// Handle field changes
|
|
|
|
|
const handleChange = useCallback(async (field: string, value: unknown) => {
|
|
|
|
|
if (!selectedNodeId || !selectedNode) return;
|
|
|
|
|
|
|
|
|
|
setIsUpdating(true);
|
|
|
|
|
setError(null);
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
await updateNode(selectedNodeId, { [field]: value });
|
|
|
|
|
} catch (err) {
|
|
|
|
|
console.error('Failed to update node:', err);
|
|
|
|
|
setError(err instanceof Error ? err.message : 'Update failed');
|
|
|
|
|
} finally {
|
|
|
|
|
setIsUpdating(false);
|
|
|
|
|
}
|
|
|
|
|
}, [selectedNodeId, selectedNode, updateNode]);
|
|
|
|
|
|
|
|
|
|
// Handle delete
|
|
|
|
|
const handleDelete = useCallback(async () => {
|
|
|
|
|
if (!selectedNodeId) return;
|
|
|
|
|
|
|
|
|
|
// Synthetic nodes can't be deleted
|
|
|
|
|
if (['model', 'solver', 'algorithm', 'surrogate'].includes(selectedNodeId)) {
|
|
|
|
|
setError('This node cannot be deleted');
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
setIsUpdating(true);
|
|
|
|
|
setError(null);
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
await removeNode(selectedNodeId);
|
|
|
|
|
clearSelection();
|
|
|
|
|
onClose?.();
|
|
|
|
|
} catch (err) {
|
|
|
|
|
console.error('Failed to delete node:', err);
|
|
|
|
|
setError(err instanceof Error ? err.message : 'Delete failed');
|
|
|
|
|
} finally {
|
|
|
|
|
setIsUpdating(false);
|
|
|
|
|
}
|
|
|
|
|
}, [selectedNodeId, removeNode, clearSelection, onClose]);
|
|
|
|
|
|
|
|
|
|
// Don't render if no node selected
|
|
|
|
|
if (!selectedNodeId || !spec) {
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Check if this is a synthetic node (model, solver, algorithm, surrogate)
|
|
|
|
|
const isSyntheticNode = ['model', 'solver', 'algorithm', 'surrogate'].includes(selectedNodeId);
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<div className="w-80 bg-dark-850 border-l border-dark-700 flex flex-col h-full">
|
|
|
|
|
{/* Header */}
|
|
|
|
|
<div className="flex justify-between items-center p-4 border-b border-dark-700">
|
|
|
|
|
<h3 className="font-semibold text-white truncate flex-1">
|
|
|
|
|
Configure {nodeLabel}
|
|
|
|
|
</h3>
|
|
|
|
|
<div className="flex items-center gap-2">
|
|
|
|
|
{!isSyntheticNode && (
|
|
|
|
|
<button
|
|
|
|
|
onClick={handleDelete}
|
|
|
|
|
disabled={isUpdating}
|
|
|
|
|
className="p-1.5 text-red-400 hover:text-red-300 hover:bg-red-500/10 rounded transition-colors"
|
|
|
|
|
title="Delete node"
|
|
|
|
|
>
|
|
|
|
|
<Trash2 size={16} />
|
|
|
|
|
</button>
|
|
|
|
|
)}
|
|
|
|
|
{onClose && (
|
|
|
|
|
<button
|
|
|
|
|
onClick={onClose}
|
|
|
|
|
className="p-1.5 text-dark-400 hover:text-white hover:bg-dark-700 rounded transition-colors"
|
|
|
|
|
title="Close panel"
|
|
|
|
|
>
|
|
|
|
|
<X size={16} />
|
|
|
|
|
</button>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{/* Error display */}
|
|
|
|
|
{error && (
|
|
|
|
|
<div className="mx-4 mt-4 p-3 bg-red-500/10 border border-red-500/30 rounded-lg flex items-start gap-2">
|
|
|
|
|
<AlertCircle size={16} className="text-red-400 flex-shrink-0 mt-0.5" />
|
|
|
|
|
<p className="text-sm text-red-400 flex-1">{error}</p>
|
|
|
|
|
<button onClick={() => setError(null)} className="text-red-400 hover:text-red-300">
|
|
|
|
|
<X size={14} />
|
|
|
|
|
</button>
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
{/* Content */}
|
|
|
|
|
<div className="flex-1 overflow-y-auto p-4">
|
|
|
|
|
<div className="space-y-4">
|
|
|
|
|
{/* Loading indicator */}
|
|
|
|
|
{isUpdating && (
|
|
|
|
|
<div className="text-xs text-primary-400 animate-pulse">Updating...</div>
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
{/* Model node (synthetic) */}
|
|
|
|
|
{nodeType === 'model' && spec.model && (
|
|
|
|
|
<ModelNodeConfig spec={spec} />
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
{/* Solver node (synthetic) */}
|
|
|
|
|
{nodeType === 'solver' && (
|
|
|
|
|
<SolverNodeConfig spec={spec} />
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
{/* Algorithm node (synthetic) */}
|
|
|
|
|
{nodeType === 'algorithm' && (
|
|
|
|
|
<AlgorithmNodeConfig spec={spec} />
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
{/* Surrogate node (synthetic) */}
|
|
|
|
|
{nodeType === 'surrogate' && (
|
|
|
|
|
<SurrogateNodeConfig spec={spec} />
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
{/* Design Variable */}
|
|
|
|
|
{nodeType === 'designVar' && selectedNode && (
|
|
|
|
|
<DesignVarNodeConfig
|
|
|
|
|
node={selectedNode as DesignVariable}
|
|
|
|
|
onChange={handleChange}
|
|
|
|
|
/>
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
{/* Extractor */}
|
|
|
|
|
{nodeType === 'extractor' && selectedNode && (
|
|
|
|
|
<ExtractorNodeConfig
|
|
|
|
|
node={selectedNode as Extractor}
|
|
|
|
|
onChange={handleChange}
|
|
|
|
|
/>
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
{/* Objective */}
|
|
|
|
|
{nodeType === 'objective' && selectedNode && (
|
|
|
|
|
<ObjectiveNodeConfig
|
|
|
|
|
node={selectedNode as Objective}
|
|
|
|
|
onChange={handleChange}
|
|
|
|
|
/>
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
{/* Constraint */}
|
|
|
|
|
{nodeType === 'constraint' && selectedNode && (
|
|
|
|
|
<ConstraintNodeConfig
|
|
|
|
|
node={selectedNode as Constraint}
|
|
|
|
|
onChange={handleChange}
|
|
|
|
|
/>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{/* File Browser Modal */}
|
|
|
|
|
<FileBrowser
|
|
|
|
|
isOpen={showFileBrowser}
|
|
|
|
|
onClose={() => setShowFileBrowser(false)}
|
|
|
|
|
onSelect={() => {
|
|
|
|
|
// This would update the model path - but model is synthetic
|
|
|
|
|
setShowFileBrowser(false);
|
|
|
|
|
}}
|
|
|
|
|
fileTypes={['.sim', '.prt', '.fem', '.afem']}
|
|
|
|
|
/>
|
|
|
|
|
|
|
|
|
|
{/* Introspection Panel */}
|
|
|
|
|
{showIntrospection && spec.model.sim?.path && (
|
|
|
|
|
<div className="fixed top-20 right-96 z-40">
|
|
|
|
|
<IntrospectionPanel
|
|
|
|
|
filePath={spec.model.sim.path}
|
2026-01-20 14:14:14 -05:00
|
|
|
studyId={useSpecStore.getState().studyId || undefined}
|
2026-01-20 11:53:26 -05:00
|
|
|
onClose={() => setShowIntrospection(false)}
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ============================================================================
|
|
|
|
|
// Type-specific configuration components
|
|
|
|
|
// ============================================================================
|
|
|
|
|
|
|
|
|
|
interface SpecConfigProps {
|
|
|
|
|
spec: NonNullable<ReturnType<typeof useSpec>>;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function ModelNodeConfig({ spec }: SpecConfigProps) {
|
feat: Add panel management, validation, and error handling to canvas
Phase 1 - Panel Management System:
- Create usePanelStore.ts for centralized panel state management
- Add PanelContainer.tsx for draggable floating panels
- Create FloatingIntrospectionPanel.tsx (persistent, doesn't disappear on node click)
- Create ResultsPanel.tsx for trial result details
- Refactor NodeConfigPanelV2 to use panel store for introspection
- Integrate PanelContainer into CanvasView
Phase 2 - Pre-run Validation:
- Create specValidator.ts with comprehensive validation rules
- Add ValidationPanel (enhanced version with error navigation)
- Add Validate button to SpecRenderer with status indicator
- Block run if validation fails
- Check for: design vars, objectives, extractors, bounds, connections
Phase 3 - Error Handling & Recovery:
- Create ErrorPanel.tsx for displaying optimization errors
- Add error classification (nx_crash, solver_fail, extractor_error, etc.)
- Add recovery suggestions based on error type
- Update status endpoint to return error info
- Add _get_study_error_info helper to check error_status.json and DB
- Integrate error detection into status polling
Documentation:
- Add CANVAS_ROBUSTNESS_PLAN.md with full implementation plan
2026-01-21 21:35:31 -05:00
|
|
|
const { setIntrospectionData, openPanel } = usePanelStore();
|
|
|
|
|
|
|
|
|
|
const handleOpenIntrospection = () => {
|
|
|
|
|
// Set up introspection data and open the panel
|
|
|
|
|
setIntrospectionData({
|
|
|
|
|
filePath: spec.model.sim?.path || '',
|
|
|
|
|
studyId: useSpecStore.getState().studyId || undefined,
|
|
|
|
|
});
|
|
|
|
|
};
|
2026-01-20 11:53:26 -05:00
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<>
|
|
|
|
|
<div>
|
|
|
|
|
<label className={labelClass}>Model File</label>
|
|
|
|
|
<input
|
|
|
|
|
type="text"
|
|
|
|
|
value={spec.model.sim?.path || ''}
|
|
|
|
|
readOnly
|
|
|
|
|
className={`${inputClass} font-mono text-sm bg-dark-900 cursor-not-allowed`}
|
|
|
|
|
title="Model path is read-only. Change via study configuration."
|
|
|
|
|
/>
|
|
|
|
|
<p className="text-xs text-dark-500 mt-1">Read-only. Set in study configuration.</p>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div>
|
|
|
|
|
<label className={labelClass}>Solver Type</label>
|
|
|
|
|
<input
|
|
|
|
|
type="text"
|
|
|
|
|
value={spec.model.sim?.solution_type || 'Not detected'}
|
|
|
|
|
readOnly
|
|
|
|
|
className={`${inputClass} bg-dark-900 cursor-not-allowed`}
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{spec.model.sim?.path && (
|
|
|
|
|
<button
|
feat: Add panel management, validation, and error handling to canvas
Phase 1 - Panel Management System:
- Create usePanelStore.ts for centralized panel state management
- Add PanelContainer.tsx for draggable floating panels
- Create FloatingIntrospectionPanel.tsx (persistent, doesn't disappear on node click)
- Create ResultsPanel.tsx for trial result details
- Refactor NodeConfigPanelV2 to use panel store for introspection
- Integrate PanelContainer into CanvasView
Phase 2 - Pre-run Validation:
- Create specValidator.ts with comprehensive validation rules
- Add ValidationPanel (enhanced version with error navigation)
- Add Validate button to SpecRenderer with status indicator
- Block run if validation fails
- Check for: design vars, objectives, extractors, bounds, connections
Phase 3 - Error Handling & Recovery:
- Create ErrorPanel.tsx for displaying optimization errors
- Add error classification (nx_crash, solver_fail, extractor_error, etc.)
- Add recovery suggestions based on error type
- Update status endpoint to return error info
- Add _get_study_error_info helper to check error_status.json and DB
- Integrate error detection into status polling
Documentation:
- Add CANVAS_ROBUSTNESS_PLAN.md with full implementation plan
2026-01-21 21:35:31 -05:00
|
|
|
onClick={handleOpenIntrospection}
|
2026-01-20 11:53:26 -05:00
|
|
|
className="w-full flex items-center justify-center gap-2 px-3 py-2.5 bg-primary-500/20
|
|
|
|
|
hover:bg-primary-500/30 border border-primary-500/30 rounded-lg
|
|
|
|
|
text-primary-400 text-sm font-medium transition-colors"
|
|
|
|
|
>
|
|
|
|
|
<Microscope size={16} />
|
|
|
|
|
Introspect Model
|
|
|
|
|
</button>
|
|
|
|
|
)}
|
feat: Add panel management, validation, and error handling to canvas
Phase 1 - Panel Management System:
- Create usePanelStore.ts for centralized panel state management
- Add PanelContainer.tsx for draggable floating panels
- Create FloatingIntrospectionPanel.tsx (persistent, doesn't disappear on node click)
- Create ResultsPanel.tsx for trial result details
- Refactor NodeConfigPanelV2 to use panel store for introspection
- Integrate PanelContainer into CanvasView
Phase 2 - Pre-run Validation:
- Create specValidator.ts with comprehensive validation rules
- Add ValidationPanel (enhanced version with error navigation)
- Add Validate button to SpecRenderer with status indicator
- Block run if validation fails
- Check for: design vars, objectives, extractors, bounds, connections
Phase 3 - Error Handling & Recovery:
- Create ErrorPanel.tsx for displaying optimization errors
- Add error classification (nx_crash, solver_fail, extractor_error, etc.)
- Add recovery suggestions based on error type
- Update status endpoint to return error info
- Add _get_study_error_info helper to check error_status.json and DB
- Integrate error detection into status polling
Documentation:
- Add CANVAS_ROBUSTNESS_PLAN.md with full implementation plan
2026-01-21 21:35:31 -05:00
|
|
|
|
|
|
|
|
{/* Note: IntrospectionPanel is now rendered by PanelContainer, not here */}
|
2026-01-20 11:53:26 -05:00
|
|
|
</>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function SolverNodeConfig({ spec }: SpecConfigProps) {
|
|
|
|
|
return (
|
|
|
|
|
<div>
|
|
|
|
|
<label className={labelClass}>Solution Type</label>
|
|
|
|
|
<input
|
|
|
|
|
type="text"
|
|
|
|
|
value={spec.model.sim?.solution_type || 'Not configured'}
|
|
|
|
|
readOnly
|
|
|
|
|
className={`${inputClass} bg-dark-900 cursor-not-allowed`}
|
|
|
|
|
title="Solver type is determined by the model file."
|
|
|
|
|
/>
|
|
|
|
|
<p className="text-xs text-dark-500 mt-1">Detected from model file.</p>
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function AlgorithmNodeConfig({ spec }: SpecConfigProps) {
|
|
|
|
|
const algo = spec.optimization.algorithm;
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<>
|
|
|
|
|
<div>
|
|
|
|
|
<label className={labelClass}>Method</label>
|
|
|
|
|
<input
|
|
|
|
|
type="text"
|
|
|
|
|
value={algo?.type || 'TPE'}
|
|
|
|
|
readOnly
|
|
|
|
|
className={`${inputClass} bg-dark-900 cursor-not-allowed`}
|
|
|
|
|
/>
|
|
|
|
|
<p className="text-xs text-dark-500 mt-1">Edit in optimization settings.</p>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div>
|
|
|
|
|
<label className={labelClass}>Max Trials</label>
|
|
|
|
|
<input
|
|
|
|
|
type="number"
|
|
|
|
|
value={spec.optimization.budget?.max_trials || 100}
|
|
|
|
|
readOnly
|
|
|
|
|
className={`${inputClass} bg-dark-900 cursor-not-allowed`}
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
</>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function SurrogateNodeConfig({ spec }: SpecConfigProps) {
|
|
|
|
|
const surrogate = spec.optimization.surrogate;
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<>
|
|
|
|
|
<div className="flex items-center gap-2">
|
|
|
|
|
<input
|
|
|
|
|
type="checkbox"
|
|
|
|
|
id="surrogate-enabled"
|
|
|
|
|
checked={surrogate?.enabled || false}
|
|
|
|
|
readOnly
|
|
|
|
|
className="w-4 h-4 rounded bg-dark-800 border-dark-600 text-primary-500 cursor-not-allowed"
|
|
|
|
|
/>
|
|
|
|
|
<label htmlFor="surrogate-enabled" className="text-sm font-medium text-dark-300">
|
|
|
|
|
Neural Surrogate {surrogate?.enabled ? 'Enabled' : 'Disabled'}
|
|
|
|
|
</label>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{surrogate?.enabled && (
|
|
|
|
|
<>
|
|
|
|
|
<div>
|
|
|
|
|
<label className={labelClass}>Model Type</label>
|
|
|
|
|
<input
|
|
|
|
|
type="text"
|
|
|
|
|
value={surrogate.type || 'MLP'}
|
|
|
|
|
readOnly
|
|
|
|
|
className={`${inputClass} bg-dark-900 cursor-not-allowed`}
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div>
|
|
|
|
|
<label className={labelClass}>Min Training Samples</label>
|
|
|
|
|
<input
|
|
|
|
|
type="number"
|
|
|
|
|
value={surrogate.config?.min_training_samples || 20}
|
|
|
|
|
readOnly
|
|
|
|
|
className={`${inputClass} bg-dark-900 cursor-not-allowed`}
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
</>
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
<p className="text-xs text-dark-500">Edit in optimization settings.</p>
|
|
|
|
|
</>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ============================================================================
|
|
|
|
|
// Editable node configs
|
|
|
|
|
// ============================================================================
|
|
|
|
|
|
|
|
|
|
interface DesignVarNodeConfigProps {
|
|
|
|
|
node: DesignVariable;
|
|
|
|
|
onChange: (field: string, value: unknown) => void;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function DesignVarNodeConfig({ node, onChange }: DesignVarNodeConfigProps) {
|
|
|
|
|
return (
|
|
|
|
|
<>
|
|
|
|
|
<div>
|
|
|
|
|
<label className={labelClass}>Name</label>
|
|
|
|
|
<input
|
|
|
|
|
type="text"
|
|
|
|
|
value={node.name}
|
|
|
|
|
onChange={(e) => onChange('name', e.target.value)}
|
|
|
|
|
className={inputClass}
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div>
|
|
|
|
|
<label className={labelClass}>Expression Name</label>
|
|
|
|
|
<input
|
|
|
|
|
type="text"
|
|
|
|
|
value={node.expression_name}
|
|
|
|
|
onChange={(e) => onChange('expression_name', e.target.value)}
|
|
|
|
|
placeholder="NX expression name"
|
|
|
|
|
className={`${inputClass} font-mono text-sm`}
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div className="grid grid-cols-2 gap-2">
|
|
|
|
|
<div>
|
|
|
|
|
<label className={labelClass}>Min</label>
|
|
|
|
|
<input
|
|
|
|
|
type="number"
|
|
|
|
|
value={node.bounds.min}
|
|
|
|
|
onChange={(e) => onChange('bounds', { ...node.bounds, min: parseFloat(e.target.value) })}
|
|
|
|
|
className={inputClass}
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
<div>
|
|
|
|
|
<label className={labelClass}>Max</label>
|
|
|
|
|
<input
|
|
|
|
|
type="number"
|
|
|
|
|
value={node.bounds.max}
|
|
|
|
|
onChange={(e) => onChange('bounds', { ...node.bounds, max: parseFloat(e.target.value) })}
|
|
|
|
|
className={inputClass}
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{node.baseline !== undefined && (
|
|
|
|
|
<div>
|
|
|
|
|
<label className={labelClass}>Baseline</label>
|
|
|
|
|
<input
|
|
|
|
|
type="number"
|
|
|
|
|
value={node.baseline}
|
|
|
|
|
onChange={(e) => onChange('baseline', parseFloat(e.target.value))}
|
|
|
|
|
className={inputClass}
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
<div>
|
|
|
|
|
<label className={labelClass}>Units</label>
|
|
|
|
|
<input
|
|
|
|
|
type="text"
|
|
|
|
|
value={node.units || ''}
|
|
|
|
|
onChange={(e) => onChange('units', e.target.value)}
|
|
|
|
|
placeholder="mm"
|
|
|
|
|
className={inputClass}
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div className="flex items-center gap-2">
|
|
|
|
|
<input
|
|
|
|
|
type="checkbox"
|
|
|
|
|
id={`${node.id}-enabled`}
|
|
|
|
|
checked={node.enabled !== false}
|
|
|
|
|
onChange={(e) => onChange('enabled', e.target.checked)}
|
|
|
|
|
className="w-4 h-4 rounded bg-dark-800 border-dark-600 text-primary-500 focus:ring-primary-500"
|
|
|
|
|
/>
|
|
|
|
|
<label htmlFor={`${node.id}-enabled`} className="text-sm text-dark-300">
|
|
|
|
|
Enabled
|
|
|
|
|
</label>
|
|
|
|
|
</div>
|
|
|
|
|
</>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
interface ExtractorNodeConfigProps {
|
|
|
|
|
node: Extractor;
|
|
|
|
|
onChange: (field: string, value: unknown) => void;
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-20 13:08:12 -05:00
|
|
|
// Default 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.
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
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]
|
|
|
|
|
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,
|
|
|
|
|
}
|
|
|
|
|
`;
|
|
|
|
|
|
2026-01-20 11:53:26 -05:00
|
|
|
function ExtractorNodeConfig({ node, onChange }: ExtractorNodeConfigProps) {
|
2026-01-20 13:08:12 -05:00
|
|
|
const [showCodeEditor, setShowCodeEditor] = useState(false);
|
|
|
|
|
const studyId = useSpecStore(state => state.studyId);
|
|
|
|
|
|
2026-01-20 11:53:26 -05:00
|
|
|
const extractorOptions = [
|
|
|
|
|
{ id: 'E1', name: 'Displacement', type: 'displacement' },
|
|
|
|
|
{ id: 'E2', name: 'Frequency', type: 'frequency' },
|
|
|
|
|
{ id: 'E3', name: 'Solid Stress', type: 'solid_stress' },
|
|
|
|
|
{ id: 'E4', name: 'BDF Mass', type: 'mass_bdf' },
|
|
|
|
|
{ id: 'E5', name: 'CAD Mass', type: 'mass_expression' },
|
|
|
|
|
{ id: 'E8', name: 'Zernike (OP2)', type: 'zernike_op2' },
|
|
|
|
|
{ id: 'E9', name: 'Zernike (CSV)', type: 'zernike_csv' },
|
|
|
|
|
{ id: 'E10', name: 'Zernike (RMS)', type: 'zernike_rms' },
|
|
|
|
|
];
|
|
|
|
|
|
2026-01-20 13:08:12 -05:00
|
|
|
// Check if this is a custom function type
|
|
|
|
|
// Using string comparison to handle both 'custom_function' and potential legacy 'custom' values
|
|
|
|
|
const isCustomType = node.type === 'custom_function' || (node.type as string) === 'custom';
|
|
|
|
|
|
|
|
|
|
// Get current source code
|
|
|
|
|
const currentCode = node.function?.source_code || DEFAULT_EXTRACTOR_TEMPLATE;
|
|
|
|
|
|
|
|
|
|
// Handle Claude generation request (non-streaming fallback)
|
|
|
|
|
const handleRequestGeneration = useCallback(async (prompt: string): Promise<string> => {
|
|
|
|
|
const response = await generateExtractorCode({
|
|
|
|
|
prompt,
|
|
|
|
|
study_id: studyId || undefined,
|
|
|
|
|
existing_code: node.function?.source_code,
|
|
|
|
|
output_names: node.outputs?.map(o => o.name) || [],
|
|
|
|
|
});
|
|
|
|
|
return response.code;
|
|
|
|
|
}, [studyId, node.function?.source_code, node.outputs]);
|
|
|
|
|
|
|
|
|
|
// Handle streaming generation (preferred)
|
|
|
|
|
const handleStreamingGeneration = useCallback((
|
|
|
|
|
request: { prompt: string; study_id?: string; existing_code?: string; output_names?: string[] },
|
|
|
|
|
callbacks: { onToken: (t: string) => void; onComplete: (c: string, o: string[]) => void; onError: (e: string) => void }
|
|
|
|
|
) => {
|
|
|
|
|
return streamExtractorCode(request, callbacks);
|
|
|
|
|
}, []);
|
|
|
|
|
|
|
|
|
|
// Handle code change from editor
|
|
|
|
|
const handleCodeChange = useCallback((code: string) => {
|
|
|
|
|
onChange('function', {
|
|
|
|
|
...node.function,
|
|
|
|
|
name: node.function?.name || 'custom_extract',
|
|
|
|
|
source_code: code,
|
|
|
|
|
});
|
|
|
|
|
}, [node.function, onChange]);
|
|
|
|
|
|
|
|
|
|
// Handle code validation (includes syntax check and dependency check)
|
|
|
|
|
const handleValidateCode = useCallback(async (code: string) => {
|
|
|
|
|
// First check syntax
|
|
|
|
|
const syntaxResult = await validateExtractorCode(code);
|
|
|
|
|
if (!syntaxResult.valid) {
|
|
|
|
|
return {
|
|
|
|
|
success: false,
|
|
|
|
|
error: syntaxResult.error,
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Then check dependencies
|
|
|
|
|
const depResult = await checkCodeDependencies(code);
|
|
|
|
|
|
|
|
|
|
// Build combined result
|
|
|
|
|
const warnings: string[] = [...depResult.warnings];
|
|
|
|
|
if (depResult.missing.length > 0) {
|
|
|
|
|
warnings.push(`Missing packages: ${depResult.missing.join(', ')}`);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
success: true,
|
|
|
|
|
error: warnings.length > 0 ? `Warnings: ${warnings.join('; ')}` : undefined,
|
|
|
|
|
outputs: depResult.imports.length > 0 ? { imports: depResult.imports.join(', ') } : undefined,
|
|
|
|
|
};
|
|
|
|
|
}, []);
|
|
|
|
|
|
|
|
|
|
// Handle live testing against OP2 file
|
|
|
|
|
const handleTestCode = useCallback(async (code: string) => {
|
|
|
|
|
const result = await testExtractorCode({
|
|
|
|
|
code,
|
|
|
|
|
study_id: studyId || undefined,
|
|
|
|
|
subcase_id: 1,
|
|
|
|
|
});
|
|
|
|
|
return result;
|
|
|
|
|
}, [studyId]);
|
|
|
|
|
|
2026-01-20 11:53:26 -05:00
|
|
|
return (
|
|
|
|
|
<>
|
|
|
|
|
<div>
|
|
|
|
|
<label className={labelClass}>Name</label>
|
|
|
|
|
<input
|
|
|
|
|
type="text"
|
|
|
|
|
value={node.name}
|
|
|
|
|
onChange={(e) => onChange('name', e.target.value)}
|
|
|
|
|
className={inputClass}
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div>
|
|
|
|
|
<label className={labelClass}>Extractor Type</label>
|
|
|
|
|
<select
|
|
|
|
|
value={node.type}
|
|
|
|
|
onChange={(e) => onChange('type', e.target.value)}
|
|
|
|
|
className={selectClass}
|
|
|
|
|
>
|
|
|
|
|
<option value="">Select...</option>
|
|
|
|
|
{extractorOptions.map(opt => (
|
|
|
|
|
<option key={opt.id} value={opt.type}>
|
|
|
|
|
{opt.id} - {opt.name}
|
|
|
|
|
</option>
|
|
|
|
|
))}
|
2026-01-20 13:08:12 -05:00
|
|
|
<option value="custom_function">Custom Function</option>
|
2026-01-20 11:53:26 -05:00
|
|
|
</select>
|
|
|
|
|
</div>
|
|
|
|
|
|
2026-01-20 13:08:12 -05:00
|
|
|
{/* Custom Code Editor Button */}
|
|
|
|
|
{isCustomType && (
|
|
|
|
|
<>
|
|
|
|
|
<button
|
|
|
|
|
onClick={() => setShowCodeEditor(true)}
|
|
|
|
|
className="w-full flex items-center justify-center gap-2 px-3 py-2.5
|
|
|
|
|
bg-violet-500/20 hover:bg-violet-500/30 border border-violet-500/30
|
|
|
|
|
rounded-lg text-violet-400 text-sm font-medium transition-colors"
|
|
|
|
|
>
|
|
|
|
|
<Code size={16} />
|
|
|
|
|
Edit Custom Code
|
|
|
|
|
</button>
|
|
|
|
|
|
|
|
|
|
{node.function?.source_code && (
|
|
|
|
|
<div className="text-xs text-dark-500 flex items-center gap-1.5">
|
|
|
|
|
<FileCode size={12} />
|
|
|
|
|
Custom code defined ({node.function.source_code.split('\n').length} lines)
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
</>
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
{/* Code Editor Modal */}
|
|
|
|
|
{showCodeEditor && (
|
|
|
|
|
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/60 backdrop-blur-sm">
|
|
|
|
|
<div className="w-[900px] h-[700px] bg-dark-850 rounded-xl overflow-hidden shadow-2xl border border-dark-600 flex flex-col">
|
2026-01-20 14:14:14 -05:00
|
|
|
{/* Code Editor with built-in header containing toolbar buttons */}
|
|
|
|
|
<CodeEditorPanel
|
|
|
|
|
initialCode={currentCode}
|
|
|
|
|
extractorName={`Custom Extractor: ${node.name}`}
|
|
|
|
|
outputs={node.outputs?.map(o => o.name) || []}
|
|
|
|
|
onChange={handleCodeChange}
|
|
|
|
|
onRequestGeneration={handleRequestGeneration}
|
|
|
|
|
onRequestStreamingGeneration={handleStreamingGeneration}
|
|
|
|
|
onRun={handleValidateCode}
|
|
|
|
|
onTest={handleTestCode}
|
|
|
|
|
onClose={() => setShowCodeEditor(false)}
|
|
|
|
|
showHeader={true}
|
|
|
|
|
height="100%"
|
|
|
|
|
studyId={studyId || undefined}
|
|
|
|
|
/>
|
2026-01-20 13:08:12 -05:00
|
|
|
</div>
|
2026-01-20 11:53:26 -05:00
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
|
2026-01-20 13:08:12 -05:00
|
|
|
{/* Outputs */}
|
2026-01-20 11:53:26 -05:00
|
|
|
<div>
|
|
|
|
|
<label className={labelClass}>Outputs</label>
|
|
|
|
|
<input
|
|
|
|
|
type="text"
|
|
|
|
|
value={node.outputs?.map(o => o.name).join(', ') || ''}
|
|
|
|
|
readOnly
|
|
|
|
|
placeholder="value, unit"
|
|
|
|
|
className={`${inputClass} bg-dark-900 cursor-not-allowed`}
|
|
|
|
|
/>
|
2026-01-20 13:08:12 -05:00
|
|
|
<p className="text-xs text-dark-500 mt-1">
|
|
|
|
|
{isCustomType
|
|
|
|
|
? 'Detected from return statement in code.'
|
|
|
|
|
: 'Outputs are defined by extractor type.'}
|
|
|
|
|
</p>
|
2026-01-20 11:53:26 -05:00
|
|
|
</div>
|
|
|
|
|
</>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
interface ObjectiveNodeConfigProps {
|
|
|
|
|
node: Objective;
|
|
|
|
|
onChange: (field: string, value: unknown) => void;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function ObjectiveNodeConfig({ node, onChange }: ObjectiveNodeConfigProps) {
|
|
|
|
|
return (
|
|
|
|
|
<>
|
|
|
|
|
<div>
|
|
|
|
|
<label className={labelClass}>Name</label>
|
|
|
|
|
<input
|
|
|
|
|
type="text"
|
|
|
|
|
value={node.name}
|
|
|
|
|
onChange={(e) => onChange('name', e.target.value)}
|
|
|
|
|
className={inputClass}
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div>
|
|
|
|
|
<label className={labelClass}>Direction</label>
|
|
|
|
|
<select
|
|
|
|
|
value={node.direction}
|
|
|
|
|
onChange={(e) => onChange('direction', e.target.value)}
|
|
|
|
|
className={selectClass}
|
|
|
|
|
>
|
|
|
|
|
<option value="minimize">Minimize</option>
|
|
|
|
|
<option value="maximize">Maximize</option>
|
|
|
|
|
</select>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div>
|
|
|
|
|
<label className={labelClass}>Weight</label>
|
|
|
|
|
<input
|
|
|
|
|
type="number"
|
|
|
|
|
step="0.1"
|
|
|
|
|
min="0"
|
|
|
|
|
value={node.weight ?? 1}
|
|
|
|
|
onChange={(e) => onChange('weight', parseFloat(e.target.value))}
|
|
|
|
|
className={inputClass}
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{node.target !== undefined && (
|
|
|
|
|
<div>
|
|
|
|
|
<label className={labelClass}>Target Value</label>
|
|
|
|
|
<input
|
|
|
|
|
type="number"
|
|
|
|
|
value={node.target}
|
|
|
|
|
onChange={(e) => onChange('target', parseFloat(e.target.value))}
|
|
|
|
|
className={inputClass}
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
</>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
interface ConstraintNodeConfigProps {
|
|
|
|
|
node: Constraint;
|
|
|
|
|
onChange: (field: string, value: unknown) => void;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function ConstraintNodeConfig({ node, onChange }: ConstraintNodeConfigProps) {
|
|
|
|
|
return (
|
|
|
|
|
<>
|
|
|
|
|
<div>
|
|
|
|
|
<label className={labelClass}>Name</label>
|
|
|
|
|
<input
|
|
|
|
|
type="text"
|
|
|
|
|
value={node.name}
|
|
|
|
|
onChange={(e) => onChange('name', e.target.value)}
|
|
|
|
|
className={inputClass}
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div className="grid grid-cols-2 gap-2">
|
|
|
|
|
<div>
|
|
|
|
|
<label className={labelClass}>Type</label>
|
|
|
|
|
<select
|
|
|
|
|
value={node.type}
|
|
|
|
|
onChange={(e) => onChange('type', e.target.value)}
|
|
|
|
|
className={selectClass}
|
|
|
|
|
>
|
|
|
|
|
<option value="less_than">< Less than</option>
|
|
|
|
|
<option value="less_equal"><= Less or equal</option>
|
|
|
|
|
<option value="greater_than">> Greater than</option>
|
|
|
|
|
<option value="greater_equal">>= Greater or equal</option>
|
|
|
|
|
<option value="equal">= Equal</option>
|
|
|
|
|
</select>
|
|
|
|
|
</div>
|
|
|
|
|
<div>
|
|
|
|
|
<label className={labelClass}>Threshold</label>
|
|
|
|
|
<input
|
|
|
|
|
type="number"
|
|
|
|
|
value={node.threshold}
|
|
|
|
|
onChange={(e) => onChange('threshold', parseFloat(e.target.value))}
|
|
|
|
|
className={inputClass}
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
</>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export default NodeConfigPanelV2;
|