sadiqM's picture
Initial commit: NephroScreen — CKD screening ML app
dc3d345
Raw
History Blame Contribute Delete
4.4 kB
// 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();