hydropd / website /src /components /MultiModelPicker.tsx
rikardsaqe's picture
Upload folder using huggingface_hub
2273160 verified
Raw
History Blame Contribute Delete
4.15 kB
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>
)
}