fix(canvas): Fix IntrospectionPanel to handle new NX introspection API response format

- Handle expressions as object with user/internal arrays (new format) or direct array (old)
- Add useMemo for expression filtering
- Make extractors_available, dependent_files, warnings optional with safe access
- Support both 'unit' and 'units' field names
This commit is contained in:
2026-01-20 14:26:20 -05:00
parent 2f0f45de86
commit ced79b8d39

View File

@@ -2,7 +2,7 @@
* Introspection Panel - Shows discovered expressions and extractors from NX model * Introspection Panel - Shows discovered expressions and extractors from NX model
*/ */
import { useState, useEffect, useCallback } from 'react'; import { useState, useEffect, useCallback, useMemo } from 'react';
import { import {
X, X,
Search, Search,
@@ -27,9 +27,11 @@ interface IntrospectionPanelProps {
interface Expression { interface Expression {
name: string; name: string;
value: number; value: number;
rhs?: string;
min?: number; min?: number;
max?: number; max?: number;
unit: string; unit?: string;
units?: string; // API returns 'units' not 'unit'
type: string; type: string;
source?: string; source?: string;
} }
@@ -47,14 +49,34 @@ interface DependentFile {
name: string; name: string;
} }
// The API returns expressions in a nested structure
interface ExpressionsResult {
user: Expression[];
internal: Expression[];
total_count: number;
user_count: number;
}
interface IntrospectionResult { interface IntrospectionResult {
file_path: string; part_file?: string;
file_type: string; part_path?: string;
expressions: Expression[]; file_path?: string;
solver_type: string | null; file_type?: string;
dependent_files: DependentFile[]; success?: boolean;
extractors_available: Extractor[]; error?: string | null;
warnings: string[]; // 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<string, unknown>;
materials?: Record<string, unknown>;
bodies?: Record<string, unknown>;
attributes?: Array<{ title: string; value: string }>;
units?: Record<string, unknown>;
linked_parts?: Record<string, unknown>;
} }
export function IntrospectionPanel({ filePath, studyId, onClose }: IntrospectionPanelProps) { export function IntrospectionPanel({ filePath, studyId, onClose }: IntrospectionPanelProps) {
@@ -161,10 +183,23 @@ export function IntrospectionPanel({ filePath, studyId, onClose }: Introspection
}); });
}; };
const filteredExpressions = // Handle both array format (old) and object format (new API)
result?.expressions.filter((e) => const allExpressions: Expression[] = useMemo(() => {
e.name.toLowerCase().includes(searchTerm.toLowerCase()) 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 ( return (
<div className="bg-dark-850 border border-dark-700 rounded-xl w-80 max-h-[70vh] flex flex-col shadow-xl"> <div className="bg-dark-850 border border-dark-700 rounded-xl w-80 max-h-[70vh] flex flex-col shadow-xl">
@@ -260,7 +295,7 @@ export function IntrospectionPanel({ filePath, studyId, onClose }: Introspection
<div className="flex-1 min-w-0"> <div className="flex-1 min-w-0">
<p className="text-sm text-white truncate">{expr.name}</p> <p className="text-sm text-white truncate">{expr.name}</p>
<p className="text-xs text-dark-500"> <p className="text-xs text-dark-500">
{expr.value} {expr.unit} {expr.value} {expr.units || expr.unit || ''}
{expr.source === 'inferred' && ( {expr.source === 'inferred' && (
<span className="ml-1 text-amber-500">(inferred)</span> <span className="ml-1 text-amber-500">(inferred)</span>
)} )}
@@ -281,7 +316,8 @@ export function IntrospectionPanel({ filePath, studyId, onClose }: Introspection
)} )}
</div> </div>
{/* Extractors Section */} {/* Extractors Section - only show if available */}
{(result.extractors_available?.length ?? 0) > 0 && (
<div className="border border-dark-700 rounded-lg overflow-hidden"> <div className="border border-dark-700 rounded-lg overflow-hidden">
<button <button
onClick={() => toggleSection('extractors')} onClick={() => toggleSection('extractors')}
@@ -290,7 +326,7 @@ export function IntrospectionPanel({ filePath, studyId, onClose }: Introspection
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<FlaskConical size={14} className="text-cyan-400" /> <FlaskConical size={14} className="text-cyan-400" />
<span className="text-sm font-medium text-white"> <span className="text-sm font-medium text-white">
Available Extractors ({result.extractors_available.length}) Available Extractors ({result.extractors_available?.length || 0})
</span> </span>
</div> </div>
{expandedSections.has('extractors') ? ( {expandedSections.has('extractors') ? (
@@ -302,7 +338,7 @@ export function IntrospectionPanel({ filePath, studyId, onClose }: Introspection
{expandedSections.has('extractors') && ( {expandedSections.has('extractors') && (
<div className="p-2 space-y-1 max-h-48 overflow-y-auto"> <div className="p-2 space-y-1 max-h-48 overflow-y-auto">
{result.extractors_available.map((ext) => ( {(result.extractors_available || []).map((ext) => (
<div <div
key={ext.id} key={ext.id}
className="flex items-center justify-between p-2 bg-dark-850 rounded hover:bg-dark-750 group transition-colors" className="flex items-center justify-between p-2 bg-dark-850 rounded hover:bg-dark-750 group transition-colors"
@@ -327,9 +363,10 @@ export function IntrospectionPanel({ filePath, studyId, onClose }: Introspection
</div> </div>
)} )}
</div> </div>
)}
{/* Dependent Files */} {/* Dependent Files */}
{result.dependent_files.length > 0 && ( {(result.dependent_files?.length ?? 0) > 0 && (
<div className="border border-dark-700 rounded-lg overflow-hidden"> <div className="border border-dark-700 rounded-lg overflow-hidden">
<button <button
onClick={() => toggleSection('files')} onClick={() => toggleSection('files')}
@@ -338,7 +375,7 @@ export function IntrospectionPanel({ filePath, studyId, onClose }: Introspection
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<FileBox size={14} className="text-amber-400" /> <FileBox size={14} className="text-amber-400" />
<span className="text-sm font-medium text-white"> <span className="text-sm font-medium text-white">
Dependent Files ({result.dependent_files.length}) Dependent Files ({result.dependent_files?.length || 0})
</span> </span>
</div> </div>
{expandedSections.has('files') ? ( {expandedSections.has('files') ? (
@@ -350,7 +387,7 @@ export function IntrospectionPanel({ filePath, studyId, onClose }: Introspection
{expandedSections.has('files') && ( {expandedSections.has('files') && (
<div className="p-2 space-y-1 max-h-32 overflow-y-auto"> <div className="p-2 space-y-1 max-h-32 overflow-y-auto">
{result.dependent_files.map((file) => ( {(result.dependent_files || []).map((file) => (
<div <div
key={file.path} key={file.path}
className="flex items-center gap-2 p-2 bg-dark-850 rounded" className="flex items-center gap-2 p-2 bg-dark-850 rounded"
@@ -368,13 +405,13 @@ export function IntrospectionPanel({ filePath, studyId, onClose }: Introspection
)} )}
{/* Warnings */} {/* Warnings */}
{result.warnings.length > 0 && ( {(result.warnings?.length ?? 0) > 0 && (
<div className="p-2 bg-amber-500/10 border border-amber-500/30 rounded-lg"> <div className="p-2 bg-amber-500/10 border border-amber-500/30 rounded-lg">
<div className="flex items-center gap-1.5 mb-1"> <div className="flex items-center gap-1.5 mb-1">
<AlertTriangle size={12} className="text-amber-400" /> <AlertTriangle size={12} className="text-amber-400" />
<p className="text-xs text-amber-400 font-medium">Warnings</p> <p className="text-xs text-amber-400 font-medium">Warnings</p>
</div> </div>
{result.warnings.map((w, i) => ( {(result.warnings || []).map((w, i) => (
<p key={i} className="text-xs text-amber-300"> <p key={i} className="text-xs text-amber-300">
{w} {w}
</p> </p>