Spaces:
Running
Running
| import { useMemo, useState } from 'react' | |
| import { MODELS, displayAuroc, featureSetLabel, type RealModel } from '../data/models' | |
| interface MultiModelPickerProps { | |
| selected: Set<string> // model codes | |
| onChange: (next: Set<string>) => void | |
| } | |
| interface Group { | |
| key: string | |
| species: string | |
| models: RealModel[] | |
| } | |
| /** Checkbox multi-select of detectability models, grouped by species (mirrors the single-select | |
| * ModelPicker accordion). Used by the Protein Digestibility tab to screen across several models. */ | |
| export default function MultiModelPicker({ selected, onChange }: MultiModelPickerProps) { | |
| const groups = useMemo<Group[]>(() => { | |
| const byKey = new Map<string, Group>() | |
| for (const m of MODELS) { | |
| if (!byKey.has(m.speciesKey)) | |
| byKey.set(m.speciesKey, { key: m.speciesKey, species: m.species, models: [] }) | |
| byKey.get(m.speciesKey)!.models.push(m) | |
| } | |
| return [...byKey.values()] | |
| }, []) | |
| const [openGroups, setOpenGroups] = useState<Set<string>>(new Set()) | |
| const toggleGroup = (k: string) => | |
| setOpenGroups((prev) => { | |
| const next = new Set(prev) | |
| next.has(k) ? next.delete(k) : next.add(k) | |
| return next | |
| }) | |
| function toggle(code: string) { | |
| const next = new Set(selected) | |
| next.has(code) ? next.delete(code) : next.add(code) | |
| onChange(next) | |
| } | |
| return ( | |
| <div className="field"> | |
| <div | |
| className="field-label" | |
| style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }} | |
| > | |
| <span> | |
| Detectability models{' '} | |
| <span className="field-hint"> | |
| {selected.size} selected · combined set needs all of them | |
| </span> | |
| </span> | |
| {selected.size > 0 && ( | |
| <button type="button" className="btn btn-ghost btn-sm" onClick={() => onChange(new Set())}> | |
| Clear | |
| </button> | |
| )} | |
| </div> | |
| <div className="species-accordion"> | |
| {groups.map((g) => { | |
| const isOpen = openGroups.has(g.key) | |
| const nSel = g.models.filter((m) => selected.has(m.code)).length | |
| return ( | |
| <div key={g.key} className={'species-group' + (isOpen ? ' open' : '')}> | |
| <button type="button" className="species-head" onClick={() => toggleGroup(g.key)}> | |
| <span className={'species-caret caret' + (isOpen ? ' open' : '')}>▶</span> | |
| <span className="species-name">{g.species.split('(')[0].trim()}</span> | |
| <span className="species-count"> | |
| {g.models.length} model{g.models.length > 1 ? 's' : ''} | |
| </span> | |
| {nSel > 0 && !isOpen && ( | |
| <span className="badge badge-good species-sel">{nSel} selected</span> | |
| )} | |
| </button> | |
| {isOpen && ( | |
| <div className="option-list species-body"> | |
| {g.models.map((m) => { | |
| const on = selected.has(m.code) | |
| return ( | |
| <label key={m.code} className={'option' + (on ? ' selected' : '')}> | |
| <input type="checkbox" checked={on} onChange={() => toggle(m.code)} /> | |
| <span className="option-body"> | |
| <span | |
| className="option-title" | |
| style={{ display: 'flex', gap: 8, alignItems: 'center' }} | |
| > | |
| <span className="mono">{m.code}</span> · {m.enzyme} | |
| <span className="badge badge-accent">AUROC {displayAuroc(m)}</span> | |
| </span> | |
| <span className="option-desc"> | |
| {featureSetLabel(m.bestFeatureSet)} · n={m.nPos.toLocaleString()} | |
| {m.flags.length > 0 && ' · caveat, see Prediction tab'} | |
| </span> | |
| </span> | |
| </label> | |
| ) | |
| })} | |
| </div> | |
| )} | |
| </div> | |
| ) | |
| })} | |
| </div> | |
| </div> | |
| ) | |
| } | |