| <!DOCTYPE html> |
| <html lang="en"> |
| <head> |
| <meta charset="UTF-8" /> |
| <meta name="viewport" content="width=device-width, initial-scale=1.0" /> |
| <title>SepsisPilot β ICU Decision Engine</title> |
| <link rel="preconnect" href="https://fonts.googleapis.com" /> |
| <link href="https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:wght@400;600;700&family=Inter:wght@300;400;500;600&display=swap" rel="stylesheet" /> |
| <style> |
| |
| |
| |
| :root { |
| --bg: #050810; |
| --bg2: #080d1a; |
| --surface: #0d1526; |
| --surface2: #111e35; |
| --border: #1a2d4a; |
| --border2: #243d61; |
| |
| --accent: #00e5b0; |
| --accent-dim: rgba(0,229,176,0.12); |
| --red: #ff4c6a; |
| --red-dim: rgba(255,76,106,0.12); |
| --amber: #ffb547; |
| --amber-dim: rgba(255,181,71,0.12); |
| --blue: #4d9fff; |
| --blue-dim: rgba(77,159,255,0.12); |
| |
| --text: #dce8f5; |
| --text2: #8ca3c0; |
| --text3: #4a6480; |
| |
| --mono: 'IBM Plex Mono', monospace; |
| --sans: 'Inter', sans-serif; |
| |
| --radius: 10px; |
| --glow-green: 0 0 20px rgba(0,229,176,0.15); |
| --glow-red: 0 0 20px rgba(255,76,106,0.15); |
| } |
| |
| *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; } |
| |
| body { |
| background: var(--bg); |
| color: var(--text); |
| font-family: var(--sans); |
| font-size: 14px; |
| min-height: 100vh; |
| overflow-x: hidden; |
| } |
| |
| |
| body::before { |
| content: ''; |
| position: fixed; |
| inset: 0; |
| background-image: |
| linear-gradient(rgba(0,229,176,0.02) 1px, transparent 1px), |
| linear-gradient(90deg, rgba(0,229,176,0.02) 1px, transparent 1px); |
| background-size: 40px 40px; |
| pointer-events: none; |
| z-index: 0; |
| } |
| |
| |
| |
| |
| .app { position: relative; z-index: 1; display: flex; flex-direction: column; min-height: 100vh; } |
| |
| header { |
| display: flex; |
| align-items: center; |
| justify-content: space-between; |
| padding: 16px 28px; |
| border-bottom: 1px solid var(--border); |
| background: rgba(8,13,26,0.9); |
| backdrop-filter: blur(8px); |
| position: sticky; top: 0; z-index: 100; |
| } |
| |
| .logo-group { display: flex; align-items: center; gap: 14px; } |
| .logo-icon { |
| width: 38px; height: 38px; |
| background: linear-gradient(135deg, rgba(0,229,176,0.2), rgba(0,229,176,0.05)); |
| border: 1px solid rgba(0,229,176,0.3); |
| border-radius: 10px; |
| display: flex; align-items: center; justify-content: center; |
| font-size: 18px; |
| box-shadow: var(--glow-green); |
| } |
| .logo-text { font-family: var(--mono); font-size: 17px; font-weight: 700; color: var(--accent); letter-spacing: -0.5px; } |
| .logo-sub { font-size: 11px; color: var(--text3); margin-top: 1px; } |
| |
| .header-right { display: flex; align-items: center; gap: 16px; } |
| .badge { |
| font-family: var(--mono); |
| font-size: 10px; |
| padding: 4px 9px; |
| border-radius: 5px; |
| background: var(--accent-dim); |
| border: 1px solid rgba(0,229,176,0.25); |
| color: var(--accent); |
| letter-spacing: 0.5px; |
| } |
| .server-status { |
| display: flex; align-items: center; gap: 6px; |
| font-size: 11px; color: var(--text3); |
| } |
| .dot { |
| width: 7px; height: 7px; border-radius: 50%; |
| background: var(--red); |
| } |
| .dot.live { background: var(--accent); animation: pulse 2s infinite; } |
| |
| .main { display: grid; grid-template-columns: 280px 1fr 300px; gap: 0; flex: 1; } |
| |
| |
| |
| |
| .panel { padding: 20px; border-right: 1px solid var(--border); } |
| .panel:last-child { border-right: none; } |
| |
| .panel-title { |
| font-family: var(--mono); |
| font-size: 9px; |
| letter-spacing: 2px; |
| text-transform: uppercase; |
| color: var(--text3); |
| margin-bottom: 18px; |
| padding-bottom: 10px; |
| border-bottom: 1px solid var(--border); |
| } |
| |
| |
| |
| |
| .left-panel { border-right: 1px solid var(--border); } |
| |
| .task-card { |
| border: 1px solid var(--border); |
| border-radius: var(--radius); |
| padding: 14px; |
| cursor: pointer; |
| margin-bottom: 8px; |
| transition: all 0.18s; |
| position: relative; |
| overflow: hidden; |
| } |
| .task-card::before { |
| content: ''; |
| position: absolute; |
| left: 0; top: 0; bottom: 0; |
| width: 3px; |
| border-radius: 2px 0 0 2px; |
| } |
| .task-card.easy::before { background: var(--accent); } |
| .task-card.medium::before { background: var(--amber); } |
| .task-card.hard::before { background: var(--red); } |
| |
| .task-card:hover { border-color: var(--border2); background: rgba(255,255,255,0.02); } |
| .task-card.active { |
| border-color: rgba(0,229,176,0.35); |
| background: var(--accent-dim); |
| box-shadow: var(--glow-green); |
| } |
| .task-card.active.medium { border-color: rgba(255,181,71,0.35); background: var(--amber-dim); } |
| .task-card.active.hard { border-color: rgba(255,76,106,0.35); background: var(--red-dim); } |
| |
| .task-name { font-weight: 600; font-size: 13px; margin-bottom: 4px; } |
| .task-desc { font-size: 11px; color: var(--text2); line-height: 1.45; } |
| .task-diff { |
| display: inline-block; |
| font-family: var(--mono); |
| font-size: 9px; |
| padding: 2px 7px; |
| border-radius: 4px; |
| margin-bottom: 6px; |
| text-transform: uppercase; |
| letter-spacing: 1px; |
| } |
| .easy-badge { background: rgba(0,229,176,0.1); color: var(--accent); } |
| .medium-badge { background: rgba(255,181,71,0.1); color: var(--amber); } |
| .hard-badge { background: rgba(255,76,106,0.1); color: var(--red); } |
| |
| .ctrl-section { margin-top: 20px; } |
| |
| .btn { |
| width: 100%; |
| padding: 11px 16px; |
| border-radius: var(--radius); |
| border: none; |
| font-family: var(--sans); |
| font-size: 13px; |
| font-weight: 600; |
| cursor: pointer; |
| transition: all 0.15s; |
| letter-spacing: 0.2px; |
| } |
| .btn-primary { |
| background: var(--accent); |
| color: #021a12; |
| margin-bottom: 8px; |
| } |
| .btn-primary:hover { filter: brightness(1.1); box-shadow: var(--glow-green); } |
| .btn-secondary { |
| background: transparent; |
| color: var(--text2); |
| border: 1px solid var(--border2); |
| } |
| .btn-secondary:hover { border-color: var(--accent); color: var(--accent); } |
| .btn:disabled { opacity: 0.4; cursor: not-allowed; } |
| |
| .seed-row { display: flex; gap: 8px; margin-bottom: 8px; } |
| .seed-input { |
| flex: 1; |
| background: var(--surface); |
| border: 1px solid var(--border2); |
| border-radius: 8px; |
| padding: 9px 12px; |
| color: var(--text); |
| font-family: var(--mono); |
| font-size: 13px; |
| } |
| .seed-input:focus { outline: none; border-color: var(--accent); } |
| .seed-label { font-size: 11px; color: var(--text3); margin-bottom: 6px; } |
| |
| |
| .stat-row { |
| display: flex; justify-content: space-between; align-items: center; |
| padding: 8px 0; |
| border-bottom: 1px solid var(--border); |
| } |
| .stat-row:last-child { border-bottom: none; } |
| .stat-label { font-size: 11px; color: var(--text3); } |
| .stat-value { font-family: var(--mono); font-size: 13px; font-weight: 600; } |
| .stat-value.green { color: var(--accent); } |
| .stat-value.red { color: var(--red); } |
| .stat-value.amber { color: var(--amber); } |
| |
| |
| |
| |
| .center-panel { padding: 20px 24px; border-right: 1px solid var(--border); } |
| |
| |
| .patient-banner { |
| display: flex; |
| align-items: center; |
| gap: 16px; |
| padding: 14px 18px; |
| border-radius: var(--radius); |
| background: var(--surface); |
| border: 1px solid var(--border); |
| margin-bottom: 20px; |
| } |
| .patient-id { font-family: var(--mono); font-size: 13px; font-weight: 700; color: var(--accent); } |
| .patient-meta { font-size: 11px; color: var(--text3); } |
| .status-pill { |
| margin-left: auto; |
| padding: 5px 14px; |
| border-radius: 999px; |
| font-family: var(--mono); |
| font-size: 11px; |
| font-weight: 700; |
| letter-spacing: 0.5px; |
| } |
| .pill-idle { background: var(--surface2); color: var(--text3); border: 1px solid var(--border2); } |
| .pill-active { background: var(--accent-dim); color: var(--accent); border: 1px solid rgba(0,229,176,0.3); } |
| .pill-stable { background: rgba(0,229,176,0.15); color: var(--accent); border: 1px solid rgba(0,229,176,0.4); } |
| .pill-dead { background: var(--red-dim); color: var(--red); border: 1px solid rgba(255,76,106,0.4); } |
| |
| |
| .vitals-grid { |
| display: grid; |
| grid-template-columns: repeat(3, 1fr); |
| gap: 10px; |
| margin-bottom: 20px; |
| } |
| |
| .vital-card { |
| background: var(--surface); |
| border: 1px solid var(--border); |
| border-radius: var(--radius); |
| padding: 14px 16px; |
| transition: border-color 0.3s; |
| position: relative; |
| overflow: hidden; |
| } |
| .vital-card::after { |
| content: ''; |
| position: absolute; |
| bottom: 0; left: 0; right: 0; |
| height: 2px; |
| border-radius: 0 0 var(--radius) var(--radius); |
| transition: background 0.3s; |
| } |
| .vital-card.ok { border-color: rgba(0,229,176,0.2); } |
| .vital-card.ok::after { background: var(--accent); } |
| .vital-card.warn { border-color: rgba(255,181,71,0.25); } |
| .vital-card.warn::after { background: var(--amber); } |
| .vital-card.crit { border-color: rgba(255,76,106,0.3); } |
| .vital-card.crit::after { background: var(--red); } |
| |
| .vital-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 8px; } |
| .vital-label { font-size: 10px; color: var(--text3); letter-spacing: 0.5px; text-transform: uppercase; } |
| .vital-target { font-size: 9px; color: var(--text3); font-family: var(--mono); } |
| |
| .vital-value { font-family: var(--mono); font-size: 26px; font-weight: 700; line-height: 1; } |
| .vital-value.ok { color: var(--accent); } |
| .vital-value.warn { color: var(--amber); } |
| .vital-value.crit { color: var(--red); } |
| .vital-unit { font-size: 10px; color: var(--text3); margin-top: 3px; } |
| |
| .vital-bar { height: 3px; background: var(--border); border-radius: 2px; margin-top: 10px; } |
| .vital-fill { height: 3px; border-radius: 2px; transition: width 0.5s ease; } |
| .fill-ok { background: var(--accent); } |
| .fill-warn { background: var(--amber); } |
| .fill-crit { background: var(--red); } |
| |
| .vital-delta { |
| font-family: var(--mono); |
| font-size: 10px; |
| margin-top: 4px; |
| } |
| .delta-up { color: var(--red); } |
| .delta-down { color: var(--accent); } |
| .delta-flat { color: var(--text3); } |
| |
| |
| .waveform-section { margin-bottom: 20px; } |
| .waveform-label { |
| font-family: var(--mono); |
| font-size: 9px; |
| color: var(--text3); |
| letter-spacing: 2px; |
| text-transform: uppercase; |
| margin-bottom: 8px; |
| } |
| .waveform-wrap { |
| background: var(--surface); |
| border: 1px solid var(--border); |
| border-radius: var(--radius); |
| padding: 12px; |
| } |
| canvas { display: block; width: 100%; } |
| |
| |
| .sofa-section { |
| display: grid; |
| grid-template-columns: 1fr 1fr; |
| gap: 10px; |
| margin-bottom: 20px; |
| } |
| .sofa-card, .ep-reward-card { |
| background: var(--surface); |
| border: 1px solid var(--border); |
| border-radius: var(--radius); |
| padding: 14px; |
| } |
| .mini-label { font-size: 10px; color: var(--text3); text-transform: uppercase; letter-spacing: 0.5px; margin-bottom: 6px; } |
| .big-num { font-family: var(--mono); font-size: 32px; font-weight: 700; } |
| |
| |
| .actions-section-title { |
| font-family: var(--mono); |
| font-size: 9px; |
| color: var(--text3); |
| letter-spacing: 2px; |
| text-transform: uppercase; |
| margin-bottom: 12px; |
| } |
| .actions-grid { |
| display: grid; |
| grid-template-columns: repeat(3, 1fr); |
| gap: 8px; |
| } |
| .action-btn { |
| background: var(--surface); |
| border: 1px solid var(--border); |
| border-radius: var(--radius); |
| padding: 12px 8px; |
| color: var(--text); |
| cursor: pointer; |
| transition: all 0.15s; |
| text-align: center; |
| font-family: var(--sans); |
| font-size: 11px; |
| line-height: 1.4; |
| } |
| .action-btn:hover { |
| border-color: rgba(0,229,176,0.4); |
| background: var(--accent-dim); |
| color: var(--accent); |
| } |
| .action-btn.fired { |
| border-color: var(--accent); |
| background: var(--accent-dim); |
| color: var(--accent); |
| box-shadow: var(--glow-green); |
| } |
| .action-btn:disabled { opacity: 0.35; cursor: not-allowed; } |
| .action-icon { font-size: 16px; display: block; margin-bottom: 5px; } |
| .action-name { font-weight: 600; font-size: 11px; } |
| .action-desc { font-size: 9px; color: var(--text3); margin-top: 2px; } |
| .action-btn.fired .action-desc { color: rgba(0,229,176,0.6); } |
| |
| |
| |
| |
| .right-panel { padding: 20px; overflow: hidden; display: flex; flex-direction: column; } |
| |
| .score-section { margin-bottom: 20px; } |
| .score-display { |
| font-family: var(--mono); |
| font-size: 42px; |
| font-weight: 700; |
| color: var(--accent); |
| line-height: 1; |
| margin-bottom: 8px; |
| } |
| .score-display.bad { color: var(--red); } |
| .score-sub { font-size: 11px; color: var(--text3); } |
| |
| .score-bar { height: 6px; background: var(--border); border-radius: 3px; margin: 10px 0; } |
| .score-fill { |
| height: 6px; border-radius: 3px; |
| background: linear-gradient(90deg, var(--accent), #00ff9d); |
| transition: width 0.6s ease; |
| box-shadow: 0 0 8px rgba(0,229,176,0.4); |
| } |
| |
| |
| .grader-metrics { margin-bottom: 20px; } |
| .metric-row { |
| display: flex; |
| justify-content: space-between; |
| align-items: center; |
| padding: 7px 0; |
| border-bottom: 1px solid rgba(26,45,74,0.6); |
| font-size: 11px; |
| } |
| .metric-row:last-child { border-bottom: none; } |
| .metric-name { color: var(--text2); } |
| .metric-val { font-family: var(--mono); font-weight: 600; } |
| |
| |
| .log-wrap { flex: 1; min-height: 0; display: flex; flex-direction: column; } |
| .log-box { |
| flex: 1; |
| background: var(--bg2); |
| border: 1px solid var(--border); |
| border-radius: var(--radius); |
| padding: 12px; |
| overflow-y: auto; |
| font-family: var(--mono); |
| font-size: 10px; |
| line-height: 1.7; |
| color: var(--text3); |
| min-height: 180px; |
| max-height: 300px; |
| } |
| .log-box::-webkit-scrollbar { width: 4px; } |
| .log-box::-webkit-scrollbar-thumb { background: var(--border2); border-radius: 2px; } |
| |
| .log-entry { border-bottom: 1px solid rgba(26,45,74,0.4); padding: 3px 0; } |
| .log-step-num { color: var(--blue); } |
| .log-action { color: var(--text2); } |
| .log-reward-pos { color: var(--accent); } |
| .log-reward-neg { color: var(--red); } |
| .log-done { color: var(--amber); } |
| .log-system { color: rgba(0,229,176,0.5); } |
| |
| |
| |
| |
| @keyframes pulse { 0%,100%{opacity:1;transform:scale(1)} 50%{opacity:0.5;transform:scale(0.85)} } |
| @keyframes fadeIn { from{opacity:0;transform:translateY(4px)} to{opacity:1;transform:translateY(0)} } |
| @keyframes slideIn { from{transform:translateX(-8px);opacity:0} to{transform:translateX(0);opacity:1} } |
| |
| .vital-card { animation: fadeIn 0.3s ease; } |
| |
| .flicker { animation: pulse 0.4s ease 2; } |
| |
| |
| |
| |
| @media (max-width: 1100px) { |
| .main { grid-template-columns: 260px 1fr; } |
| .right-panel { display: none; } |
| } |
| @media (max-width: 800px) { |
| .main { grid-template-columns: 1fr; } |
| .left-panel { border-right: none; border-bottom: 1px solid var(--border); } |
| .vitals-grid { grid-template-columns: repeat(2, 1fr); } |
| .actions-grid { grid-template-columns: repeat(2, 1fr); } |
| } |
| </style> |
| </head> |
| <body> |
| <div class="app"> |
|
|
| |
| <header> |
| <div class="logo-group"> |
| <div class="logo-icon">π«</div> |
| <div> |
| <div class="logo-text">SepsisPilot</div> |
| <div class="logo-sub">ICU Treatment Decision Engine</div> |
| </div> |
| </div> |
| <div class="header-right"> |
| <div class="server-status"> |
| <div class="dot" id="server-dot"></div> |
| <span id="server-label">Connectingβ¦</span> |
| </div> |
| <div class="badge">OPENENV v1.0</div> |
| <div class="badge" style="background:var(--blue-dim);border-color:rgba(77,159,255,0.25);color:var(--blue)">HACKATHON 2026</div> |
| </div> |
| </header> |
|
|
| |
| <div class="main"> |
|
|
| |
| <div class="panel left-panel"> |
| <div class="panel-title">Select Scenario</div> |
|
|
| <div class="task-card easy active" id="task-mild" onclick="selectTask('mild_sepsis','easy')"> |
| <span class="task-diff easy-badge">Easy</span> |
| <div class="task-name">Mild Sepsis</div> |
| <div class="task-desc">Gram-negative UTI. Broad-spectrum ABx optimal. 24-step episode.</div> |
| </div> |
|
|
| <div class="task-card medium" id="task-shock" onclick="selectTask('septic_shock','medium')"> |
| <span class="task-diff medium-badge">Medium</span> |
| <div class="task-name">Septic Shock</div> |
| <div class="task-desc">Gram-positive bacteraemia (MRSA). Vasopressors mandatory. 48-step episode.</div> |
| </div> |
|
|
| <div class="task-card hard" id="task-mods" onclick="selectTask('severe_mods','hard')"> |
| <span class="task-diff hard-badge">Hard</span> |
| <div class="task-name">Severe MODS</div> |
| <div class="task-desc">Multi-organ dysfunction. Mixed resistant infection. Precise AB sequencing required. 72-step episode.</div> |
| </div> |
|
|
| <div class="ctrl-section"> |
| <div class="seed-label">Random Seed</div> |
| <div class="seed-row"> |
| <input class="seed-input" type="number" id="seed-input" value="42" min="0" max="9999" /> |
| <button class="btn btn-secondary" onclick="setSeed(Math.floor(Math.random()*9999))" title="Random seed" style="width:44px;padding:9px;">π²</button> |
| </div> |
| <button class="btn btn-primary" onclick="doReset()" id="reset-btn">βΊ Start Episode</button> |
| <button class="btn btn-secondary" onclick="autoPlay()" id="auto-btn">βΆ Auto-Play (Heuristic)</button> |
| </div> |
|
|
| <div style="margin-top:20px"> |
| <div class="panel-title">Episode Stats</div> |
| <div class="stat-row"> |
| <span class="stat-label">Task</span> |
| <span class="stat-value" id="s-task">β</span> |
| </div> |
| <div class="stat-row"> |
| <span class="stat-label">Step</span> |
| <span class="stat-value" id="s-step">β / β</span> |
| </div> |
| <div class="stat-row"> |
| <span class="stat-label">Cumulative Reward</span> |
| <span class="stat-value" id="s-reward">β</span> |
| </div> |
| <div class="stat-row"> |
| <span class="stat-label">Alive</span> |
| <span class="stat-value green" id="s-alive">β</span> |
| </div> |
| <div class="stat-row"> |
| <span class="stat-label">Stabilized at step</span> |
| <span class="stat-value green" id="s-stab">β</span> |
| </div> |
| </div> |
| </div> |
|
|
| |
| <div class="panel center-panel"> |
|
|
| |
| <div class="patient-banner"> |
| <div> |
| <div class="patient-id" id="patient-id">ICU-0000</div> |
| <div class="patient-meta" id="patient-meta">Select a task and press Start Episode</div> |
| </div> |
| <div class="status-pill pill-idle" id="status-pill">IDLE</div> |
| </div> |
|
|
| |
| <div class="vitals-grid"> |
| <div class="vital-card" id="card-map"> |
| <div class="vital-header"> |
| <div class="vital-label">MAP</div> |
| <div class="vital-target">goal β₯ 65</div> |
| </div> |
| <div class="vital-value" id="v-map">β</div> |
| <div class="vital-unit">mmHg</div> |
| <div class="vital-bar"><div class="vital-fill" id="b-map" style="width:0%"></div></div> |
| <div class="vital-delta" id="d-map"></div> |
| </div> |
| <div class="vital-card" id="card-lac"> |
| <div class="vital-header"> |
| <div class="vital-label">Lactate</div> |
| <div class="vital-target">goal < 2.0</div> |
| </div> |
| <div class="vital-value" id="v-lac">β</div> |
| <div class="vital-unit">mmol/L</div> |
| <div class="vital-bar"><div class="vital-fill" id="b-lac" style="width:0%"></div></div> |
| <div class="vital-delta" id="d-lac"></div> |
| </div> |
| <div class="vital-card" id="card-wbc"> |
| <div class="vital-header"> |
| <div class="vital-label">WBC</div> |
| <div class="vital-target">goal 4β12</div> |
| </div> |
| <div class="vital-value" id="v-wbc">β</div> |
| <div class="vital-unit">k/uL</div> |
| <div class="vital-bar"><div class="vital-fill" id="b-wbc" style="width:0%"></div></div> |
| <div class="vital-delta" id="d-wbc"></div> |
| </div> |
| <div class="vital-card" id="card-temp"> |
| <div class="vital-header"> |
| <div class="vital-label">Temperature</div> |
| <div class="vital-target">goal 36β38Β°C</div> |
| </div> |
| <div class="vital-value" id="v-temp">β</div> |
| <div class="vital-unit">Β°C</div> |
| <div class="vital-bar"><div class="vital-fill" id="b-temp" style="width:0%"></div></div> |
| <div class="vital-delta" id="d-temp"></div> |
| </div> |
| <div class="vital-card" id="card-hr"> |
| <div class="vital-header"> |
| <div class="vital-label">Heart Rate</div> |
| <div class="vital-target">goal < 100</div> |
| </div> |
| <div class="vital-value" id="v-hr">β</div> |
| <div class="vital-unit">bpm</div> |
| <div class="vital-bar"><div class="vital-fill" id="b-hr" style="width:0%"></div></div> |
| <div class="vital-delta" id="d-hr"></div> |
| </div> |
| <div class="vital-card" id="card-cr"> |
| <div class="vital-header"> |
| <div class="vital-label">Creatinine</div> |
| <div class="vital-target">goal < 1.2</div> |
| </div> |
| <div class="vital-value" id="v-cr">β</div> |
| <div class="vital-unit">mg/dL</div> |
| <div class="vital-bar"><div class="vital-fill" id="b-cr" style="width:0%"></div></div> |
| <div class="vital-delta" id="d-cr"></div> |
| </div> |
| </div> |
|
|
| |
| <div class="waveform-section"> |
| <div class="waveform-label">MAP Trajectory</div> |
| <div class="waveform-wrap"> |
| <canvas id="waveform-canvas" height="70"></canvas> |
| </div> |
| </div> |
|
|
| |
| <div class="sofa-section"> |
| <div class="sofa-card"> |
| <div class="mini-label">SOFA Score</div> |
| <div class="big-num" id="sofa-num" style="color:var(--amber)">β</div> |
| <div style="font-size:10px;color:var(--text3);margin-top:4px">/ 24 Β· higher = worse</div> |
| </div> |
| <div class="ep-reward-card"> |
| <div class="mini-label">Episode Reward</div> |
| <div class="big-num" id="ep-reward" style="color:var(--blue)">β</div> |
| <div style="font-size:10px;color:var(--text3);margin-top:4px">cumulative</div> |
| </div> |
| </div> |
|
|
| |
| <div class="actions-section-title">Treatment Actions</div> |
| <div class="actions-grid" id="actions-grid"> |
| <button class="action-btn" id="a0" onclick="doStep(0)"> |
| <span class="action-icon">π«</span> |
| <div class="action-name">No Treatment</div> |
| <div class="action-desc">Watchful waiting</div> |
| </button> |
| <button class="action-btn" id="a1" onclick="doStep(1)"> |
| <span class="action-icon">π</span> |
| <div class="action-name">Broad AB</div> |
| <div class="action-desc">pip-tazo Β· gram-neg</div> |
| </button> |
| <button class="action-btn" id="a2" onclick="doStep(2)"> |
| <span class="action-icon">π</span> |
| <div class="action-name">Narrow AB</div> |
| <div class="action-desc">vancomycin Β· MRSA</div> |
| </button> |
| <button class="action-btn" id="a3" onclick="doStep(3)"> |
| <span class="action-icon">π</span> |
| <div class="action-name">Low Vasopressor</div> |
| <div class="action-desc">0.1 mcg/kg/min NE</div> |
| </button> |
| <button class="action-btn" id="a4" onclick="doStep(4)"> |
| <span class="action-icon">β¬οΈ</span> |
| <div class="action-name">High Vasopressor</div> |
| <div class="action-desc">0.3 mcg/kg/min Β· β renal</div> |
| </button> |
| <button class="action-btn" id="a5" onclick="doStep(5)"> |
| <span class="action-icon">ππ</span> |
| <div class="action-name">Broad + Low VP</div> |
| <div class="action-desc">combined therapy</div> |
| </button> |
| <button class="action-btn" id="a6" onclick="doStep(6)"> |
| <span class="action-icon">πβ¬οΈ</span> |
| <div class="action-name">Broad + High VP</div> |
| <div class="action-desc">aggressive Β· β renal</div> |
| </button> |
| <button class="action-btn" id="a7" onclick="doStep(7)"> |
| <span class="action-icon">ππ</span> |
| <div class="action-name">Narrow + Low VP</div> |
| <div class="action-desc">MRSA + haemodynamic</div> |
| </button> |
| <button class="action-btn" id="a8" onclick="doStep(8)"> |
| <span class="action-icon">πβ¬οΈ</span> |
| <div class="action-name">Narrow + High VP</div> |
| <div class="action-desc">max intensity Β· β renal</div> |
| </button> |
| </div> |
| </div> |
|
|
| |
| <div class="panel right-panel"> |
| <div class="panel-title">Episode Score</div> |
|
|
| <div class="score-section"> |
| <div class="score-display" id="score-display">β</div> |
| <div class="score-sub" id="score-sub">Start an episode to see score</div> |
| <div class="score-bar"> |
| <div class="score-fill" id="score-fill" style="width:0%"></div> |
| </div> |
| </div> |
|
|
| <div class="panel-title">Grader Breakdown</div> |
| <div class="grader-metrics" id="grader-metrics"> |
| <div class="metric-row"><span class="metric-name" style="color:var(--text3);font-style:italic">Complete episode to see metrics</span></div> |
| </div> |
|
|
| <div class="panel-title" style="margin-top:auto">Step Log</div> |
| <div class="log-wrap"> |
| <div class="log-box" id="log-box"> |
| <div class="log-entry log-system">βΆ SepsisPilot OpenEnv initialized</div> |
| <div class="log-entry log-system">βΆ Select a task and press Start Episode</div> |
| </div> |
| </div> |
| </div> |
|
|
| </div> |
| </div> |
|
|
| <script> |
| |
| |
| |
| const BASE = ''; |
| let currentTask = 'mild_sepsis'; |
| let episodeDone = false; |
| let lastVitals = null; |
| let prevVitals = null; |
| let mapHistory = []; |
| let autoTimer = null; |
| let episodeCount = 0; |
| |
| |
| |
| |
| async function ping() { |
| try { |
| const r = await fetch(BASE + '/health', {signal: AbortSignal.timeout(3000)}); |
| const ok = r.ok; |
| document.getElementById('server-dot').className = 'dot' + (ok ? ' live' : ''); |
| document.getElementById('server-label').textContent = ok ? 'Server live' : 'Server error'; |
| } catch { |
| document.getElementById('server-dot').className = 'dot'; |
| document.getElementById('server-label').textContent = 'Server offline'; |
| } |
| } |
| ping(); |
| setInterval(ping, 8000); |
| |
| |
| |
| |
| function selectTask(t, diff) { |
| currentTask = t; |
| document.querySelectorAll('.task-card').forEach(c => c.classList.remove('active')); |
| const map = {mild_sepsis: 'task-mild', septic_shock: 'task-shock', severe_mods: 'task-mods'}; |
| document.getElementById(map[t]).classList.add('active'); |
| } |
| |
| function setSeed(v) { |
| document.getElementById('seed-input').value = v; |
| } |
| |
| |
| |
| |
| async function doReset() { |
| stopAuto(); |
| const seed = parseInt(document.getElementById('seed-input').value) || 42; |
| episodeCount++; |
| try { |
| const r = await fetch(BASE + '/reset', { |
| method: 'POST', |
| headers: {'Content-Type':'application/json'}, |
| body: JSON.stringify({task: currentTask, seed}) |
| }); |
| if (!r.ok) { logSystem('ERROR: ' + await r.text()); return; } |
| const state = await r.json(); |
| episodeDone = false; |
| prevVitals = null; |
| lastVitals = state.vitals; |
| mapHistory = [state.vitals.map_mmhg]; |
| |
| updateVitals(state); |
| updateStats(state, null); |
| setStatus('active'); |
| clearGrader(); |
| logSystem(`Episode #${episodeCount} started β task: ${currentTask} β seed: ${seed}`); |
| document.getElementById('score-display').textContent = 'β'; |
| document.getElementById('score-display').className = 'score-display'; |
| document.getElementById('score-sub').textContent = 'Episode in progressβ¦'; |
| document.getElementById('score-fill').style.width = '0%'; |
| setActionsEnabled(true); |
| } catch(e) { logSystem('FETCH ERROR: ' + e.message); } |
| } |
| |
| |
| |
| |
| async function doStep(action) { |
| if (episodeDone) { logSystem('Episode done β press Start Episode'); return; } |
| try { |
| const r = await fetch(BASE + '/step', { |
| method: 'POST', |
| headers: {'Content-Type':'application/json'}, |
| body: JSON.stringify({action}) |
| }); |
| if (!r.ok) { logSystem('STEP ERROR: ' + await r.text()); return; } |
| const result = await r.json(); |
| |
| prevVitals = lastVitals; |
| lastVitals = result.state.vitals; |
| mapHistory.push(result.state.vitals.map_mmhg); |
| if (mapHistory.length > 60) mapHistory.shift(); |
| |
| updateVitals(result.state); |
| updateStats(result.state, result.reward); |
| logStep(result.state.step, action, result.reward, result.done); |
| highlightAction(action); |
| drawWaveform(); |
| |
| if (result.done) { |
| episodeDone = true; |
| stopAuto(); |
| setActionsEnabled(false); |
| const alive = result.state.alive; |
| setStatus(alive ? 'stable' : 'dead'); |
| await fetchGrade(); |
| } |
| } catch(e) { logSystem('FETCH ERROR: ' + e.message); } |
| } |
| |
| |
| |
| |
| async function fetchGrade() { |
| try { |
| const r = await fetch(BASE + '/grade'); |
| if (!r.ok) return; |
| const g = await r.json(); |
| const pct = (g.score * 100).toFixed(1); |
| const disp = document.getElementById('score-display'); |
| disp.textContent = pct + '%'; |
| disp.className = 'score-display' + (g.score < 0.3 ? ' bad' : ''); |
| document.getElementById('score-sub').textContent = g.reason; |
| document.getElementById('score-fill').style.width = pct + '%'; |
| renderGraderMetrics(g.metrics); |
| logSystem(`GRADE β score=${g.score.toFixed(4)} | passed=${g.passed}`); |
| } catch(e) { logSystem('GRADE ERROR: ' + e.message); } |
| } |
| |
| function renderGraderMetrics(metrics) { |
| const el = document.getElementById('grader-metrics'); |
| el.innerHTML = Object.entries(metrics).map(([k, v]) => { |
| const nice = k.replace(/_/g,' '); |
| const val = typeof v === 'number' ? v.toFixed(3) : v; |
| const color = v >= 0.7 ? 'var(--accent)' : v >= 0.4 ? 'var(--amber)' : 'var(--red)'; |
| return `<div class="metric-row"> |
| <span class="metric-name">${nice}</span> |
| <span class="metric-val" style="color:${color}">${val}</span> |
| </div>`; |
| }).join(''); |
| } |
| |
| |
| |
| |
| function autoPlay() { |
| if (episodeDone) { doReset().then(() => setTimeout(startAuto, 400)); return; } |
| startAuto(); |
| } |
| function startAuto() { |
| if (autoTimer) return; |
| document.getElementById('auto-btn').textContent = 'βΉ Stop Auto'; |
| autoTimer = setInterval(async () => { |
| if (episodeDone) { stopAuto(); return; } |
| const v = lastVitals; |
| let action = 1; |
| if (v) { |
| const mapLow = v.map_mmhg < 65; |
| action = mapLow ? 5 : 1; |
| } |
| await doStep(action); |
| }, 600); |
| } |
| function stopAuto() { |
| if (autoTimer) { clearInterval(autoTimer); autoTimer = null; } |
| document.getElementById('auto-btn').textContent = 'βΆ Auto-Play (Heuristic)'; |
| } |
| |
| |
| |
| |
| function updateVitals(state) { |
| const v = state.vitals; |
| setVital('map', v.map_mmhg, 65, 100, 40, 160, false); |
| setVital('lac', v.lactate, 0.5, 2.0, 0.1, 15, true); |
| setVital('wbc', v.wbc, 4, 12, 0.5, 35, false); |
| setVital('temp', v.temperature, 36.5,38.0, 33, 42, false); |
| setVital('hr', v.heart_rate, 60, 100, 20, 165, false); |
| setVital('cr', v.creatinine, 0.6, 1.2, 0.3, 10, false); |
| |
| document.getElementById('sofa-num').textContent = v.sofa_score.toFixed(1); |
| const sc = v.sofa_score; |
| document.getElementById('sofa-num').style.color = sc < 5 ? 'var(--accent)' : sc < 10 ? 'var(--amber)' : 'var(--red)'; |
| |
| document.getElementById('patient-id').textContent = `ICU-${String(episodeCount).padStart(4,'0')}`; |
| document.getElementById('patient-meta').textContent = `${currentTask.replace(/_/g,' ')} Β· Step ${state.step} / ${state.max_steps}`; |
| } |
| |
| function setVital(id, value, goodMin, goodMax, absMin, absMax, invertBad) { |
| const el = document.getElementById('v-' + id); |
| const bar = document.getElementById('b-' + id); |
| const card = document.getElementById('card-' + id); |
| const delt = document.getElementById('d-' + id); |
| |
| const good = value >= goodMin && value <= goodMax; |
| const mild = !good && ((value >= goodMin * 0.85 && !invertBad) || (value <= goodMax * 1.2 && invertBad)); |
| |
| const cls = good ? 'ok' : mild ? 'warn' : 'crit'; |
| el.className = 'vital-value ' + cls; |
| card.className = 'vital-card ' + cls; |
| |
| const dp = id === 'temp' ? 1 : id === 'lac' || id === 'cr' ? 2 : 1; |
| el.textContent = value.toFixed(dp); |
| |
| const pct = Math.max(0, Math.min(100, ((value - absMin) / (absMax - absMin)) * 100)); |
| bar.style.width = pct + '%'; |
| bar.className = 'vital-fill fill-' + cls; |
| |
| |
| if (prevVitals) { |
| const prev = getVitalVal(id, prevVitals); |
| const diff = value - prev; |
| if (Math.abs(diff) > 0.05) { |
| const up = diff > 0; |
| |
| const isGoodDirection = id === 'map' ? up : id === 'lac' || id === 'cr' ? !up : !up; |
| delt.className = 'vital-delta ' + (isGoodDirection ? 'delta-down' : 'delta-up'); |
| delt.textContent = (diff > 0 ? 'β² +' : 'βΌ ') + diff.toFixed(dp); |
| } else { |
| delt.textContent = 'β stable'; |
| delt.className = 'vital-delta delta-flat'; |
| } |
| } |
| } |
| |
| function getVitalVal(id, v) { |
| const map = {map: 'map_mmhg', lac: 'lactate', wbc: 'wbc', temp: 'temperature', hr: 'heart_rate', cr: 'creatinine'}; |
| return v[map[id]] ?? 0; |
| } |
| |
| function updateStats(state, reward) { |
| document.getElementById('s-task').textContent = state.task.replace(/_/g,' '); |
| document.getElementById('s-step').textContent = `${state.step} / ${state.max_steps}`; |
| const er = state.episode_reward; |
| const rew = document.getElementById('s-reward'); |
| rew.textContent = er >= 0 ? '+' + er.toFixed(2) : er.toFixed(2); |
| rew.className = 'stat-value ' + (er >= 0 ? 'green' : 'red'); |
| document.getElementById('s-alive').textContent = state.alive ? 'β Yes' : 'β Dead'; |
| document.getElementById('s-alive').className = 'stat-value ' + (state.alive ? 'green' : 'red'); |
| document.getElementById('s-stab').textContent = state.stabilized_at != null ? `Step ${state.stabilized_at}` : 'β'; |
| document.getElementById('ep-reward').textContent = er >= 0 ? '+' + er.toFixed(1) : er.toFixed(1); |
| document.getElementById('ep-reward').style.color = er >= 0 ? 'var(--accent)' : 'var(--red)'; |
| } |
| |
| function setStatus(s) { |
| const el = document.getElementById('status-pill'); |
| const map = { |
| idle: ['IDLE', 'pill-idle'], |
| active: ['β LIVE', 'pill-active'], |
| stable: ['β STABLE', 'pill-stable'], |
| dead: ['β DEAD', 'pill-dead'], |
| }; |
| const [text, cls] = map[s] || map.idle; |
| el.textContent = text; |
| el.className = 'status-pill ' + cls; |
| } |
| |
| function setActionsEnabled(enabled) { |
| document.querySelectorAll('.action-btn').forEach(b => b.disabled = !enabled); |
| } |
| |
| function highlightAction(action) { |
| document.querySelectorAll('.action-btn').forEach((b, i) => { |
| b.classList.toggle('fired', i === action); |
| }); |
| } |
| |
| function clearGrader() { |
| document.getElementById('grader-metrics').innerHTML = |
| '<div class="metric-row"><span class="metric-name" style="color:var(--text3);font-style:italic">Complete episode to see metrics</span></div>'; |
| } |
| |
| |
| |
| |
| function drawWaveform() { |
| const canvas = document.getElementById('waveform-canvas'); |
| const ctx = canvas.getContext('2d'); |
| const W = canvas.offsetWidth; |
| const H = 70; |
| canvas.width = W; |
| canvas.height = H; |
| |
| ctx.clearRect(0, 0, W, H); |
| |
| |
| const goalY = H - ((65 - 30) / (130 - 30)) * H; |
| ctx.setLineDash([4, 4]); |
| ctx.strokeStyle = 'rgba(0,229,176,0.25)'; |
| ctx.lineWidth = 1; |
| ctx.beginPath(); ctx.moveTo(0, goalY); ctx.lineTo(W, goalY); ctx.stroke(); |
| ctx.setLineDash([]); |
| |
| if (mapHistory.length < 2) return; |
| |
| const data = mapHistory; |
| const n = data.length; |
| const xStep = W / Math.max(n - 1, 1); |
| |
| |
| const grad = ctx.createLinearGradient(0, 0, 0, H); |
| grad.addColorStop(0, 'rgba(0,229,176,0.25)'); |
| grad.addColorStop(1, 'rgba(0,229,176,0)'); |
| |
| ctx.beginPath(); |
| data.forEach((v, i) => { |
| const x = i * xStep; |
| const y = H - ((Math.min(130, Math.max(30, v)) - 30) / 100) * H; |
| i === 0 ? ctx.moveTo(x, y) : ctx.lineTo(x, y); |
| }); |
| ctx.lineTo((n - 1) * xStep, H); |
| ctx.lineTo(0, H); |
| ctx.closePath(); |
| ctx.fillStyle = grad; |
| ctx.fill(); |
| |
| |
| ctx.beginPath(); |
| data.forEach((v, i) => { |
| const x = i * xStep; |
| const y = H - ((Math.min(130, Math.max(30, v)) - 30) / 100) * H; |
| const last = data[n - 1]; |
| ctx.strokeStyle = last >= 65 ? 'var(--accent)' : last >= 50 ? 'var(--amber)' : 'var(--red)'; |
| i === 0 ? ctx.moveTo(x, y) : ctx.lineTo(x, y); |
| }); |
| ctx.lineWidth = 2; |
| ctx.stroke(); |
| |
| |
| const last = data[n - 1]; |
| const lx = (n - 1) * xStep; |
| const ly = H - ((Math.min(130, Math.max(30, last)) - 30) / 100) * H; |
| ctx.beginPath(); |
| ctx.arc(lx, ly, 4, 0, Math.PI * 2); |
| ctx.fillStyle = last >= 65 ? 'var(--accent)' : 'var(--red)'; |
| ctx.fill(); |
| |
| |
| ctx.font = '9px IBM Plex Mono'; |
| ctx.fillStyle = 'rgba(0,229,176,0.4)'; |
| ctx.fillText('goal 65', 4, goalY - 4); |
| } |
| |
| |
| |
| |
| function logStep(step, action, reward, done) { |
| const rCls = reward >= 0 ? 'log-reward-pos' : 'log-reward-neg'; |
| const actionNames = ['no_tx','broad_ab','narrow_ab','low_vp','high_vp','broad+low','broad+high','narrow+low','narrow+high']; |
| const html = |
| `<span class="log-step-num">T=${String(step).padStart(2,'0')}</span> ` + |
| `<span class="log-action">act=${actionNames[action] ?? action}</span> ` + |
| `<span class="${rCls}">r=${reward >= 0 ? '+' : ''}${reward.toFixed(3)}</span>` + |
| (done ? ' <span class="log-done">DONE</span>' : ''); |
| appendLog(html); |
| } |
| |
| function logSystem(msg) { |
| appendLog(`<span class="log-system">βΆ ${msg}</span>`); |
| } |
| |
| function appendLog(html) { |
| const box = document.getElementById('log-box'); |
| const div = document.createElement('div'); |
| div.className = 'log-entry'; |
| div.innerHTML = html; |
| box.appendChild(div); |
| box.scrollTop = box.scrollHeight; |
| } |
| |
| |
| |
| |
| setActionsEnabled(false); |
| window.addEventListener('resize', drawWaveform); |
| </script> |
| </body> |
| </html> |