/** * Introspection Panel - Shows discovered expressions and extractors from NX model */ import { useState, useEffect, useCallback } from 'react'; import { X, Search, RefreshCw, Plus, ChevronDown, ChevronRight, FileBox, Cpu, FlaskConical, SlidersHorizontal, AlertTriangle, } from 'lucide-react'; import { useCanvasStore } from '../../../hooks/useCanvasStore'; interface IntrospectionPanelProps { filePath: string; studyId?: string; onClose: () => void; } interface Expression { name: string; value: number; min?: number; max?: number; unit: string; type: string; source?: string; } interface Extractor { id: string; name: string; description?: string; always?: boolean; } interface DependentFile { path: string; type: string; name: string; } interface IntrospectionResult { file_path: string; file_type: string; expressions: Expression[]; solver_type: string | null; dependent_files: DependentFile[]; extractors_available: Extractor[]; warnings: 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(''); 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]); 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, }); }; const filteredExpressions = result?.expressions.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" />
{/* 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.unit} {expr.source === 'inferred' && ( (inferred) )}

)) )}
)}
{/* Extractors Section */}
{expandedSections.has('extractors') && (
{result.extractors_available.map((ext) => (

{ext.name}

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

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

{file.name}

{file.type}

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

Warnings

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

{w}

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