feat: Add part selector dropdown to IntrospectionPanel
- Fetch available parts from /nx/parts on panel mount - Dropdown to select which part to introspect (default = assembly) - Hides idealized parts (*_i.prt) from dropdown - Shows part size in dropdown (KB or MB) - Header shows selected part name in primary color - Refresh button respects current part selection - Auto-introspects when part selection changes
This commit is contained in:
@@ -138,6 +138,15 @@ interface BaselineRunResult {
|
|||||||
message: string;
|
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) {
|
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);
|
||||||
@@ -147,14 +156,37 @@ export function IntrospectionPanel({ filePath, studyId, onClose }: Introspection
|
|||||||
);
|
);
|
||||||
const [searchTerm, setSearchTerm] = useState('');
|
const [searchTerm, setSearchTerm] = useState('');
|
||||||
|
|
||||||
|
// Part selection state
|
||||||
|
const [availableParts, setAvailableParts] = useState<PartInfo[]>([]);
|
||||||
|
const [selectedPart, setSelectedPart] = useState<string>(''); // empty = default/assembly
|
||||||
|
const [isLoadingParts, setIsLoadingParts] = useState(false);
|
||||||
|
|
||||||
// Baseline run state
|
// Baseline run state
|
||||||
const [isRunningBaseline, setIsRunningBaseline] = useState(false);
|
const [isRunningBaseline, setIsRunningBaseline] = useState(false);
|
||||||
const [baselineResult, setBaselineResult] = useState<BaselineRunResult | null>(null);
|
const [baselineResult, setBaselineResult] = useState<BaselineRunResult | null>(null);
|
||||||
|
|
||||||
const { addNode, nodes } = useCanvasStore();
|
const { addNode, nodes } = useCanvasStore();
|
||||||
|
|
||||||
const runIntrospection = useCallback(async () => {
|
// Fetch available parts when studyId changes
|
||||||
if (!filePath) return;
|
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);
|
setIsLoading(true);
|
||||||
setError(null);
|
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 we have a studyId, use the study-aware introspection endpoint
|
||||||
if (studyId) {
|
if (studyId) {
|
||||||
// Don't encode studyId - it may contain slashes for nested paths (e.g., M1_Mirror/study_name)
|
// Use specific part endpoint if a part is selected
|
||||||
res = await fetch(`/api/optimization/studies/${studyId}/nx/introspect`);
|
const endpoint = partName
|
||||||
|
? `/api/optimization/studies/${studyId}/nx/introspect/${encodeURIComponent(partName)}`
|
||||||
|
: `/api/optimization/studies/${studyId}/nx/introspect`;
|
||||||
|
res = await fetch(endpoint);
|
||||||
} else {
|
} else {
|
||||||
// Fallback to direct path introspection
|
// Fallback to direct path introspection
|
||||||
res = await fetch('/api/nx/introspect', {
|
res = await fetch('/api/nx/introspect', {
|
||||||
@@ -191,9 +226,21 @@ export function IntrospectionPanel({ filePath, studyId, onClose }: Introspection
|
|||||||
}
|
}
|
||||||
}, [filePath, studyId]);
|
}, [filePath, studyId]);
|
||||||
|
|
||||||
|
// Fetch parts list on mount
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
runIntrospection();
|
fetchAvailableParts();
|
||||||
}, [runIntrospection]);
|
}, [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<HTMLSelectElement>) => {
|
||||||
|
const newPart = e.target.value;
|
||||||
|
setSelectedPart(newPart);
|
||||||
|
};
|
||||||
|
|
||||||
// Run baseline FEA simulation
|
// Run baseline FEA simulation
|
||||||
const runBaseline = useCallback(async () => {
|
const runBaseline = useCallback(async () => {
|
||||||
@@ -301,11 +348,14 @@ export function IntrospectionPanel({ filePath, studyId, onClose }: Introspection
|
|||||||
<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">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<Search size={16} className="text-primary-400" />
|
<Search size={16} className="text-primary-400" />
|
||||||
<span className="font-medium text-white text-sm">Model Introspection</span>
|
<span className="font-medium text-white text-sm">
|
||||||
|
Model Introspection
|
||||||
|
{selectedPart && <span className="text-primary-400 ml-1">({selectedPart})</span>}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-1">
|
<div className="flex items-center gap-1">
|
||||||
<button
|
<button
|
||||||
onClick={runIntrospection}
|
onClick={() => runIntrospection(selectedPart || undefined)}
|
||||||
disabled={isLoading}
|
disabled={isLoading}
|
||||||
className="p-1.5 text-dark-400 hover:text-white hover:bg-dark-700 rounded transition-colors"
|
className="p-1.5 text-dark-400 hover:text-white hover:bg-dark-700 rounded transition-colors"
|
||||||
title="Refresh"
|
title="Refresh"
|
||||||
@@ -321,8 +371,37 @@ export function IntrospectionPanel({ filePath, studyId, onClose }: Introspection
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Search */}
|
{/* Part Selector + Search */}
|
||||||
<div className="px-4 py-2 border-b border-dark-700">
|
<div className="px-4 py-2 border-b border-dark-700 space-y-2">
|
||||||
|
{/* Part dropdown */}
|
||||||
|
{studyId && availableParts.length > 0 && (
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<label className="text-xs text-dark-400 whitespace-nowrap">Part:</label>
|
||||||
|
<select
|
||||||
|
value={selectedPart}
|
||||||
|
onChange={handlePartChange}
|
||||||
|
disabled={isLoading || isLoadingParts}
|
||||||
|
className="flex-1 px-2 py-1.5 bg-dark-800 border border-dark-600 rounded-lg
|
||||||
|
text-sm text-white focus:outline-none focus:border-primary-500
|
||||||
|
disabled:opacity-50"
|
||||||
|
>
|
||||||
|
<option value="">Default (Assembly)</option>
|
||||||
|
{availableParts
|
||||||
|
.filter(p => !p.is_idealized) // Hide idealized parts
|
||||||
|
.map(part => (
|
||||||
|
<option key={part.name} value={part.stem}>
|
||||||
|
{part.stem} ({part.size_kb > 1000 ? `${(part.size_kb/1024).toFixed(1)}MB` : `${part.size_kb}KB`})
|
||||||
|
</option>
|
||||||
|
))
|
||||||
|
}
|
||||||
|
</select>
|
||||||
|
{isLoadingParts && (
|
||||||
|
<RefreshCw size={12} className="animate-spin text-dark-400" />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Search input */}
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
placeholder="Filter expressions..."
|
placeholder="Filter expressions..."
|
||||||
|
|||||||
Reference in New Issue
Block a user