File size: 4,151 Bytes
2273160
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
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>
  )
}