Diabetes-Prediction / static /interface.html
abdullah2026's picture
Upload folder using huggingface_hub
f06b6e1 verified
Raw
History Blame Contribute Delete
51.7 kB
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Diabetes Risk Predictor</title>
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link href="https://fonts.googleapis.com/css2?family=Figtree:wght@300;400;500;600;700;800&display=swap" rel="stylesheet" />
<style>
:root {
--bg: #030912;
--sur: rgba(255,255,255,0.04);
--sur2: rgba(255,255,255,0.07);
--brd: rgba(255,255,255,0.08);
--brd2: rgba(255,255,255,0.14);
--cyan: #06B6D4;
--cyan-lt: #67E8F9;
--cyan-glow: rgba(6,182,212,0.22);
--t1: #E2F4FF;
--t2: #94A3B8;
--t3: #475569;
--hi: #F43F5E; --hi-s: rgba(244,63,94,0.13); --hi-b: rgba(244,63,94,0.28);
--mo: #F97316; --mo-s: rgba(249,115,60,0.13); --mo-b: rgba(249,115,60,0.28);
--lo: #10B981; --lo-s: rgba(16,185,129,0.13); --lo-b: rgba(16,185,129,0.28);
--r: 14px; --r-s: 9px; --ease: cubic-bezier(.4,0,.2,1);
}
*,*::before,*::after{box-sizing:border-box;margin:0;padding:0}
html{scroll-behavior:smooth}
body{
font-family:'Figtree',system-ui,sans-serif;
background:var(--bg);
color:var(--t1);
min-height:100vh;
-webkit-font-smoothing:antialiased;
overflow-x:hidden;
}
/* ── Blood-cell canvas ─────────────────────────────── */
#bgCanvas{
position:fixed;inset:0;z-index:0;
pointer-events:none;opacity:.9;
}
/* ── Header ────────────────────────────────────────── */
header{
position:sticky;top:0;z-index:50;
height:68px;
background:rgba(3,9,18,.82);
backdrop-filter:blur(24px);
-webkit-backdrop-filter:blur(24px);
border-bottom:1px solid var(--brd);
display:flex;align-items:center;justify-content:space-between;
padding:0 2rem;
overflow:hidden;
}
/* ECG wave lives here as absolute background */
.ecg-track{
position:absolute;inset:0;overflow:hidden;
pointer-events:none;
/* fade in after brand (~300px), fade out before status pill (~120px from right) */
-webkit-mask-image:linear-gradient(
to right,
transparent 0px,
transparent 300px,
black 390px,
black calc(100% - 130px),
transparent calc(100% - 60px)
);
mask-image:linear-gradient(
to right,
transparent 0px,
transparent 300px,
black 390px,
black calc(100% - 130px),
transparent calc(100% - 60px)
);
}
.ecg-svg{
position:absolute;top:50%;left:0;
transform:translateY(-50%);
will-change:transform;
}
/* cycle width = 140px β†’ animation translates exactly one cycle */
.ecg-svg.playing{
animation:ecgScroll 3s linear infinite;
}
@keyframes ecgScroll{
from{transform:translateY(-50%) translateX(0)}
to {transform:translateY(-50%) translateX(-140px)}
}
/* faint baseline so the full line is always visible */
.ecg-base{
fill:none;
stroke:var(--cyan);
stroke-width:1;
opacity:.12;
transition:stroke 1s ease;
}
.ecg-path{
fill:none;
stroke:var(--cyan);
stroke-width:2;
stroke-linecap:round;
stroke-linejoin:round;
opacity:.55;
transition:stroke 1s ease, opacity .5s;
}
.brand{position:relative;z-index:1;display:flex;align-items:center;gap:.85rem}
.brand-icon{
width:40px;height:40px;border-radius:11px;
background:linear-gradient(135deg,var(--cyan),var(--cyan-lt));
box-shadow:0 0 18px var(--cyan-glow);
display:flex;align-items:center;justify-content:center;
flex-shrink:0;
}
.brand-name{font-size:1rem;font-weight:700;color:var(--t1);letter-spacing:-.015em;line-height:1.2}
.brand-tag{
font-size:.65rem;font-weight:700;letter-spacing:.1em;
text-transform:uppercase;color:var(--cyan-lt);
background:rgba(103,232,249,.1);border:1px solid rgba(103,232,249,.2);
padding:.16rem .55rem;border-radius:100px;
}
.hd-right{position:relative;z-index:1;display:flex;align-items:center;gap:.85rem}
.status-pill{
display:flex;align-items:center;gap:.6rem;
padding:.55rem 1.1rem;
background:rgba(16,185,129,0.10);
border:1.5px solid rgba(16,185,129,0.35);
border-radius:100px;
font-size:.82rem;font-weight:700;
color:#34D399;
letter-spacing:.04em;
box-shadow:0 0 14px rgba(16,185,129,0.18);
}
.sdot{
width:9px;height:9px;border-radius:50%;
background:#10B981;
box-shadow:0 0 8px #10B981, 0 0 16px rgba(16,185,129,0.5);
animation:blink 2s ease infinite;
flex-shrink:0;
}
@keyframes blink{0%,100%{opacity:1;box-shadow:0 0 8px #10B981,0 0 16px rgba(16,185,129,.5)}50%{opacity:.5;box-shadow:0 0 4px #10B981}}
/* ── Main grid ─────────────────────────────────────── */
.wrap{
position:relative;z-index:1;
max-width:1300px;margin:0 auto;
padding:2rem 1.5rem 3rem;
display:grid;
grid-template-columns:1fr 1fr;
gap:1.5rem;
align-items:start;
}
@media(max-width:880px){.wrap{grid-template-columns:1fr}}
/* ── Glass card ────────────────────────────────────── */
.card{
background:var(--sur);
backdrop-filter:blur(20px);-webkit-backdrop-filter:blur(20px);
border:1px solid var(--brd);
border-radius:var(--r);overflow:hidden;
}
.card-hd{
padding:1.1rem 1.5rem;border-bottom:1px solid var(--brd);
display:flex;align-items:center;gap:.7rem;
}
.card-ico{
width:30px;height:30px;border-radius:8px;
background:var(--cyan-glow);border:1px solid rgba(6,182,212,.25);
display:flex;align-items:center;justify-content:center;
color:var(--cyan-lt);flex-shrink:0;
}
.card-ttl{font-size:.88rem;font-weight:600;color:var(--t1)}
.card-bd{padding:1.5rem}
/* ── Presets ───────────────────────────────────────── */
.pre-lbl{font-size:.67rem;font-weight:700;letter-spacing:.09em;text-transform:uppercase;color:var(--t3);margin-bottom:.45rem}
.pre-row{display:flex;gap:.5rem;margin-bottom:1.4rem}
.pre-btn{
flex:1;padding:.42rem .4rem;
border-radius:var(--r-s);border:1px solid var(--brd);
background:rgba(255,255,255,.025);
font-family:'Figtree',sans-serif;font-size:.73rem;font-weight:600;
color:var(--t2);cursor:pointer;
transition:all .18s var(--ease);
}
.pre-btn:hover{background:var(--sur2)}
.pre-btn.pl:hover{border-color:var(--lo); color:var(--lo)}
.pre-btn.pm:hover{border-color:var(--mo); color:var(--mo)}
.pre-btn.ph:hover{border-color:var(--hi); color:var(--hi)}
.pre-btn:focus-visible{outline:2px solid var(--cyan);outline-offset:2px}
/* ── Field grid ────────────────────────────────────── */
.fgrid{display:grid;grid-template-columns:1fr 1fr;gap:1.1rem 1.2rem}
.field{display:flex;flex-direction:column;gap:.28rem}
.field.full{grid-column:1/-1}
.flbl{
display:flex;justify-content:space-between;align-items:center;
}
.flbl-txt{
font-size:.7rem;font-weight:700;
text-transform:uppercase;letter-spacing:.07em;color:var(--t2);
}
.flbl-txt span{font-size:.66rem;font-weight:400;text-transform:none;letter-spacing:0;color:var(--t3)}
.status-tag{
font-size:.62rem;font-weight:700;letter-spacing:.06em;text-transform:uppercase;
padding:.1rem .48rem;border-radius:100px;
transition:all .3s var(--ease);
display:none;
}
/* dual control: slider + number */
.dual{display:flex;align-items:center;gap:.6rem}
/* range slider */
input[type="range"]{
-webkit-appearance:none;appearance:none;
flex:1;height:5px;border-radius:100px;cursor:pointer;outline:none;
background:linear-gradient(to right,var(--track-fill,var(--cyan)) 0%,var(--track-fill,var(--cyan)) var(--pct,50%),rgba(255,255,255,.1) var(--pct,50%),rgba(255,255,255,.1) 100%);
transition:background .1s;
}
input[type="range"]::-webkit-slider-thumb{
-webkit-appearance:none;appearance:none;
width:14px;height:14px;border-radius:50%;
background:var(--thumb-color,var(--cyan));
border:2px solid rgba(255,255,255,.25);
box-shadow:0 0 8px var(--thumb-glow,var(--cyan-glow));
transition:transform .15s, box-shadow .15s;
}
input[type="range"]::-webkit-slider-thumb:hover{transform:scale(1.25)}
input[type="range"]::-moz-range-thumb{
width:14px;height:14px;border-radius:50%;border:none;
background:var(--thumb-color,var(--cyan));
}
input[type="range"]:focus-visible::-webkit-slider-thumb{
box-shadow:0 0 0 3px var(--cyan-glow), 0 0 8px var(--cyan-glow);
}
/* number input */
input[type="number"]{
-moz-appearance:textfield;
width:62px;padding:.42rem .5rem;
background:rgba(255,255,255,.05);
border:1.5px solid var(--brd);border-radius:var(--r-s);
color:var(--t1);font-family:'Figtree',sans-serif;
font-size:.85rem;font-weight:600;text-align:center;
outline:none;transition:border-color .18s,box-shadow .18s;
}
input[type="number"]::-webkit-inner-spin-button,
input[type="number"]::-webkit-outer-spin-button{-webkit-appearance:none}
input[type="number"]:focus{
border-color:var(--cyan);
box-shadow:0 0 0 3px var(--cyan-glow);
}
.field-hint{font-size:.64rem;color:var(--t3)}
/* ── Run button ────────────────────────────────────── */
.btn-run{
margin-top:1.35rem;width:100%;
padding:.85rem 1rem;
background:linear-gradient(135deg,var(--cyan),var(--cyan-lt));
color:#fff;font-family:'Figtree',sans-serif;
font-size:.93rem;font-weight:700;letter-spacing:.01em;
border:none;border-radius:var(--r-s);cursor:pointer;
display:flex;align-items:center;justify-content:center;gap:.55rem;
position:relative;overflow:hidden;
transition:transform .15s var(--ease),box-shadow .15s;
}
.btn-run:hover:not(:disabled){
transform:translateY(-1px);
box-shadow:0 10px 30px rgba(6,182,212,.40);
}
.btn-run:active:not(:disabled){transform:translateY(0)}
.btn-run:disabled{cursor:not-allowed;opacity:.6}
.btn-run:focus-visible{outline:2px solid var(--cyan-lt);outline-offset:3px}
.btn-run.loading::after{
content:'';position:absolute;inset:0;
background:linear-gradient(90deg,transparent,rgba(255,255,255,.18),transparent);
animation:shim 1.3s linear infinite;
}
@keyframes shim{from{transform:translateX(-100%)}to{transform:translateX(100%)}}
.spin{
display:none;width:15px;height:15px;
border:2.5px solid rgba(255,255,255,.3);
border-top-color:#fff;border-radius:50%;
animation:rot .65s linear infinite;
}
.btn-run.loading .spin{display:block}
.btn-run.loading .btn-ic{display:none}
@keyframes rot{to{transform:rotate(360deg)}}
/* ── Error ─────────────────────────────────────────── */
.err-box{
margin-top:.85rem;display:none;
padding:.7rem .9rem;
background:var(--hi-s);border:1px solid var(--hi-b);
border-radius:var(--r-s);color:#FDA4AF;
font-size:.8rem;align-items:flex-start;gap:.45rem;
}
.err-box.show{display:flex}
/* ── Results: empty ────────────────────────────────── */
.empty-s{
padding:3.8rem 2rem;
display:flex;flex-direction:column;align-items:center;gap:.9rem;text-align:center;
}
.empty-ring{
width:64px;height:64px;border-radius:50%;
border:1.5px dashed rgba(255,255,255,.13);
display:flex;align-items:center;justify-content:center;
color:var(--t3);
animation:pulse-ring 3s ease infinite;
}
@keyframes pulse-ring{
0%,100%{box-shadow:0 0 0 0 rgba(6,182,212,0)}
50%{box-shadow:0 0 0 8px rgba(6,182,212,.07)}
}
.empty-h{font-size:.9rem;font-weight:600;color:var(--t2)}
.empty-p{font-size:.77rem;color:var(--t3);max-width:195px;line-height:1.6}
/* ── Results: output ───────────────────────────────── */
.res-out{display:none}
.res-out.show{display:block;animation:fadeUp .4s var(--ease)}
@keyframes fadeUp{from{opacity:0;transform:translateY(8px)}to{opacity:1;transform:translateY(0)}}
/* scanning overlay */
.scan-overlay{
position:absolute;inset:0;pointer-events:none;z-index:20;
background:linear-gradient(to bottom,transparent 44%,rgba(6,182,212,.07) 48%,rgba(6,182,212,.16) 50%,rgba(6,182,212,.07) 52%,transparent 56%);
display:none;
}
.card.scanning .scan-overlay{display:block;animation:scanDown 1s ease infinite}
@keyframes scanDown{from{transform:translateY(-110%)}to{transform:translateY(110%)}}
/* risk badge */
.r-badge{
display:inline-flex;align-items:center;gap:.4rem;
padding:.3rem .8rem;border-radius:100px;
font-size:.7rem;font-weight:800;text-transform:uppercase;letter-spacing:.09em;
margin-bottom:1.3rem;
}
.r-badge.high {background:var(--hi-s);color:#FB7185;border:1px solid var(--hi-b)}
.r-badge.moderate{background:var(--mo-s);color:#FB923C;border:1px solid var(--mo-b)}
.r-badge.low {background:var(--lo-s);color:#34D399;border:1px solid var(--lo-b)}
/* gauge */
.gauge-wrap{display:flex;justify-content:center;margin-bottom:1.4rem}
.gauge-container{position:relative;width:170px;height:170px;display:flex;align-items:center;justify-content:center}
.gauge-svg{position:absolute;inset:0;transform:rotate(-90deg)}
.g-track{fill:none;stroke:rgba(255,255,255,.06);stroke-width:12}
.g-fill{
fill:none;stroke-width:12;stroke-linecap:round;
stroke-dasharray:408.41; /* 2π×65 */
stroke-dashoffset:408.41;
transition:stroke-dashoffset 1s var(--ease),stroke .5s ease;
}
/* outer glow ring */
.g-outer{fill:none;stroke-width:1.5;stroke-dasharray:408.41;stroke-dashoffset:408.41;opacity:.35;transition:stroke-dashoffset 1s var(--ease),stroke .5s ease}
.gauge-info{
position:relative;z-index:1;
display:flex;flex-direction:column;align-items:center;gap:0;
}
.g-pct{font-size:2.4rem;font-weight:800;letter-spacing:-.05em;line-height:1}
.g-lbl{font-size:.64rem;font-weight:700;letter-spacing:.1em;text-transform:uppercase;color:var(--t3);margin-top:.18rem}
/* stats row */
.stats{
display:grid;grid-template-columns:1fr 1px 1fr 1px 1fr;
background:rgba(255,255,255,.025);border:1px solid var(--brd);
border-radius:var(--r-s);margin-bottom:1.35rem;overflow:hidden;
}
.stat{padding:.72rem .5rem;text-align:center}
.stat-v{font-size:.92rem;font-weight:700;color:var(--t1);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.stat-k{font-size:.62rem;font-weight:700;text-transform:uppercase;letter-spacing:.07em;color:var(--t3);margin-top:.1rem}
.st-div{background:var(--brd);align-self:stretch}
/* shap */
.shap-hd{
display:flex;align-items:center;gap:.45rem;
font-size:.7rem;font-weight:700;text-transform:uppercase;letter-spacing:.08em;
color:var(--t2);margin-bottom:.75rem;
}
.shap-ico{
width:20px;height:20px;border-radius:5px;
background:rgba(249,115,22,.12);border:1px solid rgba(249,115,22,.22);
display:flex;align-items:center;justify-content:center;color:#FB923C;flex-shrink:0;
}
/* plain-language summary box */
.shap-summary{
padding:.7rem .9rem;
background:rgba(255,255,255,.035);
border:1px solid var(--brd);
border-left:3px solid var(--cyan);
border-radius:var(--r-s);
font-size:.79rem;line-height:1.65;color:var(--t2);
margin-bottom:1rem;
}
.shap-summary strong{color:var(--t1);font-weight:600}
/* legend row */
.shap-legend{
display:flex;gap:1rem;margin-bottom:.8rem;
}
.legend-item{
display:flex;align-items:center;gap:.35rem;
font-size:.67rem;font-weight:600;color:var(--t3);
}
.legend-dot{width:8px;height:8px;border-radius:2px;flex-shrink:0}
.legend-dot.p{background:linear-gradient(90deg,#BE123C,#F87171)}
.legend-dot.n{background:linear-gradient(90deg,#065F46,#34D399)}
/* each bar row */
.sh-row{
display:grid;grid-template-columns:8.5rem 1fr 5.2rem;
align-items:center;gap:.5rem;margin-bottom:.55rem;
opacity:0;animation:shIn .3s var(--ease) forwards;
cursor:default;
}
.sh-row:hover .sh-track{background:rgba(255,255,255,.1)}
@keyframes shIn{from{opacity:0;transform:translateX(-6px)}to{opacity:1;transform:translateX(0)}}
.sh-nm{font-size:.74rem;color:var(--t1);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.sh-track{height:5px;background:rgba(255,255,255,.06);border-radius:100px;overflow:hidden;transition:background .2s}
.sh-bar{height:100%;border-radius:100px;transition:width .6s var(--ease)}
.sh-bar.p{background:linear-gradient(90deg,#BE123C,#F87171)}
.sh-bar.n{background:linear-gradient(90deg,#065F46,#34D399)}
/* impact badge (replaces raw decimal) */
.sh-badge{
font-size:.63rem;font-weight:700;text-align:center;
padding:.18rem .48rem;border-radius:100px;
white-space:nowrap;cursor:help;
transition:opacity .15s;
}
.sh-badge:hover{opacity:.75}
.sh-badge.strong {background:rgba(244,63,94,.15); color:#FB7185; border:1px solid rgba(244,63,94,.3)}
.sh-badge.moderate{background:rgba(249,115,22,.15);color:#FB923C; border:1px solid rgba(249,115,22,.3)}
.sh-badge.low {background:rgba(100,116,139,.15);color:#94A3B8;border:1px solid rgba(100,116,139,.25)}
/* ── Footer ────────────────────────────────────────── */
footer{
position:relative;z-index:1;
text-align:center;padding:1.25rem 2rem 2.5rem;
color:var(--t3);font-size:.71rem;line-height:1.7;
border-top:1px solid var(--brd);
}
@media(prefers-reduced-motion:reduce){
*,*::before,*::after{animation-duration:.01ms!important;transition-duration:.01ms!important}
}
</style>
</head>
<body>
<!-- floating blood-cell particles -->
<canvas id="bgCanvas"></canvas>
<!-- ══ HEADER ══════════════════════════════════════════════════ -->
<header>
<!-- ECG animated background -->
<div class="ecg-track" aria-hidden="true">
<svg class="ecg-svg" id="ecgSvg" height="60">
<!-- full-width faint baseline so the line is always visible -->
<line class="ecg-base" id="ecgBase" x1="0" y1="30" x2="9999" y2="30"/>
<!-- animated ECG waveform -->
<path class="ecg-path" id="ecgPath"/>
</svg>
</div>
<div class="brand">
<div class="brand-icon" aria-hidden="true">
<svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="#fff" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round">
<polyline points="22 12 18 12 15 21 9 3 6 12 2 12"/>
</svg>
</div>
<div>
<div class="brand-name">Diabetes Risk Predictor</div>
<div class="brand-tag">XGBoost Β· SHAP Explainability</div>
</div>
</div>
<div class="hd-right">
<div class="status-pill" role="status" aria-label="API status">
<div class="sdot" aria-hidden="true"></div>
<span>API Live</span>
</div>
</div>
</header>
<!-- ══ MAIN ════════════════════════════════════════════════════ -->
<div class="wrap">
<!-- ── LEFT: Patient Input ─────────────────────────── -->
<section class="card" aria-label="Patient parameter input">
<div class="card-hd">
<div class="card-ico" aria-hidden="true">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.3" stroke-linecap="round" stroke-linejoin="round">
<path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"/><circle cx="12" cy="7" r="4"/>
</svg>
</div>
<span class="card-ttl">Patient Parameters</span>
</div>
<div class="card-bd">
<div class="pre-lbl">Quick Presets</div>
<div class="pre-row">
<button class="pre-btn pl" type="button" onclick="loadPreset('low')" aria-label="Load low-risk preset">Low Risk</button>
<button class="pre-btn pm" type="button" onclick="loadPreset('moderate')" aria-label="Load moderate-risk preset">Moderate</button>
<button class="pre-btn ph" type="button" onclick="loadPreset('high')" aria-label="Load high-risk preset">High Risk</button>
</div>
<form id="pForm" onsubmit="return false;" novalidate>
<div class="fgrid">
<!-- pregnancies -->
<div class="field" id="f-pregnancies">
<div class="flbl">
<span class="flbl-txt">Pregnancies</span>
<span class="status-tag" id="st-pregnancies"></span>
</div>
<div class="dual">
<input type="range" id="r-pregnancies" min="0" max="17" step="1" value="2" oninput="sync('pregnancies',this.value)" aria-label="Pregnancies slider"/>
<input type="number" id="pregnancies" min="0" max="17" step="1" value="2" oninput="sync('pregnancies',this.value,true)" aria-label="Pregnancies value"/>
</div>
<span class="field-hint">0 – 17</span>
</div>
<!-- glucose -->
<div class="field" id="f-glucose">
<div class="flbl">
<span class="flbl-txt">Glucose <span>mg/dL</span></span>
<span class="status-tag" id="st-glucose"></span>
</div>
<div class="dual">
<input type="range" id="r-glucose" min="50" max="250" step="1" value="120" oninput="sync('glucose',this.value)" aria-label="Glucose slider"/>
<input type="number" id="glucose" min="50" max="250" step="1" value="120" oninput="sync('glucose',this.value,true)" aria-label="Glucose value"/>
</div>
<span class="field-hint">50 – 250</span>
</div>
<!-- blood_pressure -->
<div class="field" id="f-blood_pressure">
<div class="flbl">
<span class="flbl-txt">Blood Pressure <span>mm Hg</span></span>
<span class="status-tag" id="st-blood_pressure"></span>
</div>
<div class="dual">
<input type="range" id="r-blood_pressure" min="40" max="130" step="1" value="72" oninput="sync('blood_pressure',this.value)" aria-label="Blood pressure slider"/>
<input type="number" id="blood_pressure" min="40" max="130" step="1" value="72" oninput="sync('blood_pressure',this.value,true)" aria-label="Blood pressure value"/>
</div>
<span class="field-hint">40 – 130</span>
</div>
<!-- skin_thickness -->
<div class="field" id="f-skin_thickness">
<div class="flbl">
<span class="flbl-txt">Skin Thickness <span>mm</span></span>
<span class="status-tag" id="st-skin_thickness"></span>
</div>
<div class="dual">
<input type="range" id="r-skin_thickness" min="0" max="100" step="1" value="20" oninput="sync('skin_thickness',this.value)" aria-label="Skin thickness slider"/>
<input type="number" id="skin_thickness" min="0" max="100" step="1" value="20" oninput="sync('skin_thickness',this.value,true)" aria-label="Skin thickness value"/>
</div>
<span class="field-hint">0 – 100</span>
</div>
<!-- insulin -->
<div class="field" id="f-insulin">
<div class="flbl">
<span class="flbl-txt">Insulin <span>mu U/ml</span></span>
<span class="status-tag" id="st-insulin"></span>
</div>
<div class="dual">
<input type="range" id="r-insulin" min="0" max="900" step="1" value="80" oninput="sync('insulin',this.value)" aria-label="Insulin slider"/>
<input type="number" id="insulin" min="0" max="900" step="1" value="80" oninput="sync('insulin',this.value,true)" aria-label="Insulin value"/>
</div>
<span class="field-hint">0 – 900</span>
</div>
<!-- bmi -->
<div class="field" id="f-bmi">
<div class="flbl">
<span class="flbl-txt">BMI</span>
<span class="status-tag" id="st-bmi"></span>
</div>
<div class="dual">
<input type="range" id="r-bmi" min="10" max="70" step="0.1" value="25" oninput="sync('bmi',this.value)" aria-label="BMI slider"/>
<input type="number" id="bmi" min="10" max="70" step="0.1" value="25" oninput="sync('bmi',this.value,true)" aria-label="BMI value"/>
</div>
<span class="field-hint">10.0 – 70.0</span>
</div>
<!-- dpf -->
<div class="field full" id="f-dpf">
<div class="flbl">
<span class="flbl-txt">Diabetes Pedigree Function</span>
<span class="status-tag" id="st-dpf"></span>
</div>
<div class="dual">
<input type="range" id="r-dpf" min="0" max="2.5" step="0.01" value="0.35" oninput="sync('dpf',this.value)" aria-label="Diabetes pedigree slider"/>
<input type="number" id="dpf" min="0" max="2.5" step="0.01" value="0.35" oninput="sync('dpf',this.value,true)" aria-label="Diabetes pedigree value"/>
</div>
<span class="field-hint">0.0 – 2.5 &nbsp;Β·&nbsp; Hereditary risk score</span>
</div>
<!-- age -->
<div class="field" id="f-age">
<div class="flbl">
<span class="flbl-txt">Age</span>
<span class="status-tag" id="st-age"></span>
</div>
<div class="dual">
<input type="range" id="r-age" min="18" max="90" step="1" value="30" oninput="sync('age',this.value)" aria-label="Age slider"/>
<input type="number" id="age" min="18" max="90" step="1" value="30" oninput="sync('age',this.value,true)" aria-label="Age value"/>
</div>
<span class="field-hint">18 – 90</span>
</div>
</div>
<button class="btn-run" id="runBtn" type="submit" onclick="runAnalysis()" aria-label="Run diabetes risk analysis">
<div class="spin" aria-hidden="true"></div>
<svg class="btn-ic" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
<polygon points="5 3 19 12 5 21 5 3"/>
</svg>
<span id="btnTxt">Run Analysis</span>
</button>
<div class="err-box" id="errBox" role="alert" aria-live="assertive">
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" style="flex-shrink:0;margin-top:1px">
<circle cx="12" cy="12" r="10"/><line x1="12" y1="8" x2="12" y2="12"/><line x1="12" y1="16" x2="12.01" y2="16"/>
</svg>
<span id="errTxt"></span>
</div>
</form>
</div>
</section>
<!-- ── RIGHT: Risk Assessment ──────────────────────── -->
<section class="card" id="resultCard" aria-label="Prediction results" style="position:relative" aria-live="polite">
<div class="scan-overlay" id="scanOverlay" aria-hidden="true"></div>
<div class="card-hd">
<div class="card-ico" aria-hidden="true">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.3" stroke-linecap="round" stroke-linejoin="round">
<path d="M22 12h-4l-3 9L9 3l-3 9H2"/>
</svg>
</div>
<span class="card-ttl">Risk Assessment</span>
</div>
<div class="card-bd">
<!-- Empty state -->
<div class="empty-s" id="emptyS">
<div class="empty-ring" aria-hidden="true">
<svg width="26" height="26" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
<circle cx="12" cy="12" r="10"/><line x1="12" y1="8" x2="12" y2="12"/><line x1="12" y1="16" x2="12.01" y2="16"/>
</svg>
</div>
<div class="empty-h">Awaiting Analysis</div>
<p class="empty-p">Adjust the patient parameters on the left and click Run Analysis.</p>
</div>
<!-- Result output -->
<div class="res-out" id="resOut">
<div class="r-badge" id="rBadge" role="status"></div>
<!-- Gauge -->
<div class="gauge-wrap">
<div class="gauge-container">
<svg class="gauge-svg" width="170" height="170" viewBox="0 0 170 170" aria-hidden="true">
<!-- outer decoration ring -->
<circle class="g-outer" id="gOuter" cx="85" cy="85" r="78" stroke="var(--cyan)"/>
<!-- track -->
<circle class="g-track" cx="85" cy="85" r="65"/>
<!-- fill arc -->
<circle class="g-fill" id="gFill" cx="85" cy="85" r="65"/>
</svg>
<div class="gauge-info" aria-label="Risk probability gauge">
<div class="g-pct" id="gPct">β€”</div>
<div class="g-lbl">Risk Score</div>
</div>
</div>
</div>
<!-- Stats -->
<div class="stats" role="list">
<div class="stat" role="listitem">
<div class="stat-v" id="sLabel">β€”</div>
<div class="stat-k">Diagnosis</div>
</div>
<div class="st-div" aria-hidden="true"></div>
<div class="stat" role="listitem">
<div class="stat-v" id="sProb">β€”</div>
<div class="stat-k">Probability</div>
</div>
<div class="st-div" aria-hidden="true"></div>
<div class="stat" role="listitem">
<div class="stat-v">40%</div>
<div class="stat-k">Threshold</div>
</div>
</div>
<!-- SHAP -->
<div>
<div class="shap-hd">
<div class="shap-ico" aria-hidden="true">
<svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
<polygon points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2"/>
</svg>
</div>
What's Driving This Result?
</div>
<div id="shapBars" role="list" aria-label="SHAP feature contributions"></div>
</div>
</div>
</div>
</section>
</div>
<!-- ══ FOOTER ══════════════════════════════════════════════════ -->
<footer>
<strong style="color:var(--t2);font-weight:600">Clinical Disclaimer</strong><br>
This interface is for research and educational purposes only. Predictions are generated by a machine learning model
and do not constitute medical advice, diagnosis, or treatment.
Always consult a qualified healthcare professional for clinical decisions.
</footer>
<script>
/* ═══════════════════════════════════════════════════════════════
CONFIG
═══════════════════════════════════════════════════════════════ */
const API = '';
const CIRC = 2 * Math.PI * 65; // β‰ˆ 408.41
const RISK = {
high: { color:'#F43F5E', light:'#FB7185', glow:'rgba(244,63,94,.35)', label:'High Risk', ecg:'#F43F5E' },
moderate: { color:'#F97316', light:'#FB923C', glow:'rgba(249,115,22,.35)', label:'Moderate Risk', ecg:'#F97316' },
low: { color:'#10B981', light:'#34D399', glow:'rgba(16,185,129,.35)', label:'Low Risk', ecg:'#10B981' },
};
/* ═══════════════════════════════════════════════════════════════
ECG WAVEFORM β€” built in JS, set on SVG path
═══════════════════════════════════════════════════════════════ */
(function buildECG() {
/*
* Cycle width = 140px, height = 60, baseline = y=30.
* Denser waveform so the whole header is filled with visible spikes.
* SVG width = screen width + 2 extra cycles so there is never a gap.
*/
const W = 140; // cycle width β€” must match CSS keyframe translateX
const BL = 30; // baseline y
const cycle = [ // [x_offset, y_relative_to_baseline]
[0,0],[18,0],
[21,-3],[24,-8],[27,-3],[30,0], // P wave
[35,0],[37,4],[40,-34],[44,10],[47,0], // QRS complex
[50,0],[53,-2],[56,-8],[60,-13],[64,-8],[68,0], // T wave
[W,0], // flat to next cycle
];
function buildPath(cycles) {
let d = '';
for (let c = 0; c < cycles; c++) {
cycle.forEach(([x, dy], i) => {
const cmd = (c === 0 && i === 0) ? 'M' : 'L';
d += `${cmd}${c * W + x},${BL + dy} `;
});
}
return d.trim();
}
function init() {
const screenW = window.innerWidth;
const cycles = Math.ceil(screenW / W) + 3; // extra cycles for seamless fill
const svg = document.getElementById('ecgSvg');
svg.setAttribute('width', cycles * W);
svg.setAttribute('viewBox',`0 0 ${cycles * W} 60`);
document.getElementById('ecgPath').setAttribute('d', buildPath(cycles));
svg.classList.add('playing');
}
init();
window.addEventListener('resize', init);
})();
function setECGColor(color) {
document.querySelector('.ecg-path').style.stroke = color;
document.querySelector('.ecg-base').style.stroke = color;
}
/* ═══════════════════════════════════════════════════════════════
BLOOD-CELL CANVAS PARTICLES
═══════════════════════════════════════════════════════════════ */
(function initCanvas() {
const canvas = document.getElementById('bgCanvas');
const ctx = canvas.getContext('2d');
function resize() {
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
}
resize();
window.addEventListener('resize', resize);
const CELLS = Array.from({length: 28}, () => ({
x: Math.random() * window.innerWidth,
y: Math.random() * window.innerHeight,
rx: 7 + Math.random() * 11,
vy: -(0.18 + Math.random() * 0.35),
vx: (Math.random() - .5) * 0.25,
ang: Math.random() * Math.PI,
va: (Math.random() - .5) * 0.006,
op: 0.025 + Math.random() * 0.04,
}));
function drawFrame() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
CELLS.forEach(c => {
ctx.save();
ctx.translate(c.x, c.y);
ctx.rotate(c.ang);
/* outer cell body */
ctx.beginPath();
ctx.ellipse(0, 0, c.rx, c.rx * 0.58, 0, 0, Math.PI * 2);
ctx.fillStyle = `rgba(200,40,60,${c.op})`;
ctx.fill();
/* biconcave dimple */
ctx.beginPath();
ctx.ellipse(0, 0, c.rx * 0.52, c.rx * 0.22, 0, 0, Math.PI * 2);
ctx.fillStyle = `rgba(0,4,12,${c.op * 1.6})`;
ctx.fill();
ctx.restore();
c.x += c.vx;
c.y += c.vy;
c.ang += c.va;
if (c.y < -25) { c.y = canvas.height + 25; c.x = Math.random() * canvas.width; }
if (c.x < -25) c.x = canvas.width + 25;
if (c.x > canvas.width + 25) c.x = -25;
});
requestAnimationFrame(drawFrame);
}
drawFrame();
})();
/* ═══════════════════════════════════════════════════════════════
CLINICAL STATUS TAGS β€” update as sliders move
═══════════════════════════════════════════════════════════════ */
const STATUS_MAP = {
glucose: [
{ max:100, lbl:'Normal', css:'var(--lo-s)', brd:'var(--lo-b)', col:'#34D399' },
{ max:126, lbl:'Pre-Diabetic',css:'var(--mo-s)', brd:'var(--mo-b)', col:'#FB923C' },
{ max:999, lbl:'Diabetic', css:'var(--hi-s)', brd:'var(--hi-b)', col:'#FB7185' },
],
bmi: [
{ max:18.5, lbl:'Underweight',css:'var(--mo-s)',brd:'var(--mo-b)',col:'#FB923C' },
{ max:25, lbl:'Normal', css:'var(--lo-s)',brd:'var(--lo-b)',col:'#34D399' },
{ max:30, lbl:'Overweight', css:'var(--mo-s)',brd:'var(--mo-b)',col:'#FB923C' },
{ max:999, lbl:'Obese', css:'var(--hi-s)',brd:'var(--hi-b)',col:'#FB7185' },
],
blood_pressure: [
{ max:80, lbl:'Normal', css:'var(--lo-s)',brd:'var(--lo-b)',col:'#34D399' },
{ max:90, lbl:'Elevated', css:'var(--mo-s)',brd:'var(--mo-b)',col:'#FB923C' },
{ max:999, lbl:'High', css:'var(--hi-s)',brd:'var(--hi-b)',col:'#FB7185' },
],
insulin: [
{ max:100, lbl:'Normal', css:'var(--lo-s)',brd:'var(--lo-b)',col:'#34D399' },
{ max:166, lbl:'Elevated',css:'var(--mo-s)',brd:'var(--mo-b)',col:'#FB923C' },
{ max:999, lbl:'High', css:'var(--hi-s)',brd:'var(--hi-b)',col:'#FB7185' },
],
};
function updateStatus(id, val) {
const tag = document.getElementById(`st-${id}`);
if (!tag) return;
const rules = STATUS_MAP[id];
if (!rules) { tag.style.display = 'none'; return; }
const r = rules.find(r => +val <= r.max) || rules[rules.length - 1];
const rng = document.getElementById(`r-${id}`);
const num = document.getElementById(id);
tag.textContent = r.lbl;
tag.style.display = 'inline-flex';
tag.style.background = r.css;
tag.style.border = `1px solid ${r.brd}`;
tag.style.color = r.col;
/* tint the slider thumb to match */
if (rng) {
rng.style.setProperty('--track-fill', r.col);
rng.style.setProperty('--thumb-color', r.col);
rng.style.setProperty('--thumb-glow', r.css);
}
}
/* ═══════════════════════════════════════════════════════════════
SLIDER ↔ NUMBER SYNC
═══════════════════════════════════════════════════════════════ */
function updateSliderProgress(rng) {
const mn = +rng.min, mx = +rng.max, val = +rng.value;
const pct = ((val - mn) / (mx - mn) * 100).toFixed(1) + '%';
rng.style.setProperty('--pct', pct);
}
function sync(id, val, fromNum = false) {
const rng = document.getElementById(`r-${id}`);
const num = document.getElementById(id);
if (!fromNum && rng) { num.value = parseFloat(val); }
if (fromNum && rng) { rng.value = parseFloat(val); }
if (rng) updateSliderProgress(rng);
updateStatus(id, val);
}
/* initialise all sliders on load */
['pregnancies','glucose','blood_pressure','skin_thickness','insulin','bmi','dpf','age'].forEach(id => {
const rng = document.getElementById(`r-${id}`);
if (rng) { updateSliderProgress(rng); updateStatus(id, rng.value); }
});
/* ═══════════════════════════════════════════════════════════════
PRESETS
═══════════════════════════════════════════════════════════════ */
const PRESETS = {
low: { pregnancies:1, glucose:88, blood_pressure:66, skin_thickness:16, insulin:48, bmi:21.6, dpf:0.17, age:24 },
moderate: { pregnancies:3, glucose:128, blood_pressure:77, skin_thickness:27, insulin:138, bmi:30.2, dpf:0.42, age:37 },
high: { pregnancies:7, glucose:174, blood_pressure:90, skin_thickness:44, insulin:295, bmi:43.1, dpf:0.93, age:57 },
};
function loadPreset(key) {
const p = PRESETS[key];
const map = { pregnancies:'pregnancies', glucose:'glucose', blood_pressure:'blood_pressure',
skin_thickness:'skin_thickness', insulin:'insulin', bmi:'bmi', dpf:'dpf', age:'age' };
Object.entries(map).forEach(([fid, pk]) => {
const val = p[pk];
document.getElementById(fid).value = val;
const rng = document.getElementById(`r-${fid}`);
if (rng) { rng.value = val; updateSliderProgress(rng); }
updateStatus(fid, val);
});
}
/* ═══════════════════════════════════════════════════════════════
UI HELPERS
═══════════════════════════════════════════════════════════════ */
function setErr(msg) {
const box = document.getElementById('errBox');
document.getElementById('errTxt').textContent = msg;
box.classList.toggle('show', !!msg);
}
function setLoading(on) {
const btn = document.getElementById('runBtn');
const rc = document.getElementById('resultCard');
btn.disabled = on;
btn.classList.toggle('loading', on);
document.getElementById('btnTxt').textContent = on ? 'Analyzing…' : 'Run Analysis';
rc.classList.toggle('scanning', on);
}
/* ═══════════════════════════════════════════════════════════════
GAUGE
═══════════════════════════════════════════════════════════════ */
function animateGauge(prob, level) {
const r = RISK[level] || RISK.low;
const arc = document.getElementById('gFill');
const out = document.getElementById('gOuter');
const pct = document.getElementById('gPct');
arc.style.strokeDashoffset = (CIRC * (1 - prob)).toFixed(2);
arc.style.stroke = r.color;
out.style.strokeDashoffset = (CIRC * (1 - prob)).toFixed(2);
out.style.stroke = r.color;
pct.textContent = `${Math.round(prob * 100)}%`;
pct.style.color = r.light;
}
/* ═══════════════════════════════════════════════════════════════
SHAP β€” PLAIN-LANGUAGE HELPERS
═══════════════════════════════════════════════════════════════ */
const FEATURE_LABELS = {
'Glucose': 'Blood Sugar',
'BMI': 'Body Mass Index',
'Age': 'Age',
'Insulin': 'Insulin Level',
'DiabetesPedigreeFunction': 'Family History Score',
'BloodPressure': 'Blood Pressure',
'SkinThickness': 'Skin Thickness',
'Pregnancies': 'No. of Pregnancies',
'Glucose_BMI': 'Sugar Γ— BMI Combined',
'Age_BMI': 'Age Γ— BMI Combined',
'Glucose_squared': 'High Blood Sugar Intensity',
'Insulin_BMI': 'Insulin Γ— BMI Combined',
};
function impactLevel(absVal) {
if (absVal >= 0.30) return { label: 'Strong', cls: 'strong' };
if (absVal >= 0.10) return { label: 'Moderate', cls: 'moderate' };
return { label: 'Low', cls: 'low' };
}
function plainName(feature) {
return FEATURE_LABELS[feature] || feature;
}
function generateSummary(contributions) {
const pos = contributions.filter(c => c.direction === 'increases risk');
const neg = contributions.filter(c => c.direction !== 'increases risk');
let s1 = '';
if (pos.length >= 2) {
s1 = `<strong>${plainName(pos[0].feature)}</strong> and <strong>${plainName(pos[1].feature)}</strong> are the main factors raising this patient's risk.`;
} else if (pos.length === 1) {
s1 = `<strong>${plainName(pos[0].feature)}</strong> is the main factor raising this patient's risk.`;
} else {
s1 = 'No single factor is strongly increasing this patient\'s risk.';
}
let s2 = '';
if (neg.length >= 2) {
s2 = `<strong>${plainName(neg[0].feature)}</strong> and <strong>${plainName(neg[1].feature)}</strong> are helping to keep the risk lower.`;
} else if (neg.length === 1) {
s2 = `<strong>${plainName(neg[0].feature)}</strong> is helping to keep the risk lower.`;
} else {
s2 = 'No significant protective factors were identified.';
}
return s1 + ' ' + s2;
}
/* ═══════════════════════════════════════════════════════════════
SHAP BARS
═══════════════════════════════════════════════════════════════ */
function renderShap(contributions) {
const wrap = document.getElementById('shapBars');
wrap.innerHTML = '';
const maxA = Math.max(...contributions.map(c => Math.abs(c.shap_value)), 1e-6);
/* plain-language summary */
const summaryDiv = document.createElement('div');
summaryDiv.className = 'shap-summary';
summaryDiv.innerHTML = generateSummary(contributions);
wrap.appendChild(summaryDiv);
/* legend */
const legend = document.createElement('div');
legend.className = 'shap-legend';
legend.innerHTML = `
<div class="legend-item"><div class="legend-dot p"></div>Increases risk</div>
<div class="legend-item"><div class="legend-dot n"></div>Decreases risk</div>`;
wrap.appendChild(legend);
/* bars */
contributions.forEach((c, i) => {
const pos = c.direction === 'increases risk';
const pct = (Math.abs(c.shap_value) / maxA * 100).toFixed(1);
const absV = Math.abs(c.shap_value);
const impact = impactLevel(absV);
const label = plainName(c.feature);
const sign = pos ? '+' : 'βˆ’';
const row = document.createElement('div');
row.className = 'sh-row';
row.style.animationDelay = `${i * 38}ms`;
row.setAttribute('role', 'listitem');
row.setAttribute('aria-label', `${label}: ${impact.label} impact β€” ${c.direction}`);
row.title = `${c.feature}: SHAP ${pos ? '+' : ''}${c.shap_value.toFixed(4)}`;
row.innerHTML = `
<span class="sh-nm">${label}</span>
<div class="sh-track" aria-hidden="true">
<div class="sh-bar ${pos ? 'p' : 'n'}" style="width:${pct}%"></div>
</div>
<span class="sh-badge ${impact.cls}" title="SHAP ${sign}${absV.toFixed(3)}">${impact.label}</span>`;
wrap.appendChild(row);
});
}
/* ═══════════════════════════════════════════════════════════════
SHOW RESULT
═══════════════════════════════════════════════════════════════ */
function showResult(data) {
const level = (data.risk_level || 'low').toLowerCase();
const r = RISK[level] || RISK.low;
/* ECG changes colour to match risk */
setECGColor(r.ecg);
/* Badge */
const badge = document.getElementById('rBadge');
badge.className = `r-badge ${level}`;
badge.innerHTML = `
<svg width="7" height="7" viewBox="0 0 8 8" aria-hidden="true">
<circle cx="4" cy="4" r="4" fill="${r.color}"/>
</svg>
${r.label}`;
/* Gauge */
animateGauge(data.probability, level);
/* Stats */
const lbl = document.getElementById('sLabel');
lbl.textContent = data.prediction;
lbl.style.color = r.light;
document.getElementById('sProb').textContent = `${(data.probability * 100).toFixed(1)}%`;
/* SHAP */
if (Array.isArray(data.contributions)) renderShap(data.contributions);
/* Reveal */
document.getElementById('emptyS').style.display = 'none';
const out = document.getElementById('resOut');
out.classList.remove('show');
void out.offsetWidth;
out.classList.add('show');
}
/* ═══════════════════════════════════════════════════════════════
MAIN β€” RUN ANALYSIS
═══════════════════════════════════════════════════════════════ */
async function runAnalysis() {
setErr('');
setLoading(true);
const IDS = ['pregnancies','glucose','blood_pressure','skin_thickness','insulin','bmi','dpf','age'];
const API_KEYS = ['pregnancies','glucose','blood_pressure','skin_thickness','insulin','bmi','diabetes_pedigree_function','age'];
const payload = {};
for (let i = 0; i < IDS.length; i++) {
const v = parseFloat(document.getElementById(IDS[i]).value);
if (isNaN(v)) {
setErr(`"${IDS[i].replace(/_/g,' ')}" is not a valid number.`);
setLoading(false);
return;
}
payload[API_KEYS[i]] = v;
}
try {
const res = await fetch(`${API}/predict/explain`, {
method:'POST',
headers:{'Content-Type':'application/json'},
body:JSON.stringify(payload),
});
const data = await res.json();
if (!res.ok) { setErr(data.error || `Server error (${res.status}).`); setLoading(false); return; }
showResult(data);
} catch {
setErr('Cannot reach the API β€” ensure Flask is running on localhost:5000.');
} finally {
setLoading(false);
}
}
</script>
</body>
</html>