Spaces:
Sleeping
Sleeping
| // NephroScreen frontend. Same-origin API by default; override API_BASE when the | |
| // frontend is hosted separately from the backend (e.g. Vercel + Render). | |
| const API_BASE = window.NEPHRO_API_BASE || ""; | |
| const EXAMPLE = { | |
| age: 62, bp: 80, sg: 1.01, al: 3, su: 0, bgr: 148, bu: 86, sc: 3.2, | |
| sod: 135, pot: 4.6, hemo: 9.5, pcv: 28, wbcc: 9800, rbcc: 3.4, | |
| rbc: "abnormal", pc: "abnormal", pcc: "present", ba: "notpresent", | |
| htn: "yes", dm: "yes", cad: "no", appet: "poor", pe: "yes", ane: "yes", | |
| }; | |
| const $ = (sel) => document.querySelector(sel); | |
| async function init() { | |
| try { | |
| const meta = await fetch(`${API_BASE}/api/metadata`).then((r) => r.json()); | |
| renderModelStrip(meta); | |
| renderFields(meta); | |
| } catch (e) { | |
| $("#modelStrip").innerHTML = | |
| '<span class="muted">API offline — start the backend to enable predictions.</span>'; | |
| } | |
| $("#predictForm").addEventListener("submit", onSubmit); | |
| $("#exampleBtn").addEventListener("click", loadExample); | |
| } | |
| function renderModelStrip(meta) { | |
| const rf = (meta.metrics && meta.metrics["Random Forest"]) || {}; | |
| const chips = [ | |
| ["Model", "Random Forest"], | |
| ["Accuracy", rf.accuracy != null ? rf.accuracy + "%" : "—"], | |
| ["Recall", rf.recall != null ? rf.recall + "%" : "—"], | |
| ["ROC-AUC", rf.roc_auc != null ? rf.roc_auc : "—"], | |
| ["Threshold", meta.threshold], | |
| ]; | |
| $("#modelStrip").innerHTML = chips | |
| .map(([k, v]) => `<span class="metric-chip">${k} <b>${v}</b></span>`) | |
| .join(""); | |
| } | |
| function renderFields(meta) { | |
| $("#numericFields").innerHTML = meta.numeric_fields | |
| .map( | |
| (f) => `<div class="field"> | |
| <label for="${f.name}">${f.label}</label> | |
| <input type="number" step="any" id="${f.name}" name="${f.name}" placeholder="—" /> | |
| </div>` | |
| ) | |
| .join(""); | |
| $("#categoricalFields").innerHTML = meta.categorical_fields | |
| .map((f) => { | |
| const opts = ['<option value="">—</option>'] | |
| .concat(f.choices.map((c) => `<option value="${c}">${c}</option>`)) | |
| .join(""); | |
| return `<div class="field"> | |
| <label for="${f.name}">${f.label}</label> | |
| <select id="${f.name}" name="${f.name}">${opts}</select> | |
| </div>`; | |
| }) | |
| .join(""); | |
| } | |
| function loadExample() { | |
| for (const [k, v] of Object.entries(EXAMPLE)) { | |
| const el = document.getElementById(k); | |
| if (el) el.value = v; | |
| } | |
| } | |
| function collect() { | |
| const payload = {}; | |
| new FormData($("#predictForm")).forEach((value, key) => { | |
| if (value === "" || value == null) return; | |
| const num = Number(value); | |
| payload[key] = Number.isNaN(num) || value.match(/[a-z]/i) ? value : num; | |
| }); | |
| return payload; | |
| } | |
| async function onSubmit(evt) { | |
| evt.preventDefault(); | |
| const btn = $("#submitBtn"); | |
| btn.disabled = true; | |
| btn.textContent = "Analyzing…"; | |
| try { | |
| const res = await fetch(`${API_BASE}/api/predict`, { | |
| method: "POST", | |
| headers: { "Content-Type": "application/json" }, | |
| body: JSON.stringify(collect()), | |
| }); | |
| if (!res.ok) throw new Error(await res.text()); | |
| renderResult(await res.json()); | |
| } catch (e) { | |
| alert("Prediction failed: " + e.message); | |
| } finally { | |
| btn.disabled = false; | |
| btn.textContent = "Estimate CKD risk"; | |
| } | |
| } | |
| function renderResult(r) { | |
| const pct = Math.round(r.probability * 100); | |
| const ring = | |
| r.risk_band === "High" ? "#dc2626" : r.risk_band === "Moderate" ? "#d97706" : "#16a34a"; | |
| $("#resultCard").innerHTML = ` | |
| <div class="gauge" style="--p:${pct}; --ring:${ring}"><span>${pct}%</span></div> | |
| <div class="verdict"> | |
| <h3>${r.prediction}</h3> | |
| <div class="band band-${r.risk_band}">${r.risk_band} risk</div> | |
| <p>Model probability of CKD: ${pct}% · decision threshold ${r.threshold}</p> | |
| </div>`; | |
| const inds = r.key_indicators || []; | |
| $("#indicators").innerHTML = inds.length | |
| ? inds | |
| .map( | |
| (i) => `<div class="indicator"> | |
| <span>${i.label} — <b>${i.value}</b> <span class="muted">(normal ${i.normal_range})</span></span> | |
| <span class="tag ${i.flag}">${i.flag.toUpperCase()}</span> | |
| </div>` | |
| ) | |
| .join("") | |
| : '<div class="empty">No entered lab values fall outside typical reference ranges.</div>'; | |
| $("#disclaimer").textContent = r.disclaimer; | |
| $("#resultPanel").hidden = false; | |
| $("#resultPanel").scrollIntoView({ behavior: "smooth", block: "nearest" }); | |
| } | |
| init(); | |