Spaces:
Running
Running
| // Backend client for Milestone 2 real inference. Falls back to the labeled | |
| // client-side mock (mockData.runDetectability) when the backend is unreachable, | |
| // so the static site still works offline / in a pure-demo deployment. | |
| import { parseSequences } from './parseSequences' | |
| import { | |
| runDetectability, | |
| runTraining, | |
| runBioactivityPeptides, | |
| runAllergenOrigin, | |
| type DetectabilityRow, | |
| type BioactivityRow, | |
| type AllergenRow, | |
| type TrainingConfig, | |
| type TrainingResult, | |
| } from '../data/mockData' | |
| import type { RealModel } from '../data/models' | |
| const API_BASE = import.meta.env.VITE_API_BASE ?? '/api' | |
| export interface PredictOutcome { | |
| rows: DetectabilityRow[] | |
| live: boolean // true = real backend inference; false = offline mock fallback | |
| calibrated: boolean // backend-reported; only meaningful when live | |
| } | |
| interface PredictResponse { | |
| model: string | |
| calibrated: boolean | |
| results: { | |
| peptide: string | |
| length: number | |
| score: number | |
| prediction: string | |
| inTraining?: 'positive' | 'negative' | null | |
| }[] | |
| flags: string[] | |
| } | |
| /** | |
| * Score peptides with a model. Tries the backend first; on any network/HTTP | |
| * error returns the deterministic mock so the UI degrades gracefully. | |
| */ | |
| export async function predictDetectability( | |
| input: string, | |
| model: RealModel, | |
| ): Promise<PredictOutcome> { | |
| const peptides = parseSequences(input).map((p) => p.seq) | |
| const label = `${model.code} · ${model.species}` | |
| try { | |
| const res = await fetch(`${API_BASE}/predict`, { | |
| method: 'POST', | |
| headers: { 'Content-Type': 'application/json' }, | |
| body: JSON.stringify({ peptides, modelCode: model.code }), | |
| }) | |
| if (!res.ok) throw new Error(`HTTP ${res.status}`) | |
| const data: PredictResponse = await res.json() | |
| const rows: DetectabilityRow[] = data.results.map((r) => ({ | |
| peptide: r.peptide, | |
| length: r.length, | |
| score: r.score, | |
| prediction: r.prediction === 'Detectable' ? 'Detectable' : 'Not detectable', | |
| model: label, | |
| inTraining: r.inTraining ?? null, | |
| })) | |
| return { rows, live: true, calibrated: data.calibrated } | |
| } catch { | |
| // Backend down / not deployed → labeled illustrative mock. | |
| return { rows: runDetectability(input, model), live: false, calibrated: false } | |
| } | |
| } | |
| // ---------------------------------------------------------------- bioactivity screening | |
| export interface BioactivityOutcome { | |
| rows: BioactivityRow[] | |
| live: boolean | |
| } | |
| export interface AllergenOutcome { | |
| rows: AllergenRow[] | |
| live: boolean | |
| } | |
| interface BioRespPeptide { | |
| peptide: string | |
| bioactivities: string | |
| sources: string | |
| ige_epitope: string | |
| } | |
| interface BioRespProtein { | |
| protein: string | |
| is_recognized_allergen: string | |
| allergen_name: string | |
| organism: string | |
| source: string | |
| } | |
| /** Screen peptides for bioactivity/cytotoxicity + IgE-epitope retention (Q2). */ | |
| export async function screenBioactivityPeptides(input: string): Promise<BioactivityOutcome> { | |
| const sequences = parseSequences(input).map((p) => p.seq) | |
| try { | |
| const res = await fetch(`${API_BASE}/bioactivity`, { | |
| method: 'POST', | |
| headers: { 'Content-Type': 'application/json' }, | |
| body: JSON.stringify({ mode: 'peptides', sequences }), | |
| }) | |
| if (!res.ok) throw new Error(`HTTP ${res.status}`) | |
| const data: BioRespPeptide[] = await res.json() | |
| const rows: BioactivityRow[] = data.map((r) => ({ | |
| peptide: r.peptide, | |
| bioactivities: r.bioactivities, | |
| sources: r.sources, | |
| igeEpitope: r.ige_epitope === 'yes', | |
| })) | |
| return { rows, live: true } | |
| } catch { | |
| return { rows: runBioactivityPeptides(input), live: false } | |
| } | |
| } | |
| /** Screen proteins for allergen origin, recognized-allergen protein match (Q1). */ | |
| export async function screenAllergenOrigin(input: string): Promise<AllergenOutcome> { | |
| const sequences = parseSequences(input).map((p) => p.seq) | |
| try { | |
| const res = await fetch(`${API_BASE}/bioactivity`, { | |
| method: 'POST', | |
| headers: { 'Content-Type': 'application/json' }, | |
| body: JSON.stringify({ mode: 'proteins', sequences }), | |
| }) | |
| if (!res.ok) throw new Error(`HTTP ${res.status}`) | |
| const data: BioRespProtein[] = await res.json() | |
| const rows: AllergenRow[] = data.map((r) => ({ | |
| protein: r.protein, | |
| isAllergen: r.is_recognized_allergen === 'yes', | |
| allergenName: r.allergen_name, | |
| organism: r.organism, | |
| source: r.source, | |
| })) | |
| return { rows, live: true } | |
| } catch { | |
| return { rows: runAllergenOrigin(input), live: false } | |
| } | |
| } | |
| export interface TrainOutcome { | |
| result: TrainingResult | |
| live: boolean // true = real backend training; false = offline mock fallback | |
| downloadUrl: string | null // GET to fetch the trained joblib (live only) | |
| dataUrl: string | null // GET to fetch the processed dataset CSV (live only) | |
| species?: string | |
| error?: string // set when the backend rejected the request (e.g. unsupported tax id) | |
| } | |
| const sleep = (ms: number) => new Promise((r) => setTimeout(r, ms)) | |
| /** | |
| * Train a detectability model from a peptide list. Submits an async job, polls to | |
| * completion, and maps to TrainingResult. On any network/HTTP error falls back to the | |
| * deterministic client-side mock so the static demo still works offline. | |
| */ | |
| export async function trainDetectability( | |
| input: string, | |
| taxId: string, | |
| cfg: TrainingConfig, | |
| ): Promise<TrainOutcome> { | |
| const peptides = parseSequences(input).map((p) => p.seq) | |
| try { | |
| const res = await fetch(`${API_BASE}/train`, { | |
| method: 'POST', | |
| headers: { 'Content-Type': 'application/json' }, | |
| body: JSON.stringify({ peptides, taxId, config: cfg }), | |
| }) | |
| if (!res.ok) throw new Error(`HTTP ${res.status}`) | |
| const { jobId } = (await res.json()) as { jobId: string } | |
| // poll (cap ~2 min at 1.5 s intervals) | |
| for (let i = 0; i < 80; i++) { | |
| await sleep(1500) | |
| const s = await fetch(`${API_BASE}/train/${jobId}`) | |
| if (!s.ok) throw new Error(`HTTP ${s.status}`) | |
| const job = await s.json() | |
| if (job.status === 'done') { | |
| return { | |
| result: job.result as TrainingResult, | |
| live: true, | |
| downloadUrl: `${API_BASE}/train/${jobId}/model`, | |
| dataUrl: `${API_BASE}/train/${jobId}/data`, | |
| species: job.result.species, | |
| } | |
| } | |
| if (job.status === 'error') { | |
| // A rejected request (bad tax id, unmapped peptides) is a real answer, not a | |
| // reason to silently show fake numbers, surface it with the mock as preview. | |
| return { | |
| result: runTraining(input, taxId + JSON.stringify(cfg)), | |
| live: false, | |
| downloadUrl: null, | |
| dataUrl: null, | |
| error: job.error as string, | |
| } | |
| } | |
| } | |
| throw new Error('training timed out') | |
| } catch { | |
| // Backend down / not deployed → labeled illustrative mock. | |
| return { | |
| result: runTraining(input, taxId + JSON.stringify(cfg)), | |
| live: false, | |
| downloadUrl: null, | |
| dataUrl: null, | |
| } | |
| } | |
| } | |
| // ---------------------------------------------------------------- protein digestibility | |
| export interface Enzyme { | |
| key: string | |
| label: string | |
| } | |
| // Fallback enzyme list so the page still renders if the backend is unreachable (the real | |
| // list comes from GET /api/enzymes, sourced from hydropd.api.available_enzymes()). | |
| const FALLBACK_ENZYMES: Enzyme[] = [ | |
| { key: 'trypsin', label: 'Trypsin' }, | |
| { key: 'chymotrypsin', label: 'Chymotrypsin' }, | |
| { key: 'pepsin_ph1.3', label: 'Pepsin pH1.3' }, | |
| { key: 'pepsin_ph>2', label: 'Pepsin pH >2' }, | |
| { key: 'subtilisin', label: 'Subtilisin' }, | |
| { key: 'papain', label: 'Papain' }, | |
| { key: 'thermolysin', label: 'Thermolysin' }, | |
| { key: 'proteinase_k', label: 'Proteinase K' }, | |
| ] | |
| export async function fetchEnzymes(): Promise<Enzyme[]> { | |
| try { | |
| const res = await fetch(`${API_BASE}/enzymes`) | |
| if (!res.ok) throw new Error(`HTTP ${res.status}`) | |
| return (await res.json()) as Enzyme[] | |
| } catch { | |
| return FALLBACK_ENZYMES | |
| } | |
| } | |
| export interface DigestSummaryRow { | |
| enzyme: string | |
| enzyme_key: string | |
| candidate_set: number | |
| functional_set: number | |
| detected_functional_set?: number | |
| combined_all_models?: number | |
| } | |
| export interface DigestTables { | |
| candidate: Record<string, string | number>[] | |
| candidate_total: number | |
| functional: Record<string, string | number>[] | |
| functional_total: number | |
| detected: Record<string, string | number>[] | |
| detected_total: number | |
| det_cols: string[] | |
| } | |
| export interface DigestResult { | |
| summary: DigestSummaryRow[] | |
| summaryColumns: string[] | |
| models: string[] | |
| calibrated: Record<string, boolean> | |
| multi: boolean | |
| threshold: number | |
| display: Record<string, DigestTables> | |
| nProteins: number | |
| zipName: string | |
| } | |
| export interface DigestOutcome { | |
| result: DigestResult | null | |
| live: boolean | |
| zipUrl: string | null | |
| error?: string | |
| } | |
| /** | |
| * Run a protein-digestibility screen. Submits an async job, polls to completion (heavy: digestion | |
| * + functionality lookup + per-model detectability), and returns the ranked summary + a ZIP URL. | |
| * On any network/HTTP error returns a null result flagged offline so the page degrades gracefully. | |
| */ | |
| export async function computeDigestibility( | |
| proteins: string, | |
| enzymes: string[], | |
| models: string[], | |
| threshold: number, | |
| ): Promise<DigestOutcome> { | |
| try { | |
| const res = await fetch(`${API_BASE}/digestibility`, { | |
| method: 'POST', | |
| headers: { 'Content-Type': 'application/json' }, | |
| body: JSON.stringify({ proteins, enzymes, models, threshold }), | |
| }) | |
| if (!res.ok) throw new Error(`HTTP ${res.status}`) | |
| const { jobId } = (await res.json()) as { jobId: string } | |
| // poll up to ~10 min at 2.5 s intervals (a full all-enzyme, multi-model run is slow) | |
| for (let i = 0; i < 240; i++) { | |
| await sleep(2500) | |
| const s = await fetch(`${API_BASE}/digestibility/${jobId}`) | |
| if (!s.ok) throw new Error(`HTTP ${s.status}`) | |
| const job = await s.json() | |
| if (job.status === 'done') { | |
| return { | |
| result: job.result as DigestResult, | |
| live: true, | |
| zipUrl: `${API_BASE}/digestibility/${jobId}/zip`, | |
| } | |
| } | |
| if (job.status === 'error') { | |
| return { result: null, live: false, zipUrl: null, error: job.error as string } | |
| } | |
| } | |
| throw new Error('digestibility timed out') | |
| } catch (e) { | |
| return { result: null, live: false, zipUrl: null, error: (e as Error).message } | |
| } | |
| } | |