/** * ExpressionList - Display discovered expressions with selection capability * * Shows expressions from NX introspection, allowing users to: * - View all discovered expressions * - See which are design variable candidates (auto-detected) * - Select/deselect expressions to use as design variables * - View expression values and units */ import React, { useState } from 'react'; import { Check, Search, AlertTriangle, Sparkles, Info, Variable, } from 'lucide-react'; import { ExpressionInfo } from '../../types/intake'; interface ExpressionListProps { /** Expression data from introspection */ expressions: ExpressionInfo[]; /** Mass from introspection (kg) */ massKg?: number | null; /** Currently selected expressions (to become DVs) */ selectedExpressions: string[]; /** Callback when selection changes */ onSelectionChange: (selected: string[]) => void; /** Whether in read-only mode */ readOnly?: boolean; /** Compact display mode */ compact?: boolean; } export const ExpressionList: React.FC = ({ expressions, massKg, selectedExpressions, onSelectionChange, readOnly = false, compact = false, }) => { const [filter, setFilter] = useState(''); const [showCandidatesOnly, setShowCandidatesOnly] = useState(true); // Filter expressions based on search and candidate toggle const filteredExpressions = expressions.filter((expr) => { const matchesSearch = filter === '' || expr.name.toLowerCase().includes(filter.toLowerCase()); const matchesCandidate = !showCandidatesOnly || expr.is_candidate; return matchesSearch && matchesCandidate; }); // Sort: candidates first, then by confidence, then alphabetically const sortedExpressions = [...filteredExpressions].sort((a, b) => { if (a.is_candidate !== b.is_candidate) { return a.is_candidate ? -1 : 1; } if (a.confidence !== b.confidence) { return b.confidence - a.confidence; } return a.name.localeCompare(b.name); }); const toggleExpression = (name: string) => { if (readOnly) return; if (selectedExpressions.includes(name)) { onSelectionChange(selectedExpressions.filter(n => n !== name)); } else { onSelectionChange([...selectedExpressions, name]); } }; const selectAllCandidates = () => { const candidateNames = expressions .filter(e => e.is_candidate) .map(e => e.name); onSelectionChange(candidateNames); }; const clearSelection = () => { onSelectionChange([]); }; const candidateCount = expressions.filter(e => e.is_candidate).length; if (expressions.length === 0) { return (
No expressions found. Run introspection to discover model parameters.
); } return (
{/* Header with stats */}
Discovered Expressions
{expressions.length} total, {candidateCount} candidates {massKg && ( Mass: {massKg.toFixed(3)} kg )}
{!readOnly && selectedExpressions.length > 0 && ( {selectedExpressions.length} selected )}
{/* Controls */} {!compact && (
{/* Search */}
setFilter(e.target.value)} className="w-full pl-8 pr-3 py-1.5 text-sm rounded-lg bg-dark-700 border border-dark-600 text-white placeholder-dark-500 focus:border-primary-500/50 focus:outline-none" />
{/* Show candidates only toggle */} {/* Quick actions */} {!readOnly && (
)}
)} {/* Expression list */}
{!readOnly && ( )} {sortedExpressions.map((expr) => { const isSelected = selectedExpressions.includes(expr.name); return ( toggleExpression(expr.name)} className={` ${readOnly ? '' : 'cursor-pointer hover:bg-dark-700/50'} ${isSelected ? 'bg-primary-500/10' : ''} transition-colors `} > {!readOnly && ( )} ); })}
Name Value Units Candidate
{isSelected && }
{expr.name} {expr.formula && ( )}
{expr.value !== null ? expr.value.toFixed(3) : '-'} {expr.units || '-'} {expr.is_candidate ? ( {Math.round(expr.confidence * 100)}% ) : ( - )}
{sortedExpressions.length === 0 && (
No expressions match your filter
)}
{/* Help text */} {!readOnly && !compact && (

Select expressions to use as design variables. Candidates (marked with %) are automatically identified based on naming patterns and units.

)}
); }; export default ExpressionList;