nilotpaldhar2004's picture
Auto deploy from GitHub: version 3.0.0
7344f51
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>DermSight PRO β€” AI Skin Lesion Classification</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=Bricolage+Grotesque:opsz,wght@12..96,300;12..96,400;12..96,500;12..96,600;12..96,700;12..96,800&family=Geist:wght@300;400;500;600&family=Geist+Mono:wght@400;500&display=swap" rel="stylesheet" />
<style>
/* ── Design Tokens ─────────────────────────────────────── */
:root {
--bg: #f7f8fc;
--bg-2: #eef0f7;
--surface: #ffffff;
--surface-2: #f3f4f8;
--border: #e2e5ef;
--border-hi: #c8cddf;
--navy: #0f2240;
--navy-2: #1e3a5f;
--jade: #0d9488;
--jade-dim: rgba(13,148,136,0.08);
--jade-border: rgba(13,148,136,0.25);
--rose: #e11d48;
--rose-dim: rgba(225,29,72,0.07);
--rose-border: rgba(225,29,72,0.2);
--amber: #d97706;
--amber-dim: rgba(217,119,6,0.08);
--amber-border:rgba(217,119,6,0.25);
--green: #059669;
--green-dim: rgba(5,150,105,0.08);
--green-border:rgba(5,150,105,0.22);
--text: #0f2240;
--text-2: #4b5a72;
--text-3: #8c97ab;
--font-display:'Bricolage Grotesque', system-ui, sans-serif;
--font-body: 'Geist', system-ui, sans-serif;
--font-mono: 'Geist Mono', monospace;
--radius: 8px;
--radius-lg: 14px;
--radius-xl: 20px;
--shadow-sm: 0 1px 3px rgba(15,34,64,0.06), 0 1px 2px rgba(15,34,64,0.04);
--shadow-md: 0 4px 16px rgba(15,34,64,0.08), 0 1px 4px rgba(15,34,64,0.05);
--shadow-lg: 0 12px 40px rgba(15,34,64,0.10), 0 2px 8px rgba(15,34,64,0.06);
--transition: 180ms cubic-bezier(0.4,0,0.2,1);
}
/* ── Reset ─────────────────────────────────────────────── */
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
html { scroll-behavior: smooth; }
body {
font-family: var(--font-body);
background: var(--bg);
color: var(--text);
min-height: 100vh;
overflow-x: hidden;
}
/* Subtle dot grid pattern */
body::before {
content: '';
position: fixed; inset: 0;
background-image: radial-gradient(circle, rgba(15,34,64,0.06) 1px, transparent 1px);
background-size: 28px 28px;
pointer-events: none;
z-index: 0;
}
/* Top right accent blob */
body::after {
content: '';
position: fixed;
top: -120px; right: -120px;
width: 480px; height: 480px;
background: radial-gradient(circle, rgba(13,148,136,0.07) 0%, transparent 70%);
pointer-events: none;
z-index: 0;
}
/* ── Navigation ──────────────────────────────────────────── */
.nav {
position: sticky; top: 0; z-index: 100;
background: rgba(247,248,252,0.92);
backdrop-filter: blur(18px);
border-bottom: 1px solid var(--border);
padding: 0 28px;
height: 62px;
display: flex;
align-items: center;
justify-content: space-between;
}
.nav-brand { display: flex; align-items: center; gap: 13px; }
.brand-mark {
width: 38px; height: 38px;
background: var(--navy);
border-radius: 10px;
display: flex; align-items: center; justify-content: center;
font-family: var(--font-display);
font-size: 19px;
font-weight: 800;
color: #ffffff;
box-shadow: 0 0 0 3px rgba(13,148,136,0.2);
transition: box-shadow var(--transition);
}
.brand-mark:hover { box-shadow: 0 0 0 5px rgba(13,148,136,0.3); }
.brand-text-wrap {}
.brand-name {
font-family: var(--font-display);
font-size: 17px;
font-weight: 700;
color: var(--navy);
letter-spacing: -0.02em;
line-height: 1.2;
}
.brand-name span { color: var(--jade); }
.brand-sub {
font-family: var(--font-mono);
font-size: 9px;
letter-spacing: 0.14em;
text-transform: uppercase;
color: var(--text-3);
}
.nav-links { display: flex; align-items: center; gap: 10px; }
.nav-link {
font-family: var(--font-mono);
font-size: 10px;
letter-spacing: 0.08em;
text-transform: uppercase;
color: var(--text-2);
text-decoration: none;
padding: 7px 13px;
border: 1px solid var(--border);
border-radius: var(--radius);
transition: all var(--transition);
background: var(--surface);
}
.nav-link:hover { color: var(--jade); border-color: var(--jade-border); background: var(--jade-dim); }
.nav-link.primary {
background: var(--navy);
border-color: var(--navy);
color: #fff;
}
.nav-link.primary:hover { background: var(--navy-2); border-color: var(--navy-2); color: #fff; }
/* ── Page ────────────────────────────────────────────────── */
.page {
position: relative; z-index: 1;
max-width: 1200px;
margin: 0 auto;
padding: 40px 24px 80px;
}
/* ── Hero ────────────────────────────────────────────────── */
.hero {
text-align: center;
margin-bottom: 44px;
animation: fade-up 0.5s ease both;
}
.hero-eyebrow {
display: inline-flex;
align-items: center;
gap: 8px;
background: var(--jade-dim);
border: 1px solid var(--jade-border);
border-radius: 99px;
padding: 5px 14px;
font-family: var(--font-mono);
font-size: 10px;
letter-spacing: 0.14em;
text-transform: uppercase;
color: var(--jade);
margin-bottom: 18px;
}
.hero-eyebrow-dot {
width: 6px; height: 6px;
border-radius: 50%;
background: var(--jade);
animation: pulse-dot 2.5s ease infinite;
}
@keyframes pulse-dot {
0%,100% { box-shadow: 0 0 0 0 rgba(13,148,136,0.5); }
50% { box-shadow: 0 0 0 5px rgba(13,148,136,0); }
}
.hero-title {
font-family: var(--font-display);
font-size: clamp(30px, 5vw, 52px);
font-weight: 800;
color: var(--navy);
line-height: 1.1;
letter-spacing: -0.03em;
margin-bottom: 14px;
}
.hero-title em { font-style: italic; color: var(--jade); }
.hero-sub {
font-size: 15px;
color: var(--text-2);
max-width: 520px;
margin: 0 auto;
line-height: 1.7;
font-weight: 400;
}
/* ── Main grid ───────────────────────────────────────────── */
.main-grid {
display: grid;
grid-template-columns: 360px 1fr;
gap: 22px;
align-items: start;
}
/* ── Card ────────────────────────────────────────────────── */
.card {
background: var(--surface);
border: 1px solid var(--border);
border-radius: var(--radius-xl);
box-shadow: var(--shadow-md);
overflow: hidden;
}
.card-header {
padding: 15px 22px;
border-bottom: 1px solid var(--border);
display: flex;
align-items: center;
justify-content: space-between;
background: var(--surface-2);
}
.card-title {
font-family: var(--font-mono);
font-size: 10px;
letter-spacing: 0.14em;
text-transform: uppercase;
color: var(--text-2);
display: flex;
align-items: center;
gap: 7px;
}
.card-tag {
font-family: var(--font-mono);
font-size: 9px;
color: var(--text-3);
letter-spacing: 0.08em;
}
.card-body { padding: 22px; }
/* ── Upload zone ─────────────────────────────────────────── */
.upload-zone {
border: 2px dashed var(--border-hi);
border-radius: var(--radius-lg);
cursor: pointer;
transition: all var(--transition);
overflow: hidden;
position: relative;
}
.upload-zone:hover { border-color: var(--jade); background: var(--jade-dim); }
.upload-zone input[type="file"] { display: none; }
.upload-placeholder {
padding: 36px 20px;
text-align: center;
}
.upload-icon {
width: 60px; height: 60px;
background: var(--bg-2);
border: 1px solid var(--border);
border-radius: 14px;
display: flex; align-items: center; justify-content: center;
margin: 0 auto 12px;
font-size: 24px;
transition: transform var(--transition);
}
.upload-zone:hover .upload-icon { transform: scale(1.1); }
.upload-hint {
font-family: var(--font-mono);
font-size: 10px;
letter-spacing: 0.1em;
text-transform: uppercase;
color: var(--text-3);
line-height: 1.8;
}
.upload-hint b { color: var(--jade); font-weight: 600; }
.scan-container { display: none; position: relative; }
.scanline {
position: absolute; left: 0; right: 0;
height: 2px;
background: var(--jade);
box-shadow: 0 0 10px rgba(13,148,136,0.6);
top: 0;
animation: scan 2.4s ease-in-out infinite;
display: none;
z-index: 10;
}
@keyframes scan {
0% { top: 0%; opacity: 1; }
90% { top: 100%; opacity: 1; }
100% { top: 100%; opacity: 0; }
}
#preview {
width: 100%;
height: 250px;
object-fit: cover;
border-radius: var(--radius);
display: block;
}
/* ── Analyze button ───────────────────────────────────────── */
.analyze-btn {
width: 100%;
margin-top: 16px;
padding: 14px 20px;
background: var(--navy);
color: #ffffff;
border: none;
border-radius: var(--radius-lg);
font-family: var(--font-display);
font-size: 14px;
font-weight: 700;
letter-spacing: 0.03em;
cursor: pointer;
transition: all var(--transition);
display: flex;
align-items: center;
justify-content: center;
gap: 9px;
box-shadow: var(--shadow-sm);
}
.analyze-btn:hover {
background: var(--navy-2);
transform: translateY(-1px);
box-shadow: 0 6px 20px rgba(15,34,64,0.25);
}
.analyze-btn:active { transform: translateY(0); }
.analyze-btn:disabled { opacity: 0.45; cursor: not-allowed; transform: none; box-shadow: none; }
.spinner {
width: 16px; height: 16px;
border: 2px solid rgba(255,255,255,0.25);
border-top-color: #fff;
border-radius: 50%;
animation: spin 0.8s linear infinite;
flex-shrink: 0;
}
@keyframes spin { to { transform: rotate(360deg); } }
.wakeup-hint {
display: none;
font-family: var(--font-mono);
font-size: 10px;
letter-spacing: 0.1em;
text-transform: uppercase;
color: var(--jade);
text-align: center;
margin-top: 10px;
animation: blink 1.4s ease infinite;
}
@keyframes blink { 0%,100% { opacity: 1; } 50% { opacity: 0.35; } }
.error-toast {
display: none;
background: var(--rose-dim);
border: 1px solid var(--rose-border);
border-radius: var(--radius);
padding: 12px 15px;
font-family: var(--font-mono);
font-size: 11px;
color: var(--rose);
margin-top: 12px;
line-height: 1.5;
}
.safety-note {
margin-top: 14px;
background: var(--amber-dim);
border: 1px solid var(--amber-border);
border-radius: var(--radius);
padding: 13px 15px;
display: flex;
gap: 10px;
align-items: flex-start;
}
.safety-icon { font-size: 14px; flex-shrink: 0; margin-top: 1px; }
.safety-text { font-size: 12px; line-height: 1.65; color: var(--text-2); }
.safety-text strong { color: var(--amber); }
/* ── Result panel ────────────────────────────────────────── */
.result-idle {
min-height: 500px;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 14px;
text-align: center;
}
.idle-icon {
width: 68px; height: 68px;
background: var(--bg-2);
border: 1px solid var(--border);
border-radius: 18px;
display: flex; align-items: center; justify-content: center;
font-size: 28px;
opacity: 0.55;
}
.idle-text {
font-family: var(--font-mono);
font-size: 11px;
letter-spacing: 0.14em;
text-transform: uppercase;
color: var(--text-3);
}
.result-content { display: none; }
/* ── TOP SECTION: Dial + Verdict side by side ─────────────── */
/* They sit in their own full-width row with clear separation */
.verdict-zone {
display: flex;
align-items: flex-start;
gap: 28px;
padding: 22px;
background: var(--surface-2);
border-radius: var(--radius-lg);
border: 1px solid var(--border);
margin-bottom: 20px; /* ← gap between top zone and everything below */
flex-wrap: wrap;
}
/* SVG Confidence Dial */
.dial-block {
display: flex;
flex-direction: column;
align-items: center;
gap: 0;
flex-shrink: 0;
}
.dial-wrap {
width: 160px;
height: 90px;
position: relative;
}
.dial-svg { width: 160px; height: 90px; overflow: visible; }
.dial-track {
fill: none;
stroke: var(--bg-2);
stroke-width: 13;
stroke-linecap: round;
}
.dial-fill {
fill: none;
stroke: var(--jade);
stroke-width: 13;
stroke-linecap: round;
stroke-dasharray: 213.6; /* Ο€ Γ— r=68, 180Β° arc */
stroke-dashoffset: 213.6;
transition: stroke-dashoffset 1.4s cubic-bezier(0.34,1.1,0.64,1), stroke 0.4s;
}
.dial-center {
position: absolute; inset: 0;
display: flex; flex-direction: column;
align-items: center; justify-content: flex-end;
padding-bottom: 4px;
}
.dial-pct {
font-family: var(--font-display);
font-size: 30px;
font-weight: 800;
color: var(--navy);
line-height: 1;
letter-spacing: -0.02em;
transition: color 0.4s;
}
/* ── Confidence label sits BELOW the SVG, clearly separated ─ */
.dial-caption {
font-family: var(--font-mono);
font-size: 9px;
letter-spacing: 0.14em;
text-transform: uppercase;
color: var(--text-3);
margin-top: 6px; /* gap between dial number and label */
text-align: center;
}
/* Verdict info */
.verdict-info { flex: 1; min-width: 180px; }
.verdict-badge {
display: inline-flex;
align-items: center;
gap: 6px;
padding: 4px 12px;
border-radius: 99px;
font-family: var(--font-mono);
font-size: 9px;
letter-spacing: 0.12em;
text-transform: uppercase;
font-weight: 600;
margin-bottom: 10px;
border: 1px solid;
}
.verdict-badge.urgent { background: var(--rose-dim); color: var(--rose); border-color: var(--rose-border); }
.verdict-badge.caution { background: var(--amber-dim); color: var(--amber); border-color: var(--amber-border); }
.verdict-badge.normal { background: var(--green-dim); color: var(--green); border-color: var(--green-border); }
.verdict-name {
font-family: var(--font-display);
font-size: clamp(26px, 4vw, 38px);
font-weight: 800;
color: var(--navy);
letter-spacing: -0.03em;
line-height: 1.05;
margin-bottom: 6px;
}
.verdict-code {
font-family: var(--font-mono);
font-size: 11px;
color: var(--jade);
letter-spacing: 0.08em;
text-transform: uppercase;
}
/* ── Description ─────────────────────────────────────────── */
.desc-box {
background: var(--surface-2);
border: 1px solid var(--border);
border-left: 3px solid var(--jade);
border-radius: var(--radius);
padding: 14px 16px;
margin-bottom: 24px; /* ← clear gap below desc before the lower grid */
}
.desc-label {
font-family: var(--font-mono);
font-size: 9px;
letter-spacing: 0.14em;
text-transform: uppercase;
color: var(--jade);
margin-bottom: 7px;
}
.desc-text {
font-size: 13px;
color: var(--text-2);
line-height: 1.75;
}
/* ── Lower 2-col: bars + stats ───────────────────────────── */
.result-lower {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 20px;
}
.bars-label {
font-family: var(--font-mono);
font-size: 9px;
letter-spacing: 0.14em;
text-transform: uppercase;
color: var(--text-3);
margin-bottom: 14px;
}
.prob-bar-item { margin-bottom: 11px; }
.prob-bar-meta {
display: flex;
justify-content: space-between;
font-family: var(--font-mono);
font-size: 10px;
color: var(--text-2);
margin-bottom: 5px;
}
.prob-bar-track {
height: 5px;
background: var(--bg-2);
border-radius: 99px;
overflow: hidden;
}
.prob-bar-fill {
height: 100%;
border-radius: 99px;
background: var(--border-hi);
width: 0%;
transition: width 1.2s cubic-bezier(0.34,1.1,0.64,1);
}
.prob-bar-fill.winner {
background: var(--jade);
}
.prob-bar-fill.danger-bar {
background: var(--rose);
}
.prob-bar-fill.caution-bar {
background: var(--amber);
}
/* Stats + download */
.report-side { display: flex; flex-direction: column; gap: 10px; }
.stat-row {
background: var(--surface-2);
border: 1px solid var(--border);
border-radius: var(--radius);
padding: 11px 14px;
}
.stat-key {
font-family: var(--font-mono);
font-size: 9px;
letter-spacing: 0.12em;
text-transform: uppercase;
color: var(--text-3);
margin-bottom: 4px;
}
.stat-val {
font-family: var(--font-mono);
font-size: 14px;
font-weight: 600;
color: var(--navy);
transition: color 0.3s;
}
.download-btn {
width: 100%;
padding: 11px 16px;
background: transparent;
border: 1px solid var(--border-hi);
border-radius: var(--radius);
font-family: var(--font-mono);
font-size: 10px;
letter-spacing: 0.1em;
text-transform: uppercase;
color: var(--text-2);
cursor: pointer;
transition: all var(--transition);
display: flex;
align-items: center;
justify-content: center;
gap: 7px;
margin-top: auto;
}
.download-btn:hover {
background: var(--navy);
border-color: var(--navy);
color: #fff;
}
/* ── ISIC Reference strip ────────────────────────────────── */
.ref-strip {
margin-top: 28px;
animation: fade-up 0.5s 0.25s ease both;
}
.ref-strip-title {
font-family: var(--font-mono);
font-size: 9px;
letter-spacing: 0.2em;
text-transform: uppercase;
color: var(--text-3);
text-align: center;
margin-bottom: 13px;
}
.ref-grid {
display: grid;
grid-template-columns: repeat(7, 1fr);
gap: 8px;
}
.ref-chip {
background: var(--surface);
border: 1px solid var(--border);
border-radius: var(--radius);
padding: 10px 6px;
text-align: center;
transition: all var(--transition);
cursor: default;
box-shadow: var(--shadow-sm);
}
.ref-chip:hover { box-shadow: var(--shadow-md); transform: translateY(-2px); }
.ref-code {
font-family: var(--font-mono);
font-size: 12px;
font-weight: 600;
color: var(--jade);
margin-bottom: 4px;
}
.ref-name { font-size: 9px; color: var(--text-3); line-height: 1.4; }
.ref-chip.danger .ref-code { color: var(--rose); }
.ref-chip.danger { border-color: var(--rose-border); background: var(--rose-dim); }
.ref-chip.caution .ref-code { color: var(--amber); }
.ref-chip.caution { border-color: var(--amber-border); background: var(--amber-dim); }
/* ── Footer ──────────────────────────────────────────────── */
.footer {
position: relative; z-index: 1;
border-top: 1px solid var(--border);
padding: 28px 24px 44px;
text-align: center;
margin-top: 52px;
}
.footer-name {
font-family: var(--font-display);
font-size: 14px;
font-weight: 600;
color: var(--text-2);
margin-bottom: 16px;
}
.footer-links {
display: flex;
justify-content: center;
gap: 20px;
flex-wrap: wrap;
margin-bottom: 18px;
}
.footer-link {
font-family: var(--font-mono);
font-size: 10px;
letter-spacing: 0.1em;
text-transform: uppercase;
color: var(--text-3);
text-decoration: none;
display: flex;
align-items: center;
gap: 6px;
transition: color var(--transition);
}
.footer-link:hover { color: var(--jade); }
.footer-disclaimer {
font-size: 11px;
color: var(--text-3);
max-width: 560px;
margin: 0 auto;
line-height: 1.75;
}
/* ── Animations ──────────────────────────────────────────── */
@keyframes fade-up {
from { opacity: 0; transform: translateY(16px); }
to { opacity: 1; transform: translateY(0); }
}
.hero { animation: fade-up 0.45s ease both; }
.main-grid { animation: fade-up 0.45s 0.12s ease both; }
::-webkit-scrollbar { width: 5px; }
::-webkit-scrollbar-track { background: transparent; }
::-webkit-scrollbar-thumb { background: var(--border-hi); border-radius: 3px; }
/* ── Responsive ──────────────────────────────────────────── */
@media (max-width: 980px) {
.main-grid { grid-template-columns: 1fr; }
.result-lower { grid-template-columns: 1fr; }
.ref-grid { grid-template-columns: repeat(4, 1fr); }
}
@media (max-width: 640px) {
.nav { padding: 0 16px; }
.page { padding: 24px 14px 56px; }
.verdict-zone { flex-direction: column; gap: 20px; }
.nav-links .nav-link:not(.primary) { display: none; }
.ref-grid { grid-template-columns: repeat(4, 1fr); }
}
@media (max-width: 400px) {
.ref-grid { grid-template-columns: repeat(3, 1fr); }
.result-lower { grid-template-columns: 1fr; }
}
</style>
</head>
<body>
<!-- ── Nav ─────────────────────────────────────────────────────── -->
<nav class="nav">
<div class="nav-brand">
<div class="brand-mark">D</div>
<div class="brand-text-wrap">
<div class="brand-name">DermSight <span>PRO</span></div>
<div class="brand-sub">ResNet-50 Β· HAM10000 Β· 7 Classes</div>
</div>
</div>
<div class="nav-links">
<a href="https://github.com/nilotpaldhar2004/DermSight-AI-Deep-Learning-for-Skin-Lesion-Classification" target="_blank" class="nav-link">Source</a>
<a href="https://www.linkedin.com/in/nilotpal-dhar-24b304294" target="_blank" class="nav-link primary">LinkedIn</a>
</div>
</nav>
<!-- ── Page ────────────────────────────────────────────────────── -->
<div class="page">
<!-- Hero -->
<div class="hero">
<div>
<span class="hero-eyebrow">
<span class="hero-eyebrow-dot"></span>
Deep Residual Learning Β· Dermatology
</span>
</div>
<h1 class="hero-title">Skin Lesion <em>Classification</em></h1>
<p class="hero-sub">Upload a dermoscopic image β€” the ResNet-50 model returns a probability distribution across all 7 ISIC diagnostic categories in seconds.</p>
</div>
<!-- Main grid -->
<div class="main-grid">
<!-- Left: Upload -->
<div>
<div class="card">
<div class="card-header">
<div class="card-title">πŸ”¬ Image Input</div>
<div class="card-tag">DERMOSCOPY Β· RGB</div>
</div>
<div class="card-body">
<div class="upload-zone" onclick="document.getElementById('imageInput').click()">
<input type="file" id="imageInput" accept="image/*" onchange="previewImage(event)" />
<div class="upload-placeholder" id="uploadPlaceholder">
<div class="upload-icon">🩺</div>
<div class="upload-hint">
<b>Tap to select</b> dermoscopic image<br>
JPG Β· PNG Β· TIFF supported
</div>
</div>
<div class="scan-container" id="scanContainer">
<div class="scanline" id="scanline"></div>
<img id="preview" alt="Preview" />
</div>
</div>
<button class="analyze-btn" id="analyzeBtn" onclick="runAnalysis()">
<span id="btnText">Run Classification</span>
<div class="spinner" id="spinner" style="display:none"></div>
</button>
<div class="wakeup-hint" id="wakeupHint">
Cloud engine initializing β€” est. 30s on first load
</div>
<div class="error-toast" id="errorToast"></div>
<div class="safety-note">
<div class="safety-icon">⚠️</div>
<div class="safety-text">
<strong>Research Use Only.</strong> This tool uses deep learning to analyze dermoscopic images. It is <strong>not a medical diagnostic device</strong> and cannot replace a qualified dermatologist. Always consult a physician.
</div>
</div>
</div>
</div>
</div>
<!-- Right: Results -->
<div>
<div class="card">
<div class="card-header">
<div class="card-title">🧬 Classification Output</div>
<div class="card-tag" id="resultTag">AWAITING INPUT</div>
</div>
<div class="card-body">
<!-- Idle state -->
<div class="result-idle" id="resultIdle">
<div class="idle-icon">πŸ”­</div>
<div class="idle-text">System idling β€” awaiting image input</div>
</div>
<!-- Result -->
<div class="result-content" id="resultContent">
<!--
TOP ZONE: confidence dial + verdict text.
This whole block has margin-bottom: 20px before anything below it,
so "49.6% Confidence" is completely separate from the probability bars.
-->
<div class="verdict-zone">
<!-- Dial block: SVG arc + "Confidence" caption underneath -->
<div class="dial-block">
<div class="dial-wrap">
<!--
Arc: M 18 82 A 68 68 0 0 1 222 82
This draws a 180Β° semicircle of radius 68.
Arc length = Ο€ Γ— 68 β‰ˆ 213.6
-->
<svg class="dial-svg" viewBox="0 0 240 96">
<path class="dial-track" d="M 18 82 A 68 68 0 0 1 222 82" />
<path class="dial-fill" id="dialFill" d="M 18 82 A 68 68 0 0 1 222 82" />
</svg>
<div class="dial-center">
<div class="dial-pct" id="dialPct">β€”</div>
</div>
</div>
<!-- "Confidence" label sits BELOW the dial with margin-top gap -->
<div class="dial-caption">Confidence</div>
</div>
<!-- Verdict text: badge, prediction name, risk code -->
<div class="verdict-info">
<div class="verdict-badge normal" id="verdictBadge">
<span>●</span>
<span id="badgeText">Benign</span>
</div>
<div class="verdict-name" id="verdictName">β€”</div>
<div class="verdict-code" id="verdictCode">β€”</div>
</div>
</div>
<!-- END verdict-zone β€” 20px margin-bottom separates it from desc-box -->
<!-- Clinical description box -->
<div class="desc-box">
<div class="desc-label">// Clinical Description</div>
<div class="desc-text" id="descText">β€”</div>
</div>
<!-- END desc-box β€” 24px margin-bottom separates it from lower grid -->
<!-- Lower: probability bars + stats -->
<div class="result-lower">
<div>
<div class="bars-label">Probability Distribution Β· All Classes</div>
<div id="probBars"></div>
</div>
<div class="report-side">
<div class="stat-row">
<div class="stat-key">Primary Prediction</div>
<div class="stat-val" id="statPred">β€”</div>
</div>
<div class="stat-row">
<div class="stat-key">Confidence Score</div>
<div class="stat-val" id="statConf">β€”</div>
</div>
<div class="stat-row">
<div class="stat-key">Risk Category</div>
<div class="stat-val" id="statRisk">β€”</div>
</div>
<div class="stat-row">
<div class="stat-key">Model Architecture</div>
<div class="stat-val" style="color:var(--jade)">ResNet-50</div>
</div>
<button class="download-btn" onclick="downloadReport()">
↓ &nbsp;Export Analysis Report (.txt)
</button>
</div>
</div>
</div><!-- end result-content -->
</div><!-- end card-body -->
</div><!-- end card -->
</div>
</div><!-- end main-grid -->
<!-- ISIC reference strip -->
<div class="ref-strip">
<div class="ref-strip-title">ISIC Diagnostic Categories Β· HAM10000 Dataset</div>
<div class="ref-grid">
<div class="ref-chip danger">
<div class="ref-code">MEL</div>
<div class="ref-name">Melanoma</div>
</div>
<div class="ref-chip danger">
<div class="ref-code">BCC</div>
<div class="ref-name">Basal Cell Carcinoma</div>
</div>
<div class="ref-chip caution">
<div class="ref-code">AKIEC</div>
<div class="ref-name">Actinic Keratoses</div>
</div>
<div class="ref-chip">
<div class="ref-code">BKL</div>
<div class="ref-name">Benign Keratosis</div>
</div>
<div class="ref-chip">
<div class="ref-code">NV</div>
<div class="ref-name">Melanocytic Nevi</div>
</div>
<div class="ref-chip">
<div class="ref-code">DF</div>
<div class="ref-name">Dermatofibroma</div>
</div>
<div class="ref-chip">
<div class="ref-code">VASC</div>
<div class="ref-name">Vascular Lesions</div>
</div>
</div>
</div>
</div><!-- end page -->
<!-- Footer -->
<footer class="footer">
<div class="footer-name">Developed by Nilotpal Dhar Β· 2026</div>
<div class="footer-links">
<a class="footer-link" href="https://github.com/nilotpaldhar2004" target="_blank">
<svg width="13" height="13" fill="currentColor" viewBox="0 0 24 24"><path d="M12 .297c-6.63 0-12 5.373-12 12 0 5.303 3.438 9.8 8.205 11.385.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729 1.205.084 1.838 1.236 1.838 1.236 1.07 1.835 2.809 1.305 3.495.998.108-.776.417-1.305.76-1.605-2.665-.3-5.466-1.332-5.466-5.93 0-1.31.465-2.38 1.235-3.22-.135-.303-.54-1.523.105-3.176 0 0 1.005-.322 3.3 1.23.96-.267 1.98-.399 3-.405 1.02.006 2.04.138 3 .405 2.28-1.552 3.285-1.23 3.285-1.23.645 1.653.24 2.873.12 3.176.765.84 1.23 1.91 1.23 3.22 0 4.61-2.805 5.625-5.475 5.92.42.36.81 1.096.81 2.22 0 1.606-.015 2.896-.015 3.286 0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12"/></svg>
GitHub
</a>
<a class="footer-link" href="https://www.linkedin.com/in/nilotpal-dhar-24b304294" target="_blank">
<svg width="13" height="13" fill="currentColor" viewBox="0 0 24 24"><path d="M19 0h-14c-2.761 0-5 2.239-5 5v14c0 2.761 2.239 5 5 5h14c2.762 0 5-2.239 5-5v-14c0-2.761-2.238-5-5-5zm-11 19h-3v-11h3v11zm-1.5-12.268c-.966 0-1.75-.79-1.75-1.764s.784-1.764 1.75-1.764 1.75.79 1.75 1.764-.783 1.764-1.75 1.764zm13.5 12.268h-3v-5.604c0-3.368-4-3.113-4 0v5.604h-3v-11h3v1.765c1.396-2.586 7-2.777 7 2.476v6.759z"/></svg>
LinkedIn
</a>
<a class="footer-link" href="https://github.com/nilotpaldhar2004/DermSight-AI-Deep-Learning-for-Skin-Lesion-Classification" target="_blank">
<svg width="13" height="13" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14"/></svg>
Repository
</a>
</div>
<div class="footer-disclaimer">
βš•οΈ For research and educational use only. Not a substitute for professional medical advice, diagnosis, or treatment. All predictions must be verified by a qualified dermatologist.
</div>
</footer>
<script>
// ── Config ────────────────────────────────────────────────────
const RENDER_URL = '';
// Arc path: M 18 82 A 68 68 0 0 1 222 82
// Arc length = Ο€ Γ— r = Ο€ Γ— 68 β‰ˆ 213.6
const DIAL_LEN = 213.6;
let currentAnalysis = null;
// ── Clinical descriptions ─────────────────────────────────────
const descriptions = {
nv: 'Melanocytic Nevi: A benign proliferation of melanocytes. Common in adults; regular monitoring is recommended to detect changes in size or morphology.',
mel: 'Melanoma: A malignant tumor arising from melanocytic cells with high metastatic potential. Immediate surgical consultation and biopsy are required.',
bkl: 'Benign Keratosis: Includes seborrheic keratoses and lichen planus-like keratoses. Generally non-cancerous lesions of the outer skin layers.',
bcc: 'Basal Cell Carcinoma: The most common form of skin cancer. Slow-growing and locally invasive, but rarely metastatic. Surgical excision is standard care.',
akiec: 'Actinic Keratoses / Intraepithelial Carcinoma: Pre-cancerous lesions induced by sun damage. Can progress to squamous cell carcinoma if untreated.',
vasc: 'Vascular Lesions: Includes angiomas, angiokeratomas, and pyogenic granulomas. Typically benign blood-vessel proliferations.',
df: 'Dermatofibroma: A benign fibrous nodule usually arising on the lower limbs, often following minor skin trauma. Low recurrence after excision.',
};
// ── Risk map ──────────────────────────────────────────────────
const riskMap = {
mel: { level: 'Malignant', cls: 'urgent', risk: 'HIGH', barCls: 'danger-bar' },
bcc: { level: 'Malignant', cls: 'urgent', risk: 'HIGH', barCls: 'danger-bar' },
akiec: { level: 'Pre-cancerous', cls: 'caution', risk: 'MEDIUM', barCls: 'caution-bar' },
bkl: { level: 'Benign', cls: 'normal', risk: 'LOW', barCls: '' },
nv: { level: 'Benign', cls: 'normal', risk: 'LOW', barCls: '' },
df: { level: 'Benign', cls: 'normal', risk: 'LOW', barCls: '' },
vasc: { level: 'Benign', cls: 'normal', risk: 'LOW', barCls: '' },
};
// ── Preview image ─────────────────────────────────────────────
function previewImage(event) {
if (!event.target.files[0]) return;
document.getElementById('preview').src = URL.createObjectURL(event.target.files[0]);
document.getElementById('uploadPlaceholder').style.display = 'none';
document.getElementById('scanContainer').style.display = 'block';
document.getElementById('errorToast').style.display = 'none';
}
// ── Busy state ────────────────────────────────────────────────
function setBusy(busy) {
const btn = document.getElementById('analyzeBtn');
const spinner = document.getElementById('spinner');
const hint = document.getElementById('wakeupHint');
const scanline = document.getElementById('scanline');
btn.disabled = busy;
spinner.style.display = busy ? 'block' : 'none';
hint.style.display = busy ? 'block' : 'none';
scanline.style.display = busy ? 'block' : 'none';
document.getElementById('btnText').textContent = busy ? 'Analyzing…' : 'Run Classification';
}
// ── Dial update ───────────────────────────────────────────────
function setDial(pct, riskCls) {
const fill = document.getElementById('dialFill');
const pctEl = document.getElementById('dialPct');
const offset = DIAL_LEN - (pct / 100) * DIAL_LEN;
const color =
riskCls === 'urgent' ? 'var(--rose)' :
riskCls === 'caution' ? 'var(--amber)' :
'var(--jade)';
fill.style.strokeDashoffset = offset;
fill.style.stroke = color;
pctEl.textContent = pct.toFixed(1) + '%';
pctEl.style.color = color;
}
// ── Show result ───────────────────────────────────────────────
function showResult(data) {
const pred = data.prediction.toLowerCase();
const conf = parseFloat(data.confidence);
const risk = riskMap[pred] || { level: 'Unknown', cls: 'normal', risk: 'LOW', barCls: '' };
const desc = descriptions[pred] || 'Classification complete.';
// 1. RISK OVERRIDE LOGIC
// If the combined probability of MEL + BCC is high, we elevate the warning even if NV is the winner.
const malignantProb = (data.all_probabilities['mel'] || 0) + (data.all_probabilities['bcc'] || 0);
let displayRiskLevel = risk.risk;
let displayRiskCls = risk.cls;
let displayBadgeText = risk.level;
if (malignantProb > 20 && pred === 'nv') {
displayRiskLevel = 'ELEVATED';
displayRiskCls = 'caution';
displayBadgeText = 'Review Required';
}
// 2. Update UI Elements
setDial(conf, displayRiskCls);
const badge = document.getElementById('verdictBadge');
badge.className = `verdict-badge ${displayRiskCls}`;
document.getElementById('badgeText').textContent = displayBadgeText;
document.getElementById('verdictName').textContent = data.prediction.toUpperCase();
document.getElementById('verdictCode').textContent = `/ ${pred.toUpperCase()} Β· ${displayRiskLevel} RISK`;
document.getElementById('descText').textContent = desc;
document.getElementById('statPred').textContent = data.prediction.toUpperCase();
document.getElementById('statConf').textContent = conf.toFixed(1) + '%';
const riskEl = document.getElementById('statRisk');
riskEl.textContent = displayRiskLevel;
riskEl.style.color = displayRiskCls === 'urgent' ? 'var(--rose)' : (displayRiskCls === 'caution' ? 'var(--amber)' : 'var(--green)');
document.getElementById('resultTag').textContent = displayRiskCls === 'urgent' ? '⚠ PRIORITY' : (displayRiskCls === 'caution' ? '⚠ REVIEW' : 'βœ“ ROUTINE');
// 3. Probability bars (Sorted)
const container = document.getElementById('probBars');
container.innerHTML = '';
const sorted = Object.entries(data.all_probabilities).sort(([,a],[,b]) => b - a);
sorted.forEach(([cls, val]) => {
const pctVal = parseFloat(val).toFixed(1);
const isWin = cls.toLowerCase() === pred;
const winRisk = riskMap[cls.toLowerCase()] || {};
const barExtraCls = isWin ? ` winner` : (winRisk.barCls ? ` ${winRisk.barCls}` : '');
const item = document.createElement('div');
item.className = 'prob-bar-item';
item.innerHTML = `
<div class="prob-bar-meta">
<span style="font-weight:${isWin ? '600' : '400'};color:${isWin ? 'var(--navy)' : 'var(--text-2)'}">${cls.toUpperCase()}</span>
<span>${pctVal}%</span>
</div>
<div class="prob-bar-track">
<div class="prob-bar-fill${barExtraCls}" style="width:0%" data-width="${pctVal}%"></div>
</div>`;
container.appendChild(item);
});
document.getElementById('resultIdle').style.display = 'none';
document.getElementById('resultContent').style.display = 'block';
requestAnimationFrame(() => {
requestAnimationFrame(() => {
container.querySelectorAll('.prob-bar-fill').forEach(el => { el.style.width = el.dataset.width; });
});
});
}
// ── Show error ────────────────────────────────────────────────
function showError(msg) {
const toast = document.getElementById('errorToast');
toast.style.display = 'block';
toast.textContent = '⚠ ' + msg;
}
// ── Run analysis ──────────────────────────────────────────────
async function runAnalysis() {
const input = document.getElementById('imageInput');
if (!input.files[0]) {
showError('Please select a dermoscopic image first.');
return;
}
setBusy(true);
document.getElementById('errorToast').style.display = 'none';
const formData = new FormData();
formData.append('file', input.files[0]);
try {
const res = await fetch(`${RENDER_URL}/predict`, {
method: 'POST',
body: formData,
});
if (!res.ok) throw new Error(`Server error: HTTP ${res.status}`);
const data = await res.json();
currentAnalysis = data;
showResult(data);
} catch (err) {
showError(
err.message.includes('Failed to fetch')
? 'Cannot reach the inference server. The engine may be cold-starting β€” please wait 30 seconds and try again.'
: err.message
);
} finally {
setBusy(false);
}
}
// ── Download report ───────────────────────────────────────────
function downloadReport() {
if (!currentAnalysis) return;
const pred = currentAnalysis.prediction.toLowerCase();
const risk = riskMap[pred] || { level: 'Unknown', risk: 'UNKNOWN' };
const desc = descriptions[pred] || 'N/A';
const lines = [
'DERMSIGHT PRO β€” ANALYSIS REPORT',
`Generated : ${new Date().toLocaleString()}`,
'═'.repeat(52),
'',
`Primary Prediction : ${currentAnalysis.prediction.toUpperCase()}`,
`Confidence Score : ${parseFloat(currentAnalysis.confidence).toFixed(2)}%`,
`Risk Category : ${risk.risk}`,
`Classification Type : ${risk.level}`,
'',
'Probability Distribution (sorted by confidence):',
...Object.entries(currentAnalysis.all_probabilities)
.sort(([,a],[,b]) => parseFloat(b) - parseFloat(a))
.map(([k,v]) => ` ${k.toUpperCase().padEnd(8)} : ${parseFloat(v).toFixed(2)}%`),
'',
'Clinical Description:',
` ${desc}`,
'',
'═'.repeat(52),
'Model : ResNet-50 (fine-tuned on HAM10000)',
'Dataset : HAM10000 β€” 10,015 dermoscopic images',
'Categories : 7 ISIC diagnostic classes',
'',
'βš• DISCLAIMER: Research and educational use only.',
' Not a medical diagnosis. Consult a qualified',
' dermatologist for clinical assessment.',
'',
'Developed by Nilotpal Dhar',
'https://github.com/nilotpaldhar2004',
].join('\n');
const blob = new Blob([lines], { type: 'text/plain' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `dermsight_report_${Date.now()}.txt`;
a.click();
URL.revokeObjectURL(url);
}
</script>
</body>
</html>