fix(canvas): Bug fixes for node movement, drag-drop, config panel, and introspection
- SpecRenderer: Add localNodes state with applyNodeChanges for smooth node dragging - SpecRenderer: Fix getDefaultNodeData() - extractor uses 'custom_function' type with function definition - SpecRenderer: Fix constraint default - use constraint_type instead of type - CanvasView: Show config panel INSTEAD of chat when node selected (not blocked) - NodeConfigPanelV2: Enable showHeader for code editor toolbar (Generate/Snippets/Validate/Test buttons) - NodeConfigPanelV2: Pass studyId to IntrospectionPanel - IntrospectionPanel: Accept studyId prop and use correct API endpoint - optimization.py: Search multiple directories for model files including 1_setup/model/
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -10,7 +10,7 @@
|
|||||||
* P2.7-P2.10: SpecRenderer component with node/edge/selection handling
|
* P2.7-P2.10: SpecRenderer component with node/edge/selection handling
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { useCallback, useRef, useEffect, useMemo, DragEvent } from 'react';
|
import { useCallback, useRef, useEffect, useMemo, useState, DragEvent } from 'react';
|
||||||
import ReactFlow, {
|
import ReactFlow, {
|
||||||
Background,
|
Background,
|
||||||
Controls,
|
Controls,
|
||||||
@@ -22,6 +22,7 @@ import ReactFlow, {
|
|||||||
NodeChange,
|
NodeChange,
|
||||||
EdgeChange,
|
EdgeChange,
|
||||||
Connection,
|
Connection,
|
||||||
|
applyNodeChanges,
|
||||||
} from 'reactflow';
|
} from 'reactflow';
|
||||||
import 'reactflow/dist/style.css';
|
import 'reactflow/dist/style.css';
|
||||||
|
|
||||||
@@ -74,8 +75,28 @@ function getDefaultNodeData(type: AddableNodeType, position: { x: number; y: num
|
|||||||
case 'extractor':
|
case 'extractor':
|
||||||
return {
|
return {
|
||||||
name: `extractor_${timestamp}`,
|
name: `extractor_${timestamp}`,
|
||||||
type: 'custom',
|
type: 'custom_function', // Must be valid ExtractorType
|
||||||
|
builtin: false,
|
||||||
enabled: true,
|
enabled: true,
|
||||||
|
// Custom function extractors need a function definition
|
||||||
|
function: {
|
||||||
|
name: 'extract',
|
||||||
|
source_code: `def extract(op2_path: str, config: dict = None) -> dict:
|
||||||
|
"""
|
||||||
|
Custom extractor function.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
op2_path: Path to the OP2 results file
|
||||||
|
config: Optional configuration dict
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Dictionary with extracted values
|
||||||
|
"""
|
||||||
|
# TODO: Implement extraction logic
|
||||||
|
return {'value': 0.0}
|
||||||
|
`,
|
||||||
|
},
|
||||||
|
outputs: [{ name: 'value', metric: 'custom' }],
|
||||||
canvas_position: position,
|
canvas_position: position,
|
||||||
};
|
};
|
||||||
case 'objective':
|
case 'objective':
|
||||||
@@ -90,7 +111,8 @@ function getDefaultNodeData(type: AddableNodeType, position: { x: number; y: num
|
|||||||
case 'constraint':
|
case 'constraint':
|
||||||
return {
|
return {
|
||||||
name: `constraint_${timestamp}`,
|
name: `constraint_${timestamp}`,
|
||||||
type: 'upper',
|
constraint_type: 'hard', // Must be 'hard' or 'soft'
|
||||||
|
operator: '<=',
|
||||||
limit: 1.0,
|
limit: 1.0,
|
||||||
source_extractor_id: null,
|
source_extractor_id: null,
|
||||||
source_output: null,
|
source_output: null,
|
||||||
@@ -208,12 +230,23 @@ function SpecRendererInner({
|
|||||||
nodesRef.current = nodes;
|
nodesRef.current = nodes;
|
||||||
}, [nodes]);
|
}, [nodes]);
|
||||||
|
|
||||||
|
// Track local node state for smooth dragging
|
||||||
|
const [localNodes, setLocalNodes] = useState(nodes);
|
||||||
|
|
||||||
|
// Sync local nodes with spec-derived nodes when spec changes
|
||||||
|
useEffect(() => {
|
||||||
|
setLocalNodes(nodes);
|
||||||
|
}, [nodes]);
|
||||||
|
|
||||||
// Handle node position changes
|
// Handle node position changes
|
||||||
const onNodesChange = useCallback(
|
const onNodesChange = useCallback(
|
||||||
(changes: NodeChange[]) => {
|
(changes: NodeChange[]) => {
|
||||||
if (!editable) return;
|
if (!editable) return;
|
||||||
|
|
||||||
// Handle position changes
|
// Apply changes to local state for smooth dragging
|
||||||
|
setLocalNodes((nds) => applyNodeChanges(changes, nds));
|
||||||
|
|
||||||
|
// Handle position changes - save to spec when drag ends
|
||||||
for (const change of changes) {
|
for (const change of changes) {
|
||||||
if (change.type === 'position' && change.position && change.dragging === false) {
|
if (change.type === 'position' && change.position && change.dragging === false) {
|
||||||
// Dragging ended - update spec
|
// Dragging ended - update spec
|
||||||
@@ -458,7 +491,7 @@ function SpecRendererInner({
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
<ReactFlow
|
<ReactFlow
|
||||||
nodes={nodes}
|
nodes={localNodes}
|
||||||
edges={edges}
|
edges={edges}
|
||||||
onNodesChange={onNodesChange}
|
onNodesChange={onNodesChange}
|
||||||
onEdgesChange={onEdgesChange}
|
onEdgesChange={onEdgesChange}
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ import { useCanvasStore } from '../../../hooks/useCanvasStore';
|
|||||||
|
|
||||||
interface IntrospectionPanelProps {
|
interface IntrospectionPanelProps {
|
||||||
filePath: string;
|
filePath: string;
|
||||||
|
studyId?: string;
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -56,7 +57,7 @@ interface IntrospectionResult {
|
|||||||
warnings: string[];
|
warnings: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export function IntrospectionPanel({ filePath, onClose }: IntrospectionPanelProps) {
|
export function IntrospectionPanel({ filePath, studyId, onClose }: IntrospectionPanelProps) {
|
||||||
const [result, setResult] = useState<IntrospectionResult | null>(null);
|
const [result, setResult] = useState<IntrospectionResult | null>(null);
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
@@ -73,21 +74,37 @@ export function IntrospectionPanel({ filePath, onClose }: IntrospectionPanelProp
|
|||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
setError(null);
|
setError(null);
|
||||||
try {
|
try {
|
||||||
const res = await fetch('/api/nx/introspect', {
|
let res;
|
||||||
method: 'POST',
|
|
||||||
headers: { 'Content-Type': 'application/json' },
|
// If we have a studyId, use the study-aware introspection endpoint
|
||||||
body: JSON.stringify({ file_path: filePath }),
|
if (studyId) {
|
||||||
});
|
// Don't encode studyId - it may contain slashes for nested paths (e.g., M1_Mirror/study_name)
|
||||||
if (!res.ok) throw new Error('Introspection failed');
|
res = await fetch(`/api/optimization/studies/${studyId}/nx/introspect`);
|
||||||
|
} else {
|
||||||
|
// Fallback to direct path introspection
|
||||||
|
res = await fetch('/api/nx/introspect', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ file_path: filePath }),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!res.ok) {
|
||||||
|
const errData = await res.json().catch(() => ({}));
|
||||||
|
throw new Error(errData.detail || 'Introspection failed');
|
||||||
|
}
|
||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
setResult(data);
|
|
||||||
|
// Handle different response formats
|
||||||
|
setResult(data.introspection || data);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
setError('Failed to introspect model');
|
const msg = e instanceof Error ? e.message : 'Failed to introspect model';
|
||||||
console.error(e);
|
setError(msg);
|
||||||
|
console.error('Introspection error:', e);
|
||||||
} finally {
|
} finally {
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
}
|
}
|
||||||
}, [filePath]);
|
}, [filePath, studyId]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
runIntrospection();
|
runIntrospection();
|
||||||
|
|||||||
@@ -254,6 +254,7 @@ export function NodeConfigPanelV2({ onClose }: NodeConfigPanelV2Props) {
|
|||||||
<div className="fixed top-20 right-96 z-40">
|
<div className="fixed top-20 right-96 z-40">
|
||||||
<IntrospectionPanel
|
<IntrospectionPanel
|
||||||
filePath={spec.model.sim.path}
|
filePath={spec.model.sim.path}
|
||||||
|
studyId={useSpecStore.getState().studyId || undefined}
|
||||||
onClose={() => setShowIntrospection(false)}
|
onClose={() => setShowIntrospection(false)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -313,6 +314,7 @@ function ModelNodeConfig({ spec }: SpecConfigProps) {
|
|||||||
<div className="fixed top-20 right-96 z-40">
|
<div className="fixed top-20 right-96 z-40">
|
||||||
<IntrospectionPanel
|
<IntrospectionPanel
|
||||||
filePath={spec.model.sim.path}
|
filePath={spec.model.sim.path}
|
||||||
|
studyId={useSpecStore.getState().studyId || undefined}
|
||||||
onClose={() => setShowIntrospection(false)}
|
onClose={() => setShowIntrospection(false)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -694,38 +696,21 @@ function ExtractorNodeConfig({ node, onChange }: ExtractorNodeConfigProps) {
|
|||||||
{showCodeEditor && (
|
{showCodeEditor && (
|
||||||
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/60 backdrop-blur-sm">
|
<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">
|
<div className="w-[900px] h-[700px] bg-dark-850 rounded-xl overflow-hidden shadow-2xl border border-dark-600 flex flex-col">
|
||||||
{/* Modal Header */}
|
{/* Code Editor with built-in header containing toolbar buttons */}
|
||||||
<div className="flex items-center justify-between px-4 py-3 border-b border-dark-700 bg-dark-900">
|
<CodeEditorPanel
|
||||||
<div className="flex items-center gap-3">
|
initialCode={currentCode}
|
||||||
<FileCode size={18} className="text-violet-400" />
|
extractorName={`Custom Extractor: ${node.name}`}
|
||||||
<span className="font-medium text-white">Custom Extractor: {node.name}</span>
|
outputs={node.outputs?.map(o => o.name) || []}
|
||||||
<span className="text-xs text-dark-500 bg-dark-800 px-2 py-0.5 rounded">.py</span>
|
onChange={handleCodeChange}
|
||||||
</div>
|
onRequestGeneration={handleRequestGeneration}
|
||||||
<button
|
onRequestStreamingGeneration={handleStreamingGeneration}
|
||||||
onClick={() => setShowCodeEditor(false)}
|
onRun={handleValidateCode}
|
||||||
className="p-1.5 rounded hover:bg-dark-700 text-dark-400 hover:text-white transition-colors"
|
onTest={handleTestCode}
|
||||||
>
|
onClose={() => setShowCodeEditor(false)}
|
||||||
<X size={18} />
|
showHeader={true}
|
||||||
</button>
|
height="100%"
|
||||||
</div>
|
studyId={studyId || undefined}
|
||||||
|
/>
|
||||||
{/* Code Editor */}
|
|
||||||
<div className="flex-1">
|
|
||||||
<CodeEditorPanel
|
|
||||||
initialCode={currentCode}
|
|
||||||
extractorName={node.name}
|
|
||||||
outputs={node.outputs?.map(o => o.name) || []}
|
|
||||||
onChange={handleCodeChange}
|
|
||||||
onRequestGeneration={handleRequestGeneration}
|
|
||||||
onRequestStreamingGeneration={handleStreamingGeneration}
|
|
||||||
onRun={handleValidateCode}
|
|
||||||
onTest={handleTestCode}
|
|
||||||
onClose={() => setShowCodeEditor(false)}
|
|
||||||
showHeader={false}
|
|
||||||
height="100%"
|
|
||||||
studyId={studyId || undefined}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -472,7 +472,8 @@ export function CanvasView() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Config Panel - use V2 for spec mode, legacy for AtomizerCanvas */}
|
{/* Config Panel - use V2 for spec mode, legacy for AtomizerCanvas */}
|
||||||
{selectedNodeId && !showChat && (
|
{/* Shows INSTEAD of chat when a node is selected */}
|
||||||
|
{selectedNodeId ? (
|
||||||
useSpecMode ? (
|
useSpecMode ? (
|
||||||
<NodeConfigPanelV2 onClose={() => useSpecStore.getState().clearSelection()} />
|
<NodeConfigPanelV2 onClose={() => useSpecStore.getState().clearSelection()} />
|
||||||
) : (
|
) : (
|
||||||
@@ -480,10 +481,7 @@ export function CanvasView() {
|
|||||||
<NodeConfigPanel nodeId={selectedNodeId} />
|
<NodeConfigPanel nodeId={selectedNodeId} />
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
)}
|
) : showChat ? (
|
||||||
|
|
||||||
{/* Chat/Assistant Panel */}
|
|
||||||
{showChat && (
|
|
||||||
<div className="w-96 border-l border-dark-700 bg-dark-850 flex flex-col">
|
<div className="w-96 border-l border-dark-700 bg-dark-850 flex flex-col">
|
||||||
{/* Chat Header */}
|
{/* Chat Header */}
|
||||||
<div className="flex items-center justify-between px-4 py-3 border-b border-dark-700">
|
<div className="flex items-center justify-between px-4 py-3 border-b border-dark-700">
|
||||||
@@ -524,7 +522,7 @@ export function CanvasView() {
|
|||||||
isConnected={isConnected}
|
isConnected={isConnected}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
) : null}
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
{/* Template Selector Modal */}
|
{/* Template Selector Modal */}
|
||||||
|
|||||||
Reference in New Issue
Block a user