diff --git a/atomizer-dashboard/frontend/src/components/canvas/panels/IntrospectionPanel.tsx b/atomizer-dashboard/frontend/src/components/canvas/panels/IntrospectionPanel.tsx index 09c55a50..41896562 100644 --- a/atomizer-dashboard/frontend/src/components/canvas/panels/IntrospectionPanel.tsx +++ b/atomizer-dashboard/frontend/src/components/canvas/panels/IntrospectionPanel.tsx @@ -138,6 +138,15 @@ interface BaselineRunResult { message: string; } +// Part info from /nx/parts endpoint +interface PartInfo { + name: string; + stem: string; + is_idealized: boolean; + size_kb: number; + has_cache: boolean; +} + export function IntrospectionPanel({ filePath, studyId, onClose }: IntrospectionPanelProps) { const [result, setResult] = useState(null); const [isLoading, setIsLoading] = useState(false); @@ -147,14 +156,37 @@ export function IntrospectionPanel({ filePath, studyId, onClose }: Introspection ); const [searchTerm, setSearchTerm] = useState(''); + // Part selection state + const [availableParts, setAvailableParts] = useState([]); + const [selectedPart, setSelectedPart] = useState(''); // empty = default/assembly + const [isLoadingParts, setIsLoadingParts] = useState(false); + // Baseline run state const [isRunningBaseline, setIsRunningBaseline] = useState(false); const [baselineResult, setBaselineResult] = useState(null); const { addNode, nodes } = useCanvasStore(); - const runIntrospection = useCallback(async () => { - if (!filePath) return; + // Fetch available parts when studyId changes + const fetchAvailableParts = useCallback(async () => { + if (!studyId) return; + + setIsLoadingParts(true); + try { + const res = await fetch(`/api/optimization/studies/${studyId}/nx/parts`); + if (res.ok) { + const data = await res.json(); + setAvailableParts(data.parts || []); + } + } catch (e) { + console.error('Failed to fetch parts:', e); + } finally { + setIsLoadingParts(false); + } + }, [studyId]); + + const runIntrospection = useCallback(async (partName?: string) => { + if (!filePath && !studyId) return; setIsLoading(true); setError(null); @@ -163,8 +195,11 @@ export function IntrospectionPanel({ filePath, studyId, onClose }: Introspection // 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`); + // Use specific part endpoint if a part is selected + const endpoint = partName + ? `/api/optimization/studies/${studyId}/nx/introspect/${encodeURIComponent(partName)}` + : `/api/optimization/studies/${studyId}/nx/introspect`; + res = await fetch(endpoint); } else { // Fallback to direct path introspection res = await fetch('/api/nx/introspect', { @@ -191,9 +226,21 @@ export function IntrospectionPanel({ filePath, studyId, onClose }: Introspection } }, [filePath, studyId]); + // Fetch parts list on mount useEffect(() => { - runIntrospection(); - }, [runIntrospection]); + fetchAvailableParts(); + }, [fetchAvailableParts]); + + // Run introspection when component mounts or selected part changes + useEffect(() => { + runIntrospection(selectedPart || undefined); + }, [selectedPart]); // eslint-disable-line react-hooks/exhaustive-deps + + // Handle part selection change + const handlePartChange = (e: React.ChangeEvent) => { + const newPart = e.target.value; + setSelectedPart(newPart); + }; // Run baseline FEA simulation const runBaseline = useCallback(async () => { @@ -301,11 +348,14 @@ export function IntrospectionPanel({ filePath, studyId, onClose }: Introspection
- Model Introspection + + Model Introspection + {selectedPart && ({selectedPart})} +
- {/* Search */} -
+ {/* Part Selector + Search */} +
+ {/* Part dropdown */} + {studyId && availableParts.length > 0 && ( +
+ + + {isLoadingParts && ( + + )} +
+ )} + + {/* Search input */}