feat: Add sub-part introspection and existing FEA results UI

Backend:
- GET /nx/parts - List all .prt files in model directory
- GET /nx/introspect/{part_name} - Introspect a specific part file
  (e.g., M1_Blank.prt instead of just the assembly)
- Each part gets its own cache file (_introspection_{stem}.json)

Frontend IntrospectionPanel:
- Add 'FEA Results' section showing existing OP2/F06 sources
- Green checkmark when results exist, shows recommended source
- Expand file_deps and fea_results sections by default
- Add CheckCircle2 and Database icons

This allows introspecting component parts that contain the actual
design variable expressions (e.g., M1_Blank has 56 expressions
while the assembly ASSY_M1 only has 5).
This commit is contained in:
2026-01-20 20:59:04 -05:00
parent 4749944a48
commit 0c252e3a65
2 changed files with 242 additions and 1 deletions

View File

@@ -21,6 +21,8 @@ import {
Settings2,
GitBranch,
File,
Database,
CheckCircle2,
} from 'lucide-react';
import { useCanvasStore } from '../../../hooks/useCanvasStore';
@@ -101,6 +103,22 @@ interface IntrospectionResult {
units?: Record<string, unknown>;
linked_parts?: Record<string, unknown>;
file_dependencies?: FileDependencies;
existing_fea_results?: {
has_results: boolean;
sources: Array<{
location: string;
path: string;
op2: string[];
f06: string[];
bdf: string[];
timestamp?: number;
}>;
recommended?: {
location: string;
path: string;
op2: string[];
};
};
}
// Baseline run result interface
@@ -125,7 +143,7 @@ export function IntrospectionPanel({ filePath, studyId, onClose }: Introspection
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const [expandedSections, setExpandedSections] = useState<Set<string>>(
new Set(['expressions', 'extractors'])
new Set(['expressions', 'extractors', 'file_deps', 'fea_results'])
);
const [searchTerm, setSearchTerm] = useState('');
@@ -783,6 +801,77 @@ export function IntrospectionPanel({ filePath, studyId, onClose }: Introspection
</div>
)}
{/* Existing FEA Results Section */}
{result.existing_fea_results && (
<div className="border border-dark-700 rounded-lg overflow-hidden">
<button
onClick={() => toggleSection('fea_results')}
className="w-full flex items-center justify-between px-3 py-2 bg-dark-800 hover:bg-dark-750 transition-colors"
>
<div className="flex items-center gap-2">
{result.existing_fea_results.has_results ? (
<CheckCircle2 size={14} className="text-green-400" />
) : (
<Database size={14} className="text-dark-500" />
)}
<span className="text-sm font-medium text-white">
FEA Results {result.existing_fea_results.has_results ?
`(${result.existing_fea_results.sources?.length || 0} sources)` :
'(none)'}
</span>
</div>
{expandedSections.has('fea_results') ? (
<ChevronDown size={14} className="text-dark-400" />
) : (
<ChevronRight size={14} className="text-dark-400" />
)}
</button>
{expandedSections.has('fea_results') && (
<div className="p-2 space-y-2">
{result.existing_fea_results.has_results ? (
<>
<p className="text-xs text-green-400 mb-2">
Existing results found - no solve needed for extraction
</p>
{result.existing_fea_results.sources?.map((source, idx) => (
<div
key={idx}
className={`p-2 rounded text-xs ${
source.location === result.existing_fea_results?.recommended?.location
? 'bg-green-500/10 border border-green-500/30'
: 'bg-dark-850'
}`}
>
<div className="flex items-center justify-between mb-1">
<span className="text-white font-medium">{source.location}</span>
{source.location === result.existing_fea_results?.recommended?.location && (
<span className="text-[10px] text-green-400 bg-green-500/20 px-1 rounded">
recommended
</span>
)}
</div>
<div className="text-dark-400 space-y-0.5">
{source.op2?.length > 0 && (
<p>OP2: {source.op2.join(', ')}</p>
)}
{source.f06?.length > 0 && (
<p>F06: {source.f06.join(', ')}</p>
)}
</div>
</div>
))}
</>
) : (
<p className="text-xs text-dark-500 text-center py-2">
No FEA results found. Run baseline to generate results.
</p>
)}
</div>
)}
</div>
)}
{/* Extractors Section - only show if available */}
{(result.extractors_available?.length ?? 0) > 0 && (
<div className="border border-dark-700 rounded-lg overflow-hidden">