/** * Introspection Panel - Shows discovered expressions and extractors from NX model */ import { useState, useEffect, useCallback, useMemo } from 'react'; import { X, Search, RefreshCw, Plus, ChevronDown, ChevronRight, FileBox, Cpu, FlaskConical, SlidersHorizontal, AlertTriangle, Scale, Link, Box, Settings2, GitBranch, File, } from 'lucide-react'; import { useCanvasStore } from '../../../hooks/useCanvasStore'; interface IntrospectionPanelProps { filePath: string; studyId?: string; onClose: () => void; } interface Expression { name: string; value: number; rhs?: string; min?: number; max?: number; unit?: string; units?: string; // API returns 'units' not 'unit' type: string; source?: string; } interface Extractor { id: string; name: string; description?: string; always?: boolean; } interface DependentFile { path: string; type: string; name: string; } // File dependency structure from backend interface FileDependencies { files: { sim: string[]; afm: string[]; fem: string[]; prt: string[]; idealized: string[]; }; dependencies: Array<{ source: string; target: string; type: string; }>; root_sim: string | null; } // The API returns expressions in a nested structure interface ExpressionsResult { user: Expression[]; internal: Expression[]; total_count: number; user_count: number; } interface IntrospectionResult { part_file?: string; part_path?: string; file_path?: string; file_type?: string; success?: boolean; error?: string | null; // Expressions can be either an array (old format) or object with user/internal (new format) expressions: Expression[] | ExpressionsResult; solver_type?: string | null; dependent_files?: DependentFile[]; extractors_available?: Extractor[]; warnings?: string[]; // Additional fields from NX introspection mass_properties?: Record; materials?: Record; bodies?: Record; attributes?: Array<{ title: string; value: string }>; units?: Record; linked_parts?: Record; file_dependencies?: FileDependencies; } // Baseline run result interface interface BaselineRunResult { success: boolean; study_id: string; baseline_dir: string; sim_file?: string; elapsed_time?: number; result_files?: { op2: string[]; f06: string[]; bdf: string[]; }; errors?: string[]; error?: string; message: string; } export function IntrospectionPanel({ filePath, studyId, onClose }: IntrospectionPanelProps) { const [result, setResult] = useState(null); const [isLoading, setIsLoading] = useState(false); const [error, setError] = useState(null); const [expandedSections, setExpandedSections] = useState>( new Set(['expressions', 'extractors']) ); const [searchTerm, setSearchTerm] = useState(''); // Baseline run state const [isRunningBaseline, setIsRunningBaseline] = useState(false); const [baselineResult, setBaselineResult] = useState(null); const { addNode, nodes } = useCanvasStore(); const runIntrospection = useCallback(async () => { if (!filePath) return; setIsLoading(true); setError(null); try { let res; // If we have a studyId, use the study-aware introspection endpoint if (studyId) { // Don't encode studyId - it may contain slashes for nested paths (e.g., M1_Mirror/study_name) 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(); // Handle different response formats setResult(data.introspection || data); } catch (e) { const msg = e instanceof Error ? e.message : 'Failed to introspect model'; setError(msg); console.error('Introspection error:', e); } finally { setIsLoading(false); } }, [filePath, studyId]); useEffect(() => { runIntrospection(); }, [runIntrospection]); // Run baseline FEA simulation const runBaseline = useCallback(async () => { if (!studyId) { setError('Study ID required to run baseline'); return; } setIsRunningBaseline(true); setBaselineResult(null); setError(null); try { const res = await fetch(`/api/optimization/studies/${studyId}/nx/run-baseline`, { method: 'POST', }); const data = await res.json(); setBaselineResult(data); if (!data.success && data.error) { setError(`Baseline run failed: ${data.error}`); } } catch (e) { const msg = e instanceof Error ? e.message : 'Failed to run baseline'; setError(msg); console.error('Baseline run error:', e); } finally { setIsRunningBaseline(false); } }, [studyId]); const toggleSection = (section: string) => { setExpandedSections((prev) => { const next = new Set(prev); if (next.has(section)) next.delete(section); else next.add(section); return next; }); }; const addExpressionAsDesignVar = (expr: Expression) => { // Find a good position (left of model node) const modelNode = nodes.find((n) => n.data.type === 'model'); const existingDvars = nodes.filter((n) => n.data.type === 'designVar'); const position = { x: (modelNode?.position.x || 300) - 250, y: (modelNode?.position.y || 100) + existingDvars.length * 100, }; // Calculate min/max based on value if not provided const minValue = expr.min ?? expr.value * 0.5; const maxValue = expr.max ?? expr.value * 1.5; addNode('designVar', position, { label: expr.name, expressionName: expr.name, minValue, maxValue, unit: expr.unit, configured: true, }); }; const addExtractorNode = (extractor: Extractor) => { // Find a good position (right of solver node) const solverNode = nodes.find((n) => n.data.type === 'solver'); const existingExtractors = nodes.filter((n) => n.data.type === 'extractor'); const position = { x: (solverNode?.position.x || 400) + 200, y: (solverNode?.position.y || 100) + existingExtractors.length * 100, }; addNode('extractor', position, { label: extractor.name, extractorId: extractor.id, extractorName: extractor.name, configured: true, }); }; // Handle both array format (old) and object format (new API) const allExpressions: Expression[] = useMemo(() => { if (!result?.expressions) return []; // Check if expressions is an array (old format) or object (new format) if (Array.isArray(result.expressions)) { return result.expressions; } // New format: { user: [...], internal: [...] } const exprObj = result.expressions as ExpressionsResult; return [...(exprObj.user || []), ...(exprObj.internal || [])]; }, [result?.expressions]); const filteredExpressions = allExpressions.filter((e) => e.name.toLowerCase().includes(searchTerm.toLowerCase()) ); return (
{/* Header */}
Model Introspection
{/* Search */}
setSearchTerm(e.target.value)} className="w-full px-3 py-1.5 bg-dark-800 border border-dark-600 rounded-lg text-sm text-white placeholder-dark-500 focus:outline-none focus:border-primary-500" />
{/* Run Baseline Button */} {studyId && (

Creates result files (.op2, .f06) for testing extractors

)} {/* Baseline Run Result */} {baselineResult && (
{baselineResult.success ? (
) : ( )} {baselineResult.message}
{baselineResult.elapsed_time && (

Completed in {baselineResult.elapsed_time.toFixed(1)}s

)} {baselineResult.result_files && (
{baselineResult.result_files.op2.length > 0 && (

OP2: {baselineResult.result_files.op2.join(', ')}

)} {baselineResult.result_files.f06.length > 0 && (

F06: {baselineResult.result_files.f06.join(', ')}

)} {baselineResult.result_files.bdf.length > 0 && (

BDF: {baselineResult.result_files.bdf.join(', ')}

)}
)} {baselineResult.error && (

{baselineResult.error}

)}
)} {/* Content */}
{isLoading ? (
Analyzing model...
) : error ? (
{error}
) : result ? (
{/* Solver Type */} {result.solver_type && (
Solver: {result.solver_type}
)} {/* Expressions Section */}
{expandedSections.has('expressions') && (
{filteredExpressions.length === 0 ? (

No expressions found

) : ( filteredExpressions.map((expr) => (

{expr.name}

{expr.value} {expr.units || expr.unit || ''} {expr.source === 'inferred' && ( (inferred) )}

)) )}
)}
{/* Mass Properties Section */} {result.mass_properties && (
{expandedSections.has('mass') && (
{result.mass_properties.mass_kg !== undefined && (
Mass {(result.mass_properties.mass_kg as number).toFixed(4)} kg
)} {result.mass_properties.volume_mm3 !== undefined && (result.mass_properties.volume_mm3 as number) > 0 && (
Volume {((result.mass_properties.volume_mm3 as number) / 1e9).toFixed(6)} m³
)} {result.mass_properties.surface_area_mm2 !== undefined && (result.mass_properties.surface_area_mm2 as number) > 0 && (
Surface Area {((result.mass_properties.surface_area_mm2 as number) / 1e6).toFixed(4)} m²
)} {Array.isArray(result.mass_properties.center_of_gravity_mm) && (
CoG (mm) [{(result.mass_properties.center_of_gravity_mm as number[]).map((v: number) => v.toFixed(1)).join(', ')}]
)} {typeof result.mass_properties.num_bodies === 'number' && (
Bodies {result.mass_properties.num_bodies}
)}
)}
)} {/* Linked Parts / File Dependencies Section */} {result.linked_parts && (
{expandedSections.has('linked') && (
{((result.linked_parts.loaded_parts as Array<{name: string, path: string, leaf_name: string}>) || []).map((part) => (

{part.name}

{part.leaf_name || part.path.split(/[/\\]/).pop()}

))} {((result.linked_parts.loaded_parts as Array) || []).length === 0 && (

No linked parts found

)}
)}
)} {/* Bodies Section */} {result.bodies && (result.bodies.counts as {total: number})?.total > 0 && (
{expandedSections.has('bodies') && (
Solid Bodies {(result.bodies.counts as {solid: number})?.solid || 0}
Sheet Bodies {(result.bodies.counts as {sheet: number})?.sheet || 0}
)}
)} {/* Units Section */} {result.units && (
{expandedSections.has('units') && (
{(result.units as {base_units?: Record})?.base_units && Object.entries((result.units as {base_units: Record}).base_units).map(([key, value]) => (
{key} {value}
)) }
)}
)} {/* File Dependencies Section (NX file chain) */} {result.file_dependencies && (
{expandedSections.has('file_deps') && (
{/* Show file tree structure */} {result.file_dependencies.root_sim && (
{/* Simulation files */} {result.file_dependencies.files.sim.map((sim) => (
{sim} (.sim)
{/* AFM files connected to this SIM */} {result.file_dependencies!.dependencies .filter(d => d.source === sim && d.type === 'sim_to_afm') .map(d => (
{d.target} (.afm)
{/* FEM files connected to this AFM */} {result.file_dependencies!.dependencies .filter(d2 => d2.source === d.target && d2.type === 'afm_to_fem') .map(d2 => (
{d2.target} (.fem)
{/* Idealized parts connected to this FEM */} {result.file_dependencies!.dependencies .filter(d3 => d3.source === d2.target && d3.type === 'fem_to_idealized') .map(d3 => (
{d3.target} (_i.prt)
{/* Geometry parts */} {result.file_dependencies!.dependencies .filter(d4 => d4.source === d3.target && d4.type === 'idealized_to_prt') .map(d4 => (
{d4.target} (.prt)
)) }
)) }
)) }
)) } {/* Direct FEM connections (no AFM) */} {result.file_dependencies!.dependencies .filter(d => d.source === sim && d.type === 'sim_to_fem') .map(d => (
{d.target} (.fem)
)) }
))} {/* Show any orphan PRT files not in the tree */} {result.file_dependencies.files.prt.filter(prt => !result.file_dependencies!.dependencies.some(d => d.target === prt) ).length > 0 && (
Additional geometry files: {result.file_dependencies.files.prt .filter(prt => !result.file_dependencies!.dependencies.some(d => d.target === prt)) .map(prt => (
{prt}
)) }
)}
)} {!result.file_dependencies.root_sim && (

No simulation file found

)}
)}
)} {/* Extractors Section - only show if available */} {(result.extractors_available?.length ?? 0) > 0 && (
{expandedSections.has('extractors') && (
{(result.extractors_available || []).map((ext) => (

{ext.name}

{ext.id} {ext.description && ` - ${ext.description}`}

))}
)}
)} {/* Dependent Files */} {(result.dependent_files?.length ?? 0) > 0 && (
{expandedSections.has('files') && (
{(result.dependent_files || []).map((file) => (

{file.name}

{file.type}

))}
)}
)} {/* Warnings */} {(result.warnings?.length ?? 0) > 0 && (

Warnings

{(result.warnings || []).map((w, i) => (

{w}

))}
)}
) : (
Select a model to introspect
)}
); }