Spaces:
Running
Running
| import { useState } from 'react' | |
| import Card from '../components/Card' | |
| import Button from '../components/Button' | |
| import SequenceInput from '../components/SequenceInput' | |
| import ModelPicker from '../components/ModelPicker' | |
| import ConditionMatcher from '../components/ConditionMatcher' | |
| import ResultsTable, { type Column } from '../components/ResultsTable' | |
| import { findModel } from '../data/models' | |
| import { classLabel } from '../data/screenMeta' | |
| import type { Conditions } from '../lib/rankModels' | |
| import { predictDetectability, screenBioactivityPeptides } from '../lib/api' | |
| import { SAMPLE_PEPTIDES, type DetectabilityRow } from '../data/mockData' | |
| const COLUMNS: Column<DetectabilityRow>[] = [ | |
| { | |
| key: 'peptide', | |
| header: 'Peptide', | |
| render: (r) => <span className="mono">{r.peptide}</span>, | |
| }, | |
| { key: 'length', header: 'Length', sortable: true, align: 'right' }, | |
| { | |
| key: 'score', | |
| header: 'Detectability score', | |
| sortable: true, | |
| align: 'right', | |
| render: (r) => r.score.toFixed(3), | |
| }, | |
| { | |
| key: 'prediction', | |
| header: 'Prediction', | |
| sortable: true, | |
| render: (r) => | |
| r.prediction === 'Detectable' ? ( | |
| <span className="badge badge-good">Detectable</span> | |
| ) : ( | |
| <span className="badge badge-muted">Not detectable</span> | |
| ), | |
| }, | |
| { | |
| key: 'bioactivities', | |
| header: 'Known bioactivity', | |
| render: (r) => { | |
| const list = (r.bioactivities ?? '').split(';').filter(Boolean) | |
| return list.length ? ( | |
| <span className="badge badge-blue" title={list.map(classLabel).join('; ')}> | |
| {classLabel(list[0])} | |
| {list.length > 1 ? ` +${list.length - 1}` : ''} | |
| </span> | |
| ) : ( | |
| <span className="muted">n/a</span> | |
| ) | |
| }, | |
| }, | |
| { | |
| key: 'inTraining', | |
| header: 'In training', | |
| sortable: true, | |
| render: (r) => | |
| r.inTraining === 'positive' ? ( | |
| <span className="badge badge-warn" title="Detected (positive) example in this model's training set. The score may reflect memorisation."> | |
| Train⁺ | |
| </span> | |
| ) : r.inTraining === 'negative' ? ( | |
| <span className="badge badge-warn" title="Undetected (negative) example in this model's training set. The score may reflect memorisation."> | |
| Train⁻ | |
| </span> | |
| ) : ( | |
| <span className="muted">held out</span> | |
| ), | |
| }, | |
| { key: 'model', header: 'Model', sortable: true }, | |
| ] | |
| export default function DetectabilityPrediction() { | |
| const [input, setInput] = useState('') | |
| const [modelCode, setModelCode] = useState('H9') | |
| const [conditions, setConditions] = useState<Conditions>({}) | |
| const [pickMode, setPickMode] = useState<'match' | 'custom'>('match') | |
| const [rows, setRows] = useState<DetectabilityRow[] | null>(null) | |
| const [loading, setLoading] = useState(false) | |
| const [live, setLive] = useState(false) | |
| const canRun = input.trim().length > 0 && !loading | |
| const model = findModel(modelCode) | |
| async function run() { | |
| setLoading(true) | |
| try { | |
| // Predict detectability and screen known bioactivity in parallel, then join | |
| // by peptide so the output (and CSV) carries function alongside detectability. | |
| const [pred, bio] = await Promise.all([ | |
| predictDetectability(input, model), | |
| screenBioactivityPeptides(input), | |
| ]) | |
| const bioMap = new Map( | |
| bio.rows.map((b) => [b.peptide.toUpperCase(), b.bioactivities]), | |
| ) | |
| setRows( | |
| pred.rows.map((r) => ({ | |
| ...r, | |
| bioactivities: bioMap.get(r.peptide.toUpperCase()) ?? '', | |
| })), | |
| ) | |
| setLive(pred.live) | |
| } finally { | |
| setLoading(false) | |
| } | |
| } | |
| const detectable = rows?.filter((r) => r.prediction === 'Detectable').length ?? 0 | |
| return ( | |
| <main className="page"> | |
| <div className="page-header"> | |
| <h1>Detectability Prediction</h1> | |
| <p className="lead"> | |
| Paste peptides and pick a model. Describe your conditions to auto-rank the | |
| best fit, or choose one yourself. Peptides seen during training are flagged, and | |
| each peptide is also screened for known bioactivity. | |
| </p> | |
| </div> | |
| <div className="grid-2"> | |
| <Card title="Peptides"> | |
| <SequenceInput | |
| label="Peptide sequences" | |
| value={input} | |
| onChange={setInput} | |
| onLoadExample={() => setInput(SAMPLE_PEPTIDES)} | |
| placeholder={'PEPTIDE\nPEPTIDE\n\nor comma-separated / FASTA'} | |
| /> | |
| </Card> | |
| <Card title="Model"> | |
| <div className="segmented" style={{ marginBottom: 14, width: '100%' }}> | |
| <button | |
| className={pickMode === 'match' ? 'active' : ''} | |
| aria-pressed={pickMode === 'match'} | |
| onClick={() => setPickMode('match')} | |
| style={{ flex: 1 }} | |
| > | |
| Match my conditions | |
| </button> | |
| <button | |
| className={pickMode === 'custom' ? 'active' : ''} | |
| aria-pressed={pickMode === 'custom'} | |
| onClick={() => setPickMode('custom')} | |
| style={{ flex: 1 }} | |
| > | |
| Custom select a model | |
| </button> | |
| </div> | |
| {pickMode === 'match' ? ( | |
| <ConditionMatcher | |
| conditions={conditions} | |
| onChange={setConditions} | |
| onPick={setModelCode} | |
| selected={modelCode} | |
| /> | |
| ) : ( | |
| <ModelPicker selected={modelCode} onChange={setModelCode} /> | |
| )} | |
| <div className="selected-model"> | |
| Selected model: <span className="mono">{model.code}</span> · {model.species.split('(')[0].trim()} · {model.enzyme} | |
| </div> | |
| <div style={{ marginTop: 12 }}> | |
| <Button onClick={run} disabled={!canRun}> | |
| {loading ? 'Predicting…' : 'Predict detectability'} | |
| </Button> | |
| {!canRun && ( | |
| <p | |
| className="muted" | |
| style={{ fontSize: 'var(--text-sm)', marginTop: 8, marginBottom: 0 }} | |
| > | |
| Add at least one peptide. | |
| </p> | |
| )} | |
| </div> | |
| </Card> | |
| </div> | |
| <Card | |
| title="Detectability predictions" | |
| actions={ | |
| rows != null && rows.length > 0 ? ( | |
| <span | |
| className={`badge ${live ? 'badge-good' : 'badge-muted'}`} | |
| title={ | |
| live | |
| ? 'Scores computed by the model (server-side inference).' | |
| : 'Backend unavailable. Showing illustrative offline scores.' | |
| } | |
| > | |
| {live ? 'Live inference' : 'Offline preview'} | |
| </span> | |
| ) : undefined | |
| } | |
| > | |
| {loading ? ( | |
| <div className="empty-state"> | |
| Scoring peptides… computing features and running the model. | |
| </div> | |
| ) : rows == null ? ( | |
| <div className="empty-state"> | |
| Run a prediction to see per-peptide detectability scores. | |
| </div> | |
| ) : rows.length === 0 ? ( | |
| <div className="empty-state">No peptides parsed from the input.</div> | |
| ) : ( | |
| <> | |
| <div className="stat-row"> | |
| <div className="stat"> | |
| <div className="stat-value">{rows.length}</div> | |
| <div className="stat-label">Peptides scored</div> | |
| </div> | |
| <div className="stat"> | |
| <div className="stat-value">{detectable}</div> | |
| <div className="stat-label">Predicted detectable</div> | |
| </div> | |
| <div className="stat"> | |
| <div className="stat-value" style={{ fontSize: '1.05rem' }}> | |
| {model.code} · {model.enzyme} | |
| </div> | |
| <div className="stat-label">Model used ({model.species})</div> | |
| </div> | |
| </div> | |
| <ResultsTable | |
| rows={rows} | |
| columns={COLUMNS} | |
| csvFilename="hydropd_detectability.csv" | |
| /> | |
| </> | |
| )} | |
| </Card> | |
| </main> | |
| ) | |
| } | |