mmrech commited on
Commit
a1b8703
·
verified ·
1 Parent(s): c68c3aa

v3.0: Expanded HTML frontend with all 34 ML inputs + Gradio API integration

Browse files
Files changed (1) hide show
  1. evb_prognosis_complete.html +769 -372
evb_prognosis_complete.html CHANGED
@@ -31,7 +31,7 @@
31
  min-height: 100vh;
32
  display: flex;
33
  justify-content: center;
34
- align-items: center;
35
  overflow-x: hidden;
36
  padding: 40px 20px;
37
  }
@@ -73,7 +73,7 @@
73
 
74
  .calculator-card {
75
  width: 100%;
76
- max-width: 1300px;
77
  background: var(--glass-bg);
78
  backdrop-filter: blur(25px) saturate(180%);
79
  -webkit-backdrop-filter: blur(25px) saturate(180%);
@@ -86,7 +86,12 @@
86
 
87
  .main-grid {
88
  display: grid;
89
- grid-template-columns: 1fr 400px;
 
 
 
 
 
90
  }
91
 
92
  .input-region {
@@ -103,9 +108,9 @@
103
  -webkit-text-fill-color: transparent;
104
  margin-bottom: 8px;
105
  }
106
- header p {
107
- color: var(--text-dim);
108
- font-size: 0.9rem;
109
  max-width: 620px;
110
  line-height: 1.5;
111
  }
@@ -140,9 +145,9 @@
140
  }
141
 
142
  .input-group { display: flex; flex-direction: column; gap: 8px; }
143
- label {
144
- font-size: 0.8rem;
145
- color: var(--text-dim);
146
  font-weight: 600;
147
  display: flex;
148
  align-items: center;
@@ -232,7 +237,11 @@
232
  background: rgba(0, 0, 0, 0.2);
233
  display: flex;
234
  flex-direction: column;
235
- gap: 32px;
 
 
 
 
236
  }
237
 
238
  .score-ring {
@@ -244,11 +253,11 @@
244
  place-items: center;
245
  background:
246
  radial-gradient(circle at center, rgba(0,0,0,0.55) 58%, transparent 60%),
247
- conic-gradient(var(--accent-primary) var(--p, 0%), rgba(255,255,255,0.08) 0);
248
  border: 1px solid var(--glass-border);
249
  box-shadow: 0 18px 40px rgba(0,0,0,0.35);
250
  position: relative;
251
- transition: all 0.5s ease;
252
  }
253
 
254
  .score-core {
@@ -279,6 +288,11 @@
279
  text-align: center;
280
  padding: 0 10px;
281
  }
 
 
 
 
 
282
 
283
  .status-badge {
284
  padding: 6px 16px;
@@ -294,38 +308,71 @@
294
  box-shadow: 0 10px 20px rgba(0, 0, 0, 0.3);
295
  transition: all 0.3s ease;
296
  }
297
- .status-low {
298
- background: var(--success);
299
  color: #000;
300
  box-shadow: 0 0 20px rgba(0, 242, 254, 0.4);
301
  }
302
- .status-mid {
303
- background: var(--warning);
304
  color: #000;
305
  box-shadow: 0 0 20px rgba(249, 212, 35, 0.4);
306
  }
307
- .status-high {
308
- background: var(--danger);
309
  color: #fff;
310
  box-shadow: 0 0 20px rgba(255, 75, 43, 0.4);
311
  }
312
 
313
- .metrics-list { display: flex; flex-direction: column; gap: 16px; }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
314
  .metric-item {
315
  display: flex;
316
  justify-content: space-between;
317
  align-items: flex-end;
318
- padding-bottom: 12px;
319
  border-bottom: 1px solid var(--glass-border);
320
  }
321
- .metric-item .m-label { font-size: 0.85rem; color: var(--text-dim); }
322
  .metric-item .m-value {
323
  font-family: 'JetBrains Mono', monospace;
324
- font-size: 1.2rem;
325
  color: #fff;
326
  }
327
  .metric-item .m-sub {
328
- font-size: 0.65rem;
329
  color: var(--accent-primary);
330
  text-align: right;
331
  display: block;
@@ -345,73 +392,119 @@
345
  }
346
 
347
  .btn-calculate {
348
- background: #fff;
349
  color: #000;
 
350
  }
351
 
352
  .btn-calculate:hover {
353
  transform: translateY(-2px);
354
- box-shadow: 0 10px 25px rgba(255, 255, 255, 0.2);
 
 
 
 
 
 
355
  }
356
 
357
  .btn-export {
358
  background: transparent;
359
  border: 1px solid var(--glass-border);
360
- color: var(--text-main);
361
- padding: 12px;
362
- font-size: 0.85rem;
363
  }
364
 
365
  .btn-export:hover {
366
- background: rgba(255, 255, 255, 0.05);
367
  border-color: var(--accent-primary);
 
368
  }
369
 
370
  .references-section {
371
- grid-column: 1/-1;
372
  padding: 32px 48px;
373
  border-top: 1px solid var(--glass-border);
374
- font-size: 0.8rem;
375
- color: var(--text-dim);
376
- background: rgba(0, 0, 0, 0.2);
377
  }
378
-
379
  .references-section h3 {
 
380
  color: var(--accent-primary);
381
  margin-bottom: 16px;
382
- font-size: 0.9rem;
383
- text-transform: uppercase;
384
- letter-spacing: 1px;
385
  }
386
-
387
  .references-section ol {
388
- line-height: 1.8;
389
  padding-left: 20px;
 
 
 
390
  }
391
 
392
- .references-section em {
393
- color: var(--text-main);
394
- font-style: italic;
 
 
 
395
  }
396
 
397
- .fade-in { animation: fadeIn 0.65s ease forwards; }
398
- @keyframes fadeIn { from { opacity: 0; transform: translateY(10px); } to { opacity: 1; transform: translateY(0); } }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
399
 
400
- @media (max-width: 1100px) {
401
- .main-grid { grid-template-columns: 1fr; }
402
- .input-region { border-right: none; border-bottom: 1px solid var(--glass-border); }
403
- .grid-layout { grid-template-columns: 1fr; }
 
 
 
 
404
  }
405
 
406
- @media (max-width: 768px) {
407
- body { padding: 20px 10px; }
408
- .input-region, .result-region, .references-section { padding: 24px; }
409
- header h1 { font-size: 2rem; }
410
- .val-display { min-width: 60px; font-size: 1rem; }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
411
  }
412
  </style>
413
  </head>
414
-
415
  <body>
416
  <div class="vapor-field">
417
  <div class="vapor-cloud c1"></div>
@@ -421,20 +514,24 @@
421
 
422
  <main class="calculator-card">
423
  <div class="disclaimer-banner">
424
- <strong>⚠️ FOR EDUCATIONAL AND RESEARCH PURPOSES ONLY</strong>
425
- This calculator is not validated for clinical decision-making without physician supervision. Not a substitute for clinical judgment. Always consult appropriate medical expertise.
426
  </div>
427
 
428
  <div class="main-grid">
429
  <section class="input-region">
430
  <header>
431
  <h1>EVB Prognosis Calculator</h1>
432
- <p>Clinical risk calculator for acute esophageal variceal bleeding using validated prognostic scores (MELD, MELD-Na, Child-Pugh, ALBI) with weighted heuristic model. <strong style="color: var(--warning);">Not a machine learning model.</strong> Estimates based on published mortality data.</p>
 
 
 
 
433
  </header>
434
 
435
  <!-- Preset Scenarios -->
436
  <div class="section-tab">
437
- <h2 class="section-title">Quick Start - Clinical Scenarios</h2>
438
  <div class="grid-layout">
439
  <div class="input-group preset-row">
440
  <label>Load Example Case</label>
@@ -449,17 +546,17 @@
449
  </div>
450
  </div>
451
 
 
452
  <div class="section-tab">
453
- <h2 class="section-title">1. Demographics & Clinical Status</h2>
454
  <div class="grid-layout">
455
  <div class="input-group">
456
  <label for="age">Patient Age</label>
457
  <div class="slider-wrap">
458
- <input type="range" id="age" min="18" max="100" value="52" />
459
- <span class="val-display" id="age-val">52</span>
460
  </div>
461
  </div>
462
-
463
  <div class="input-group">
464
  <label for="sex">Sex</label>
465
  <select id="sex">
@@ -467,162 +564,361 @@
467
  <option value="female">Female</option>
468
  </select>
469
  </div>
470
-
471
  <div class="input-group">
472
- <label for="etiology">Etiology
473
- <span class="info-icon" data-tip="Primary cause of cirrhosis. Alcohol and HCV are most common in variceal bleeding."></span>
474
  </label>
475
- <select id="etiology">
 
 
 
 
 
 
 
 
 
 
 
476
  <option value="alcohol">Alcohol</option>
477
  <option value="hcv">HCV</option>
478
- <option value="hbv">HBV</option>
479
- <option value="nash">NASH</option>
480
  <option value="other">Other</option>
481
  </select>
482
  </div>
 
 
483
 
 
 
 
 
 
 
 
 
 
 
 
 
 
484
  <div class="input-group">
485
- <label for="ascites">Ascites
486
- <span class="info-icon" data-tip="Fluid accumulation in abdomen. Marker of portal hypertension and hepatic decompensation."></span>
487
  </label>
488
- <select id="ascites">
489
- <option value="none">None</option>
490
- <option value="mild">Mild–Moderate</option>
491
- <option value="severe">Moderate–Severe</option>
492
  </select>
493
  </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
494
 
 
 
 
 
495
  <div class="input-group">
496
- <label for="enc">Encephalopathy
497
- <span class="info-icon" data-tip="Hepatic encephalopathy grades: I-II = altered sleep, mild confusion. III-IV = somnolent to coma."></span>
498
  </label>
499
- <select id="enc">
500
- <option value="none">None</option>
501
- <option value="mild">Grade I–II</option>
502
- <option value="severe">Grade III–IV</option>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
503
  </select>
504
  </div>
505
  </div>
506
  </div>
507
 
 
508
  <div class="section-tab">
509
- <h2 class="section-title">2. Laboratory Profile</h2>
510
  <div class="grid-layout">
511
  <div class="input-group">
512
- <label for="bilirubin">Total Bilirubin (mg/dL)
513
- <span class="info-icon" data-tip="Reflects hepatic synthetic dysfunction and cholestasis. Normal: 0.1-1.2 mg/dL. Major MELD component."></span>
514
  </label>
515
  <div class="slider-wrap">
516
- <input type="range" id="bilirubin" min="0.1" max="30" step="0.1" value="2.1" />
517
- <span class="val-display" id="bilirubin-val">2.1</span>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
518
  </div>
519
  </div>
520
-
521
  <div class="input-group">
522
  <label for="inr">INR
523
- <span class="info-icon" data-tip="International Normalized Ratio. Measures coagulation status. Normal: 0.8-1.2. Major MELD component."></span>
524
  </label>
525
  <div class="slider-wrap">
526
- <input type="range" id="inr" min="0.5" max="6" step="0.1" value="1.3" />
527
- <span class="val-display" id="inr-val">1.3</span>
528
  </div>
529
  </div>
530
-
531
  <div class="input-group">
532
  <label for="creatinine">Creatinine (mg/dL)
533
- <span class="info-icon" data-tip="Serum creatinine reflects renal function. Normal: 0.6-1.2 mg/dL. Major MELD component."></span>
534
  </label>
535
  <div class="slider-wrap">
536
  <input type="range" id="creatinine" min="0.1" max="10" step="0.1" value="1.0" />
537
  <span class="val-display" id="creatinine-val">1.0</span>
538
  </div>
539
  </div>
540
-
541
  <div class="input-group">
542
- <label for="albumin">Albumin (g/dL)
543
- <span class="info-icon" data-tip="Serum albumin reflects hepatic synthetic function. Normal: 3.5-5.5 g/dL. Child-Pugh component."></span>
544
  </label>
545
  <div class="slider-wrap">
546
- <input type="range" id="albumin" min="1" max="5" step="0.1" value="3.4" />
547
- <span class="val-display" id="albumin-val">3.4</span>
548
  </div>
549
  </div>
550
-
551
  <div class="input-group">
552
- <label for="sodium">Sodium (mEq/L)
553
- <span class="info-icon" data-tip="Serum sodium. Hyponatremia indicates dilutional state and poor prognosis. Normal: 135-145 mEq/L."></span>
554
  </label>
555
  <div class="slider-wrap">
556
- <input type="range" id="sodium" min="120" max="150" step="1" value="138" />
557
- <span class="val-display" id="sodium-val">138</span>
558
  </div>
559
  </div>
560
-
561
  <div class="input-group">
562
- <label for="dialysis">Dialysis Status
563
- <span class="info-icon" data-tip="Dialysis ≥2x in past week sets creatinine to 4.0 per UNOS MELD guidelines."></span>
564
  </label>
565
- <select id="dialysis">
566
- <option value="no">No</option>
567
- <option value="yes">Yes (≥2x/week)</option>
568
- </select>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
569
  </div>
570
  </div>
571
  </div>
572
 
 
573
  <div class="section-tab">
574
- <h2 class="section-title">3. Bleeding Episode Details</h2>
575
  <div class="grid-layout">
576
  <div class="input-group">
577
- <label for="bleeding">Active Bleeding at Endoscopy
578
- <span class="info-icon" data-tip="Active bleeding or spurting at time of endoscopy indicates higher risk."></span>
579
  </label>
580
- <select id="bleeding">
581
  <option value="no">No</option>
 
 
 
 
 
 
 
 
 
582
  <option value="yes">Yes</option>
583
  </select>
584
  </div>
585
-
586
  <div class="input-group">
587
- <label for="terlipressin">Terlipressin Dose (mg)
588
- <span class="info-icon" data-tip="Vasoactive drug for variceal bleeding. Typical dose 1-2mg q4-6h initially."></span>
589
  </label>
590
- <input type="number" id="terlipressin" value="2" min="0" max="20" step="0.5" />
 
 
 
591
  </div>
592
-
593
  <div class="input-group">
594
- <label for="endo_time">Time to Endoscopy (hours)
595
- <span class="info-icon" data-tip="Time from presentation to therapeutic endoscopy. Guidelines recommend <12 hours."></span>
596
  </label>
597
- <input type="number" id="endo_time" value="12" min="0" max="48" step="1" />
 
 
 
598
  </div>
599
-
600
  <div class="input-group">
601
- <label for="sbp">Spontaneous Bacterial Peritonitis
602
- <span class="info-icon" data-tip="SBP significantly increases mortality in cirrhotic patients."></span>
603
  </label>
604
- <select id="sbp">
605
- <option value="no">No</option>
606
  <option value="yes">Yes</option>
607
  </select>
608
  </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
609
  </div>
610
  </div>
611
  </section>
612
 
 
613
  <section class="result-region">
614
- <button class="btn-calculate" id="btn-run">🔮 Calculate Risk</button>
 
 
615
 
616
  <div id="results-content">
617
  <div class="score-ring" id="score-ring" style="--p:0%">
618
  <div class="score-core">
619
  <span class="pct" id="mortality-pct">--</span>
620
  <span class="label">1-Year Mortality</span>
 
621
  </div>
622
  </div>
623
 
624
  <div id="risk-badge" class="status-badge">Awaiting Input</div>
625
 
 
 
 
 
 
 
 
 
 
 
 
 
 
626
  <div class="metrics-list">
627
  <div class="metric-item">
628
  <span class="m-label">MELD Score</span>
@@ -631,7 +927,6 @@
631
  <span class="m-sub" id="meld-mort">3-mo mortality: --</span>
632
  </div>
633
  </div>
634
-
635
  <div class="metric-item">
636
  <span class="m-label">MELD-Na</span>
637
  <div>
@@ -639,7 +934,6 @@
639
  <span class="m-sub">Sodium adjusted</span>
640
  </div>
641
  </div>
642
-
643
  <div class="metric-item">
644
  <span class="m-label">Child-Pugh</span>
645
  <div>
@@ -647,7 +941,6 @@
647
  <span class="m-sub" id="cp-class">Class --</span>
648
  </div>
649
  </div>
650
-
651
  <div class="metric-item">
652
  <span class="m-label">ALBI Score</span>
653
  <div>
@@ -656,286 +949,391 @@
656
  </div>
657
  </div>
658
  </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
659
  </div>
660
 
661
- <button class="btn-export" id="btn-export">📊 Export Results (JSON)</button>
662
 
663
- <p style="font-size: 0.65rem; color: var(--text-dim); line-height: 1.5; text-align: center; margin-top: 16px;">
664
- <strong>Model Performance:</strong><br>
665
- This heuristic model is for educational demonstration. Clinical scores have established validation (MELD AUC ~0.83, MELD-Na AUC ~0.87 for transplant mortality).
666
  </p>
667
  </section>
668
  </div>
669
 
670
  <section class="references-section">
671
- <h3>📚 Clinical References</h3>
672
  <ol>
673
- <li>Kamath PS, et al. A model to predict survival in patients with end-stage liver disease. <em>Hepatology</em>. 2001;33(2):464-470. doi:10.1053/jhep.2001.22172</li>
674
- <li>Biggins SW, et al. Serum sodium predicts mortality in patients listed for liver transplantation. <em>Hepatology</em>. 2005;41(1):32-39. doi:10.1002/hep.20517</li>
675
- <li>Pugh RN, et al. Transection of the oesophagus for bleeding oesophageal varices. <em>Br J Surg</em>. 1973;60(8):646-649. doi:10.1002/bjs.1800600817</li>
676
- <li>Johnson PJ, et al. Assessment of liver function in patients with hepatocellular carcinoma: a new evidence-based approach-the ALBI grade. <em>J Clin Oncol</em>. 2015;33(6):550-558. doi:10.1200/JCO.2014.57.9151</li>
677
- <li>Garcia-Tsao G, et al. Prevention and management of gastroesophageal varices and variceal hemorrhage in cirrhosis. <em>Hepatology</em>. 2007;46(3):922-938. doi:10.1002/hep.21907</li>
678
- <li>de Franchis R, et al. Baveno VII Renewing consensus in portal hypertension. <em>J Hepatol</em>. 2022;76(4):959-974. doi:10.1016/j.jhep.2021.12.022</li>
679
  </ol>
680
  </section>
681
  </main>
682
 
683
  <script>
684
  const $ = (id) => document.getElementById(id);
 
685
 
686
  function clamp(n, a, b) { return Math.min(Math.max(n, a), b); }
687
 
688
  function syncDisplay(el) {
689
  const out = $(el.id + '-val');
690
- if (out) out.innerText = el.value;
691
  }
692
 
693
- // ============= CLINICAL SCORE CALCULATIONS =============
694
 
695
- function meldScore(bili, inr, creat, dialysis) {
696
- // UNOS MELD formula with dialysis handling
697
  const b = Math.max(bili, 1);
698
  const i = Math.max(inr, 1);
699
-
700
- // If on dialysis ≥2x/week, creatinine = 4.0 per UNOS guidelines
701
- let c = Math.max(creat, 1);
702
- if (dialysis === 'yes') {
703
- c = 4.0;
704
- } else {
705
- c = Math.min(c, 4.0); // Cap at 4.0 if not on dialysis
706
- }
707
-
708
  let meld = 3.78 * Math.log(b) + 11.2 * Math.log(i) + 9.57 * Math.log(c) + 6.43;
709
- meld = Math.round(meld);
710
- return clamp(meld, 6, 40);
711
  }
712
 
713
  function meldNaScore(meld, sodium) {
714
- // MELD-Na: sodium clipped to [125, 137]
715
  const s = clamp(sodium, 125, 137);
716
  const delta = 137 - s;
717
  let meldNa = meld + 1.32 * delta - (0.033 * meld * delta);
718
- meldNa = Math.round(meldNa);
719
- return clamp(meldNa, 6, 40);
720
  }
721
 
722
- function childPugh(bili, alb, inr, asc, enc) {
723
  let pts = 0;
724
-
725
- // Bilirubin
726
  pts += (bili < 2) ? 1 : (bili <= 3 ? 2 : 3);
727
-
728
- // Albumin
729
  pts += (alb > 3.5) ? 1 : (alb >= 2.8 ? 2 : 3);
730
-
731
- // INR
732
  pts += (inr < 1.7) ? 1 : (inr <= 2.3 ? 2 : 3);
733
-
734
- // Ascites (none/mild/severe)
735
- pts += (asc === 'none') ? 1 : (asc === 'mild' ? 2 : 3);
736
-
737
- // Encephalopathy (none/mild/severe)
738
- pts += (enc === 'none') ? 1 : (enc === 'mild' ? 2 : 3);
739
-
740
  const cls = (pts <= 6) ? 'A' : (pts <= 9 ? 'B' : 'C');
741
  return { pts, cls };
742
  }
743
 
744
  function albiScore(albumin, bilirubin) {
745
- // ALBI = (log10 bilirubin × 0.66) + (albumin × -0.085)
746
  const score = (Math.log10(bilirubin) * 0.66) + (albumin * -0.085);
747
-
748
  let grade;
749
  if (score <= -2.60) grade = '1 (Best)';
750
  else if (score <= -1.39) grade = '2';
751
  else grade = '3 (Worst)';
752
-
753
  return { score: score.toFixed(2), grade };
754
  }
755
 
756
- // ============= VALIDATION =============
757
-
758
- function validateInputs() {
759
- const errors = [];
760
-
761
- const bili = parseFloat($('bilirubin').value);
762
- const inr = parseFloat($('inr').value);
763
- const creat = parseFloat($('creatinine').value);
764
- const alb = parseFloat($('albumin').value);
765
- const sod = parseFloat($('sodium').value);
766
-
767
- // Physiologic range checks
768
- if (bili > 50) {
769
- errors.push('⚠️ Bilirubin >50 mg/dL is extremely rare. Please verify the value.');
770
- }
771
-
772
- if (inr > 10) {
773
- errors.push('⚠️ INR >10 suggests possible lab error or critical coagulopathy requiring immediate intervention.');
774
- }
775
-
776
- if (creat > 15) {
777
- errors.push('⚠️ Creatinine >15 mg/dL is extremely elevated. Verify value and dialysis status.');
778
- }
779
-
780
- if (alb < 1.5 || alb > 5.5) {
781
- errors.push('⚠️ Albumin outside typical range (1.5-5.5 g/dL). Please verify.');
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
782
  }
783
-
784
- if (sod < 115 || sod > 160) {
785
- errors.push('⚠️ Sodium critically abnormal (<115 or >160 mEq/L). This requires immediate medical attention. Please verify value.');
 
 
 
 
 
 
 
 
 
 
 
 
786
  }
787
-
788
- if (errors.length > 0) {
789
- const proceed = confirm(
790
- 'INPUT VALIDATION WARNINGS:\n\n' +
791
- errors.join('\n\n') +
792
- '\n\nDo you want to proceed with calculation anyway?'
793
- );
794
- return proceed;
 
 
 
 
 
 
795
  }
796
-
797
- return true;
798
- }
799
-
800
- // ============= RISK PROBABILITY MODEL =============
801
-
802
- function estimateRisk({ meldNa, age, asc, enc, bleeding, endoTime, sbp, cp }) {
803
- // Weighted heuristic model based on published mortality predictors
804
- // This is NOT machine learning - it's a rule-based estimation
805
-
806
- // Base risk from MELD-Na (strongest predictor)
807
- // MELD-Na 6-15: ~5-10% 1yr mortality
808
- // MELD-Na 16-25: ~20-40% 1yr mortality
809
- // MELD-Na 26-40: ~50-80% 1yr mortality
810
- let baseRisk = (meldNa - 6) / 34 * 0.65; // 0-0.65 range
811
-
812
- // Age adjustment (>65 increases risk)
813
- const ageWeight = age > 65 ? 0.08 : (age > 75 ? 0.12 : 0);
814
-
815
- // Ascites (portal hypertension marker)
816
- const ascWeight = (asc === 'none') ? 0 : (asc === 'mild' ? 0.06 : 0.12);
817
-
818
- // Encephalopathy (hepatic decompensation)
819
- const encWeight = (enc === 'none') ? 0 : (enc === 'mild' ? 0.07 : 0.14);
820
-
821
- // Active bleeding (immediate risk)
822
- const bleedWeight = (bleeding === 'yes') ? 0.08 : 0;
823
-
824
- // Delayed endoscopy (>12h associated with worse outcomes)
825
- const endoWeight = endoTime > 12 ? Math.min((endoTime - 12) / 36, 1) * 0.05 : 0;
826
-
827
- // SBP (major infection risk)
828
- const sbpWeight = (sbp === 'yes') ? 0.10 : 0;
829
-
830
- // Child-Pugh adjustment
831
- const cpWeight = cp === 'C' ? 0.08 : (cp === 'B' ? 0.04 : 0);
832
-
833
- let totalRisk = baseRisk + ageWeight + ascWeight + encWeight +
834
- bleedWeight + endoWeight + sbpWeight + cpWeight;
835
-
836
- return clamp(totalRisk, 0.02, 0.98);
837
- }
838
-
839
- // ============= RENDERING =============
840
-
841
- function render() {
842
- if (!validateInputs()) return;
843
-
844
- // Get all inputs
845
- const bili = parseFloat($('bilirubin').value);
846
- const inr = parseFloat($('inr').value);
847
- const creat = parseFloat($('creatinine').value);
848
- const sod = parseFloat($('sodium').value);
849
- const alb = parseFloat($('albumin').value);
850
- const dialysis = $('dialysis').value;
851
-
852
- const asc = $('ascites').value;
853
- const enc = $('enc').value;
854
- const bleeding = $('bleeding').value;
855
- const sbp = $('sbp').value;
856
-
857
- const age = parseInt($('age').value, 10);
858
- const endoTime = parseFloat($('endo_time').value || '0');
859
-
860
- // Calculate scores
861
- const meld = meldScore(bili, inr, creat, dialysis);
862
- const meldNa = meldNaScore(meld, sod);
863
- const cp = childPugh(bili, alb, inr, asc, enc);
864
- const albi = albiScore(alb, bili);
865
-
866
- // Estimate risk
867
- const riskProb = estimateRisk({
868
- meldNa, age, asc, enc, bleeding, endoTime, sbp, cp: cp.cls
869
- });
870
 
871
- // Animate results
872
- const results = $('results-content');
873
- results.classList.remove('fade-in');
874
- void results.offsetWidth;
875
- results.classList.add('fade-in');
876
-
877
- // Update display
878
- $('mortality-pct').innerText = (riskProb * 100).toFixed(1) + '%';
879
- $('meld-score').innerText = meld;
880
- $('meld-na-score').innerText = meldNa;
881
- $('cp-score').innerText = cp.pts;
882
- $('cp-class').innerText = 'Class ' + cp.cls;
883
- $('albi-score').innerText = albi.score;
884
- $('albi-grade').innerText = 'Grade ' + albi.grade;
885
-
886
- // Progress ring
887
- $('score-ring').style.setProperty('--p', (riskProb * 100).toFixed(1) + '%');
888
-
889
- // Risk category
890
- const badge = $('risk-badge');
891
- if (riskProb < 0.30) {
892
- badge.innerText = 'Low Risk';
893
- badge.className = 'status-badge status-low';
894
- } else if (riskProb < 0.60) {
895
- badge.innerText = 'Moderate Risk';
896
- badge.className = 'status-badge status-mid';
897
  } else {
898
- badge.innerText = 'High Risk';
899
- badge.className = 'status-badge status-high';
900
  }
901
 
902
- // MELD mortality interpretation
903
- const meldMort =
904
- meld < 10 ? '<10%' :
905
- meld < 20 ? '10–19%' :
906
- meld < 30 ? '20–50%' : '>50%';
907
 
908
- $('meld-mort').innerText = '3-mo mortality: ' + meldMort;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
909
  }
910
 
911
  // ============= PRESETS =============
912
 
913
  const PRESETS = {
914
  compensated: {
915
- age: 55, bilirubin: 1.2, inr: 1.1, creatinine: 0.9,
916
- albumin: 3.8, sodium: 140, ascites: 'none', enc: 'none',
917
- bleeding: 'no', dialysis: 'no', sbp: 'no', endo_time: 8
 
 
 
 
 
 
 
918
  },
919
  decompensated: {
920
- age: 62, bilirubin: 2.8, inr: 1.6, creatinine: 1.3,
921
- albumin: 3.0, sodium: 134, ascites: 'mild', enc: 'none',
922
- bleeding: 'yes', dialysis: 'no', sbp: 'no', endo_time: 14
 
 
 
 
 
 
 
923
  },
924
  advanced: {
925
- age: 58, bilirubin: 5.2, inr: 2.4, creatinine: 2.1,
926
- albumin: 2.4, sodium: 128, ascites: 'severe', enc: 'severe',
927
- bleeding: 'yes', dialysis: 'no', sbp: 'yes', endo_time: 18
 
 
 
 
 
 
 
928
  },
929
  hrs: {
930
- age: 64, bilirubin: 3.5, inr: 1.8, creatinine: 3.2,
931
- albumin: 2.7, sodium: 130, ascites: 'severe', enc: 'mild',
932
- bleeding: 'yes', dialysis: 'yes', sbp: 'yes', endo_time: 10
 
 
 
 
 
 
 
933
  }
934
  };
935
 
936
  function loadPreset(name) {
937
  if (!name || !PRESETS[name]) return;
938
-
939
  const preset = PRESETS[name];
940
  Object.keys(preset).forEach(key => {
941
  const el = $(key);
@@ -951,39 +1349,38 @@
951
 
952
  function exportResults() {
953
  const timestamp = new Date().toISOString();
954
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
955
  const data = {
956
  timestamp,
957
  disclaimer: "FOR RESEARCH/EDUCATIONAL USE ONLY - NOT MEDICAL ADVICE",
958
- calculatorVersion: "2.0-Clinical",
959
- inputs: {
960
- demographics: {
961
- age: parseInt($('age').value),
962
- sex: $('sex').value,
963
- etiology: $('etiology').value
964
- },
965
- clinical: {
966
- ascites: $('ascites').value,
967
- encephalopathy: $('enc').value,
968
- activeBleeding: $('bleeding').value,
969
- sbp: $('sbp').value,
970
- dialysisStatus: $('dialysis').value
971
- },
972
- laboratory: {
973
- bilirubin_mg_dL: parseFloat($('bilirubin').value),
974
- inr: parseFloat($('inr').value),
975
- creatinine_mg_dL: parseFloat($('creatinine').value),
976
- albumin_g_dL: parseFloat($('albumin').value),
977
- sodium_mEq_L: parseFloat($('sodium').value)
978
- },
979
- therapy: {
980
- terlipressinDose_mg: parseFloat($('terlipressin').value),
981
- timeToEndoscopy_hours: parseFloat($('endo_time').value)
982
- }
983
- },
984
  results: {
985
  estimatedMortality1Year: $('mortality-pct').innerText,
986
  riskCategory: $('risk-badge').innerText,
 
 
 
 
987
  scores: {
988
  MELD: $('meld-score').innerText,
989
  MELDNa: $('meld-na-score').innerText,
@@ -993,15 +1390,14 @@
993
  ALBIGrade: $('albi-grade').innerText.replace('Grade ', '')
994
  }
995
  },
996
- notes: "This is a heuristic model for educational purposes. Not validated for clinical decisions."
997
  };
998
-
999
  const blob = new Blob([JSON.stringify(data, null, 2)], {type: 'application/json'});
1000
  const url = URL.createObjectURL(blob);
1001
  const a = document.createElement('a');
1002
  a.href = url;
1003
- const dateStr = timestamp.split('T')[0];
1004
- a.download = `evb_risk_assessment_${dateStr}_${Date.now()}.json`;
1005
  a.click();
1006
  URL.revokeObjectURL(url);
1007
  }
@@ -1009,23 +1405,24 @@
1009
  // ============= INITIALIZATION =============
1010
 
1011
  function wireLiveUpdates() {
1012
- const liveIds = [
1013
- 'age','sex','etiology','ascites','enc','dialysis',
1014
- 'bilirubin','inr','creatinine','albumin','sodium',
1015
- 'bleeding','terlipressin','endo_time','sbp'
 
 
 
 
1016
  ];
1017
 
1018
- liveIds.forEach(id => {
1019
  const el = $(id);
1020
  if (!el) return;
1021
-
1022
  const handler = () => {
1023
  if (el.type === 'range') syncDisplay(el);
1024
  };
1025
-
1026
  el.addEventListener('input', handler);
1027
  el.addEventListener('change', handler);
1028
-
1029
  if (el.type === 'range') syncDisplay(el);
1030
  });
1031
 
 
31
  min-height: 100vh;
32
  display: flex;
33
  justify-content: center;
34
+ align-items: flex-start;
35
  overflow-x: hidden;
36
  padding: 40px 20px;
37
  }
 
73
 
74
  .calculator-card {
75
  width: 100%;
76
+ max-width: 1400px;
77
  background: var(--glass-bg);
78
  backdrop-filter: blur(25px) saturate(180%);
79
  -webkit-backdrop-filter: blur(25px) saturate(180%);
 
86
 
87
  .main-grid {
88
  display: grid;
89
+ grid-template-columns: 1fr 420px;
90
+ }
91
+
92
+ @media (max-width: 900px) {
93
+ .main-grid { grid-template-columns: 1fr; }
94
+ .input-region { border-right: none; border-bottom: 1px solid var(--glass-border); }
95
  }
96
 
97
  .input-region {
 
108
  -webkit-text-fill-color: transparent;
109
  margin-bottom: 8px;
110
  }
111
+ header p {
112
+ color: var(--text-dim);
113
+ font-size: 0.9rem;
114
  max-width: 620px;
115
  line-height: 1.5;
116
  }
 
145
  }
146
 
147
  .input-group { display: flex; flex-direction: column; gap: 8px; }
148
+ label {
149
+ font-size: 0.8rem;
150
+ color: var(--text-dim);
151
  font-weight: 600;
152
  display: flex;
153
  align-items: center;
 
237
  background: rgba(0, 0, 0, 0.2);
238
  display: flex;
239
  flex-direction: column;
240
+ gap: 24px;
241
+ position: sticky;
242
+ top: 0;
243
+ max-height: 100vh;
244
+ overflow-y: auto;
245
  }
246
 
247
  .score-ring {
 
253
  place-items: center;
254
  background:
255
  radial-gradient(circle at center, rgba(0,0,0,0.55) 58%, transparent 60%),
256
+ conic-gradient(var(--ring-color, var(--accent-primary)) var(--p, 0%), rgba(255,255,255,0.08) 0);
257
  border: 1px solid var(--glass-border);
258
  box-shadow: 0 18px 40px rgba(0,0,0,0.35);
259
  position: relative;
260
+ transition: all 0.6s ease;
261
  }
262
 
263
  .score-core {
 
288
  text-align: center;
289
  padding: 0 10px;
290
  }
291
+ .score-core .sub-label {
292
+ font-size: 0.6rem;
293
+ color: var(--accent-primary);
294
+ margin-top: 2px;
295
+ }
296
 
297
  .status-badge {
298
  padding: 6px 16px;
 
308
  box-shadow: 0 10px 20px rgba(0, 0, 0, 0.3);
309
  transition: all 0.3s ease;
310
  }
311
+ .status-low {
312
+ background: var(--success);
313
  color: #000;
314
  box-shadow: 0 0 20px rgba(0, 242, 254, 0.4);
315
  }
316
+ .status-mid {
317
+ background: var(--warning);
318
  color: #000;
319
  box-shadow: 0 0 20px rgba(249, 212, 35, 0.4);
320
  }
321
+ .status-high {
322
+ background: var(--danger);
323
  color: #fff;
324
  box-shadow: 0 0 20px rgba(255, 75, 43, 0.4);
325
  }
326
 
327
+ .ci-bar {
328
+ width: 100%;
329
+ height: 8px;
330
+ background: rgba(255,255,255,0.08);
331
+ border-radius: 4px;
332
+ position: relative;
333
+ margin: 4px 0;
334
+ }
335
+ .ci-bar-fill {
336
+ position: absolute;
337
+ height: 100%;
338
+ border-radius: 4px;
339
+ background: linear-gradient(90deg, var(--success), var(--warning), var(--danger));
340
+ opacity: 0.6;
341
+ transition: all 0.5s ease;
342
+ }
343
+ .ci-bar-marker {
344
+ position: absolute;
345
+ width: 3px;
346
+ height: 14px;
347
+ top: -3px;
348
+ background: #fff;
349
+ border-radius: 2px;
350
+ transition: all 0.5s ease;
351
+ }
352
+ .ci-text {
353
+ display: flex;
354
+ justify-content: space-between;
355
+ font-size: 0.65rem;
356
+ color: var(--text-dim);
357
+ font-family: 'JetBrains Mono', monospace;
358
+ }
359
+
360
+ .metrics-list { display: flex; flex-direction: column; gap: 14px; }
361
  .metric-item {
362
  display: flex;
363
  justify-content: space-between;
364
  align-items: flex-end;
365
+ padding-bottom: 10px;
366
  border-bottom: 1px solid var(--glass-border);
367
  }
368
+ .metric-item .m-label { font-size: 0.82rem; color: var(--text-dim); }
369
  .metric-item .m-value {
370
  font-family: 'JetBrains Mono', monospace;
371
+ font-size: 1.15rem;
372
  color: #fff;
373
  }
374
  .metric-item .m-sub {
375
+ font-size: 0.62rem;
376
  color: var(--accent-primary);
377
  text-align: right;
378
  display: block;
 
392
  }
393
 
394
  .btn-calculate {
395
+ background: linear-gradient(135deg, var(--accent-primary), var(--accent-secondary));
396
  color: #000;
397
+ font-size: 0.95rem;
398
  }
399
 
400
  .btn-calculate:hover {
401
  transform: translateY(-2px);
402
+ box-shadow: 0 10px 25px rgba(0, 210, 255, 0.3);
403
+ }
404
+
405
+ .btn-calculate:disabled {
406
+ opacity: 0.5;
407
+ cursor: wait;
408
+ transform: none;
409
  }
410
 
411
  .btn-export {
412
  background: transparent;
413
  border: 1px solid var(--glass-border);
414
+ color: var(--text-dim);
415
+ font-size: 0.8rem;
 
416
  }
417
 
418
  .btn-export:hover {
 
419
  border-color: var(--accent-primary);
420
+ color: var(--accent-primary);
421
  }
422
 
423
  .references-section {
 
424
  padding: 32px 48px;
425
  border-top: 1px solid var(--glass-border);
426
+ background: rgba(0,0,0,0.15);
 
 
427
  }
 
428
  .references-section h3 {
429
+ font-size: 0.85rem;
430
  color: var(--accent-primary);
431
  margin-bottom: 16px;
 
 
 
432
  }
 
433
  .references-section ol {
 
434
  padding-left: 20px;
435
+ font-size: 0.72rem;
436
+ color: var(--text-dim);
437
+ line-height: 1.8;
438
  }
439
 
440
+ .fade-in {
441
+ animation: fadeIn 0.4s ease-out;
442
+ }
443
+ @keyframes fadeIn {
444
+ from { opacity: 0; transform: translateY(10px); }
445
+ to { opacity: 1; transform: translateY(0); }
446
  }
447
 
448
+ .model-badge {
449
+ display: inline-flex;
450
+ align-items: center;
451
+ gap: 6px;
452
+ background: rgba(0, 210, 255, 0.1);
453
+ border: 1px solid rgba(0, 210, 255, 0.3);
454
+ border-radius: 20px;
455
+ padding: 4px 12px;
456
+ font-size: 0.7rem;
457
+ color: var(--accent-primary);
458
+ margin-top: 8px;
459
+ }
460
+ .model-badge .dot {
461
+ width: 6px; height: 6px;
462
+ border-radius: 50%;
463
+ background: var(--accent-primary);
464
+ animation: pulse 2s infinite;
465
+ }
466
+ @keyframes pulse {
467
+ 0%, 100% { opacity: 1; }
468
+ 50% { opacity: 0.3; }
469
+ }
470
 
471
+ .error-banner {
472
+ background: rgba(239, 68, 68, 0.15);
473
+ border: 1px solid rgba(239, 68, 68, 0.4);
474
+ border-radius: 8px;
475
+ padding: 12px 16px;
476
+ font-size: 0.8rem;
477
+ color: #ff8a8a;
478
+ display: none;
479
  }
480
 
481
+ .comparison-table {
482
+ width: 100%;
483
+ border-collapse: collapse;
484
+ font-size: 0.72rem;
485
+ margin-top: 8px;
486
+ }
487
+ .comparison-table th, .comparison-table td {
488
+ padding: 6px 8px;
489
+ text-align: left;
490
+ border-bottom: 1px solid var(--glass-border);
491
+ }
492
+ .comparison-table th {
493
+ color: var(--accent-primary);
494
+ font-weight: 600;
495
+ text-transform: uppercase;
496
+ letter-spacing: 1px;
497
+ font-size: 0.65rem;
498
+ }
499
+ .comparison-table td {
500
+ color: var(--text-dim);
501
+ }
502
+ .comparison-table td:last-child {
503
+ font-family: 'JetBrains Mono', monospace;
504
+ color: #fff;
505
  }
506
  </style>
507
  </head>
 
508
  <body>
509
  <div class="vapor-field">
510
  <div class="vapor-cloud c1"></div>
 
514
 
515
  <main class="calculator-card">
516
  <div class="disclaimer-banner">
517
+ <strong>RESEARCH USE ONLY &mdash; NOT FOR CLINICAL DECISION-MAKING</strong>
518
+ This tool is for educational and research purposes. Predictions are generated by a Random Forest machine learning model (AUC 0.915). It has not been approved by any regulatory agency. Always rely on clinical judgment.
519
  </div>
520
 
521
  <div class="main-grid">
522
  <section class="input-region">
523
  <header>
524
  <h1>EVB Prognosis Calculator</h1>
525
+ <p>Machine learning-based risk calculator for acute esophageal variceal bleeding. Uses a calibrated Random Forest model trained on clinical data with 34 features.</p>
526
+ <div class="model-badge">
527
+ <span class="dot"></span>
528
+ ML Model &mdash; Random Forest (AUC 0.915)
529
+ </div>
530
  </header>
531
 
532
  <!-- Preset Scenarios -->
533
  <div class="section-tab">
534
+ <h2 class="section-title">Quick Start &mdash; Clinical Scenarios</h2>
535
  <div class="grid-layout">
536
  <div class="input-group preset-row">
537
  <label>Load Example Case</label>
 
546
  </div>
547
  </div>
548
 
549
+ <!-- Section 1: Demographics -->
550
  <div class="section-tab">
551
+ <h2 class="section-title">1. Demographics &amp; History</h2>
552
  <div class="grid-layout">
553
  <div class="input-group">
554
  <label for="age">Patient Age</label>
555
  <div class="slider-wrap">
556
+ <input type="range" id="age" min="18" max="100" value="50" />
557
+ <span class="val-display" id="age-val">50</span>
558
  </div>
559
  </div>
 
560
  <div class="input-group">
561
  <label for="sex">Sex</label>
562
  <select id="sex">
 
564
  <option value="female">Female</option>
565
  </select>
566
  </div>
 
567
  <div class="input-group">
568
+ <label for="race">Race*
569
+ <span class="info-icon" data-tip="Included as a model feature per original training data. May reflect social determinants of health rather than biological differences.">&#9432;</span>
570
  </label>
571
+ <select id="race">
572
+ <option value="white">White</option>
573
+ <option value="black">Black</option>
574
+ <option value="asian">Asian</option>
575
+ <option value="other">Other</option>
576
+ </select>
577
+ </div>
578
+ <div class="input-group">
579
+ <label for="etiology_cirrosis">Etiology of Cirrhosis
580
+ <span class="info-icon" data-tip="Primary cause of cirrhosis. Alcohol and HCV are most common in variceal bleeding.">&#9432;</span>
581
+ </label>
582
+ <select id="etiology_cirrosis">
583
  <option value="alcohol">Alcohol</option>
584
  <option value="hcv">HCV</option>
585
+ <option value="alcohol+hcv">Alcohol + HCV</option>
 
586
  <option value="other">Other</option>
587
  </select>
588
  </div>
589
+ </div>
590
+ </div>
591
 
592
+ <!-- Section 2: Clinical Status -->
593
+ <div class="section-tab">
594
+ <h2 class="section-title">2. Clinical Status</h2>
595
+ <div class="grid-layout">
596
+ <div class="input-group">
597
+ <label for="ascitis">Ascites
598
+ <span class="info-icon" data-tip="Fluid accumulation in abdomen. Marker of portal hypertension and hepatic decompensation.">&#9432;</span>
599
+ </label>
600
+ <select id="ascitis">
601
+ <option value="no">No</option>
602
+ <option value="yes" selected>Yes</option>
603
+ </select>
604
+ </div>
605
  <div class="input-group">
606
+ <label for="hepatorenal_syndrome">Hepatorenal Syndrome
607
+ <span class="info-icon" data-tip="Renal failure in the setting of advanced liver disease without other identifiable cause.">&#9432;</span>
608
  </label>
609
+ <select id="hepatorenal_syndrome">
610
+ <option value="no" selected>No</option>
611
+ <option value="yes">Yes</option>
 
612
  </select>
613
  </div>
614
+ <div class="input-group">
615
+ <label for="hepatocellular_carcinoma">Hepatocellular Carcinoma
616
+ <span class="info-icon" data-tip="Primary liver cancer, common in cirrhotic patients. Worsens prognosis.">&#9432;</span>
617
+ </label>
618
+ <select id="hepatocellular_carcinoma">
619
+ <option value="no" selected>No</option>
620
+ <option value="yes">Yes</option>
621
+ </select>
622
+ </div>
623
+ <div class="input-group">
624
+ <label for="portal_vein_thrombosis">Portal Vein Thrombosis
625
+ <span class="info-icon" data-tip="Blood clot in the portal vein. Increases portal hypertension and bleeding risk.">&#9432;</span>
626
+ </label>
627
+ <select id="portal_vein_thrombosis">
628
+ <option value="no" selected>No</option>
629
+ <option value="yes">Yes</option>
630
+ </select>
631
+ </div>
632
+ <div class="input-group">
633
+ <label for="dialisis">Dialysis
634
+ <span class="info-icon" data-tip="Dialysis &ge;2x in past week. Indicates severe renal impairment.">&#9432;</span>
635
+ </label>
636
+ <select id="dialisis">
637
+ <option value="no" selected>No</option>
638
+ <option value="yes">Yes</option>
639
+ </select>
640
+ </div>
641
+ </div>
642
+ </div>
643
 
644
+ <!-- Section 3: Medications -->
645
+ <div class="section-tab">
646
+ <h2 class="section-title">3. Current Medications</h2>
647
+ <div class="grid-layout">
648
  <div class="input-group">
649
+ <label for="omeprazole">Omeprazole (PPI)
650
+ <span class="info-icon" data-tip="Proton pump inhibitor. Used for gastric acid suppression.">&#9432;</span>
651
  </label>
652
+ <select id="omeprazole">
653
+ <option value="no" selected>No</option>
654
+ <option value="yes">Yes</option>
655
+ </select>
656
+ </div>
657
+ <div class="input-group">
658
+ <label for="spironolactone">Spironolactone
659
+ <span class="info-icon" data-tip="Aldosterone antagonist diuretic. Used for ascites management.">&#9432;</span>
660
+ </label>
661
+ <select id="spironolactone">
662
+ <option value="no">No</option>
663
+ <option value="yes" selected>Yes</option>
664
+ </select>
665
+ </div>
666
+ <div class="input-group">
667
+ <label for="furosemide">Furosemide
668
+ <span class="info-icon" data-tip="Loop diuretic. Used in combination with spironolactone for ascites.">&#9432;</span>
669
+ </label>
670
+ <select id="furosemide">
671
+ <option value="no">No</option>
672
+ <option value="yes" selected>Yes</option>
673
+ </select>
674
+ </div>
675
+ <div class="input-group">
676
+ <label for="propanolol">Propranolol
677
+ <span class="info-icon" data-tip="Non-selective beta-blocker. Used for portal hypertension prophylaxis.">&#9432;</span>
678
+ </label>
679
+ <select id="propanolol">
680
+ <option value="no" selected>No</option>
681
+ <option value="yes">Yes</option>
682
  </select>
683
  </div>
684
  </div>
685
  </div>
686
 
687
+ <!-- Section 4: Laboratory Profile -->
688
  <div class="section-tab">
689
+ <h2 class="section-title">4. Laboratory Profile</h2>
690
  <div class="grid-layout">
691
  <div class="input-group">
692
+ <label for="albumin">Albumin (g/dL)
693
+ <span class="info-icon" data-tip="Reflects hepatic synthetic function. Normal: 3.5-5.5 g/dL. Child-Pugh component.">&#9432;</span>
694
  </label>
695
  <div class="slider-wrap">
696
+ <input type="range" id="albumin" min="1" max="5" step="0.1" value="3.5" />
697
+ <span class="val-display" id="albumin-val">3.5</span>
698
+ </div>
699
+ </div>
700
+ <div class="input-group">
701
+ <label for="total_bilirrubin">Total Bilirubin (mg/dL)
702
+ <span class="info-icon" data-tip="Reflects hepatic dysfunction and cholestasis. Normal: 0.1-1.2 mg/dL. Major MELD component.">&#9432;</span>
703
+ </label>
704
+ <div class="slider-wrap">
705
+ <input type="range" id="total_bilirrubin" min="0.1" max="30" step="0.1" value="2.0" />
706
+ <span class="val-display" id="total_bilirrubin-val">2.0</span>
707
+ </div>
708
+ </div>
709
+ <div class="input-group">
710
+ <label for="direct_bilirrubina">Direct Bilirubin (mg/dL)
711
+ <span class="info-icon" data-tip="Conjugated bilirubin. Elevated in cholestatic conditions. Normal: 0.0-0.3 mg/dL.">&#9432;</span>
712
+ </label>
713
+ <div class="slider-wrap">
714
+ <input type="range" id="direct_bilirrubina" min="0.1" max="10" step="0.1" value="0.5" />
715
+ <span class="val-display" id="direct_bilirrubina-val">0.5</span>
716
  </div>
717
  </div>
 
718
  <div class="input-group">
719
  <label for="inr">INR
720
+ <span class="info-icon" data-tip="International Normalized Ratio. Measures coagulation. Normal: 0.8-1.2. Major MELD component.">&#9432;</span>
721
  </label>
722
  <div class="slider-wrap">
723
+ <input type="range" id="inr" min="0.5" max="5" step="0.1" value="1.2" />
724
+ <span class="val-display" id="inr-val">1.2</span>
725
  </div>
726
  </div>
 
727
  <div class="input-group">
728
  <label for="creatinine">Creatinine (mg/dL)
729
+ <span class="info-icon" data-tip="Serum creatinine reflects renal function. Normal: 0.6-1.2 mg/dL. Major MELD component.">&#9432;</span>
730
  </label>
731
  <div class="slider-wrap">
732
  <input type="range" id="creatinine" min="0.1" max="10" step="0.1" value="1.0" />
733
  <span class="val-display" id="creatinine-val">1.0</span>
734
  </div>
735
  </div>
 
736
  <div class="input-group">
737
+ <label for="sodium">Sodium (mEq/L)
738
+ <span class="info-icon" data-tip="Serum sodium. Hyponatremia indicates poor prognosis. Normal: 135-145 mEq/L.">&#9432;</span>
739
  </label>
740
  <div class="slider-wrap">
741
+ <input type="range" id="sodium" min="120" max="160" step="1" value="140" />
742
+ <span class="val-display" id="sodium-val">140</span>
743
  </div>
744
  </div>
 
745
  <div class="input-group">
746
+ <label for="potassium">Potassium (mEq/L)
747
+ <span class="info-icon" data-tip="Serum potassium. Normal: 3.5-5.0 mEq/L. Abnormalities common in liver disease.">&#9432;</span>
748
  </label>
749
  <div class="slider-wrap">
750
+ <input type="range" id="potassium" min="2" max="6" step="0.1" value="4.0" />
751
+ <span class="val-display" id="potassium-val">4.0</span>
752
  </div>
753
  </div>
 
754
  <div class="input-group">
755
+ <label for="platelets">Platelets (&times;10&sup3;/&mu;L)
756
+ <span class="info-icon" data-tip="Platelet count. Low platelets suggest portal hypertension/hypersplenism. Normal: 150-400.">&#9432;</span>
757
  </label>
758
+ <div class="slider-wrap">
759
+ <input type="range" id="platelets" min="10" max="500" step="1" value="150" />
760
+ <span class="val-display" id="platelets-val">150</span>
761
+ </div>
762
+ </div>
763
+ <div class="input-group">
764
+ <label for="hemoglobin">Hemoglobin (g/dL)
765
+ <span class="info-icon" data-tip="Blood hemoglobin. Low values indicate anemia/bleeding. Normal: 12-17 g/dL.">&#9432;</span>
766
+ </label>
767
+ <div class="slider-wrap">
768
+ <input type="range" id="hemoglobin" min="5" max="20" step="0.1" value="13" />
769
+ <span class="val-display" id="hemoglobin-val">13.0</span>
770
+ </div>
771
+ </div>
772
+ <div class="input-group">
773
+ <label for="hematocrit">Hematocrit (%)
774
+ <span class="info-icon" data-tip="Percentage of blood volume occupied by red blood cells. Normal: 36-48%.">&#9432;</span>
775
+ </label>
776
+ <div class="slider-wrap">
777
+ <input type="range" id="hematocrit" min="15" max="60" step="1" value="40" />
778
+ <span class="val-display" id="hematocrit-val">40</span>
779
+ </div>
780
+ </div>
781
+ <div class="input-group">
782
+ <label for="leucocytes">Leukocytes (&times;10&sup3;/&mu;L)
783
+ <span class="info-icon" data-tip="White blood cell count. Elevated in infection/inflammation. Normal: 4-11.">&#9432;</span>
784
+ </label>
785
+ <div class="slider-wrap">
786
+ <input type="range" id="leucocytes" min="1" max="50" step="0.1" value="6.0" />
787
+ <span class="val-display" id="leucocytes-val">6.0</span>
788
+ </div>
789
+ </div>
790
+ <div class="input-group">
791
+ <label for="ast">AST (U/L)
792
+ <span class="info-icon" data-tip="Aspartate aminotransferase. Liver enzyme. Normal: 10-40 U/L.">&#9432;</span>
793
+ </label>
794
+ <div class="slider-wrap">
795
+ <input type="range" id="ast" min="10" max="500" step="1" value="35" />
796
+ <span class="val-display" id="ast-val">35</span>
797
+ </div>
798
+ </div>
799
+ <div class="input-group">
800
+ <label for="alt">ALT (U/L)
801
+ <span class="info-icon" data-tip="Alanine aminotransferase. Liver enzyme. Normal: 7-56 U/L.">&#9432;</span>
802
+ </label>
803
+ <div class="slider-wrap">
804
+ <input type="range" id="alt" min="10" max="500" step="1" value="25" />
805
+ <span class="val-display" id="alt-val">25</span>
806
+ </div>
807
  </div>
808
  </div>
809
  </div>
810
 
811
+ <!-- Section 5: Endoscopic Findings & Bleeding -->
812
  <div class="section-tab">
813
+ <h2 class="section-title">5. Endoscopic Findings &amp; Bleeding Episode</h2>
814
  <div class="grid-layout">
815
  <div class="input-group">
816
+ <label for="varices">Varices Present
817
+ <span class="info-icon" data-tip="Presence of esophageal or gastric varices on endoscopy.">&#9432;</span>
818
  </label>
819
+ <select id="varices">
820
  <option value="no">No</option>
821
+ <option value="yes" selected>Yes</option>
822
+ </select>
823
+ </div>
824
+ <div class="input-group">
825
+ <label for="red_wale_marks">Red Wale Marks
826
+ <span class="info-icon" data-tip="Longitudinal red streaks on varices. Indicate high risk of rupture.">&#9432;</span>
827
+ </label>
828
+ <select id="red_wale_marks">
829
+ <option value="no" selected>No</option>
830
  <option value="yes">Yes</option>
831
  </select>
832
  </div>
 
833
  <div class="input-group">
834
+ <label for="rupture_point">Rupture Point Visible
835
+ <span class="info-icon" data-tip="Visible point of variceal rupture on endoscopy.">&#9432;</span>
836
  </label>
837
+ <select id="rupture_point">
838
+ <option value="no" selected>No</option>
839
+ <option value="yes">Yes</option>
840
+ </select>
841
  </div>
 
842
  <div class="input-group">
843
+ <label for="active_bleeding">Active Bleeding
844
+ <span class="info-icon" data-tip="Active bleeding or spurting at time of endoscopy.">&#9432;</span>
845
  </label>
846
+ <select id="active_bleeding">
847
+ <option value="no" selected>No</option>
848
+ <option value="yes">Yes</option>
849
+ </select>
850
  </div>
 
851
  <div class="input-group">
852
+ <label for="rebleeding">Rebleeding
853
+ <span class="info-icon" data-tip="Recurrence of bleeding after initial hemostasis.">&#9432;</span>
854
  </label>
855
+ <select id="rebleeding">
856
+ <option value="no" selected>No</option>
857
  <option value="yes">Yes</option>
858
  </select>
859
  </div>
860
+ <div class="input-group">
861
+ <label for="therapy">Endoscopic Therapy
862
+ <span class="info-icon" data-tip="Type of endoscopic treatment performed for variceal bleeding.">&#9432;</span>
863
+ </label>
864
+ <select id="therapy">
865
+ <option value="Banding" selected>Banding</option>
866
+ <option value="Sclerotherapy">Sclerotherapy</option>
867
+ <option value="No therapy">No therapy</option>
868
+ </select>
869
+ </div>
870
+ <div class="input-group">
871
+ <label for="terlipressin_dose">Terlipressin Dose (mg)
872
+ <span class="info-icon" data-tip="Vasoactive drug for variceal bleeding. Typical dose 1-2mg q4-6h initially.">&#9432;</span>
873
+ </label>
874
+ <div class="slider-wrap">
875
+ <input type="range" id="terlipressin_dose" min="0" max="20" step="0.5" value="2" />
876
+ <span class="val-display" id="terlipressin_dose-val">2.0</span>
877
+ </div>
878
+ </div>
879
+ <div class="input-group">
880
+ <label for="time_to_endoscophy_hours">Time to Endoscopy (hours)
881
+ <span class="info-icon" data-tip="Time from presentation to therapeutic endoscopy. Guidelines recommend &lt;12 hours.">&#9432;</span>
882
+ </label>
883
+ <div class="slider-wrap">
884
+ <input type="range" id="time_to_endoscophy_hours" min="0" max="48" step="1" value="12" />
885
+ <span class="val-display" id="time_to_endoscophy_hours-val">12</span>
886
+ </div>
887
+ </div>
888
  </div>
889
  </div>
890
  </section>
891
 
892
+ <!-- ============= RESULTS PANEL ============= -->
893
  <section class="result-region">
894
+ <button class="btn-calculate" id="btn-run">&#128302; Calculate Risk (ML Model)</button>
895
+
896
+ <div id="error-banner" class="error-banner"></div>
897
 
898
  <div id="results-content">
899
  <div class="score-ring" id="score-ring" style="--p:0%">
900
  <div class="score-core">
901
  <span class="pct" id="mortality-pct">--</span>
902
  <span class="label">1-Year Mortality</span>
903
+ <span class="sub-label" id="prediction-label"></span>
904
  </div>
905
  </div>
906
 
907
  <div id="risk-badge" class="status-badge">Awaiting Input</div>
908
 
909
+ <!-- Confidence Interval -->
910
+ <div id="ci-section" style="display:none;">
911
+ <div style="font-size:0.72rem; color:var(--text-dim); margin-bottom:4px;">95% Confidence Interval</div>
912
+ <div class="ci-bar">
913
+ <div class="ci-bar-fill" id="ci-fill"></div>
914
+ <div class="ci-bar-marker" id="ci-marker"></div>
915
+ </div>
916
+ <div class="ci-text">
917
+ <span id="ci-lower">--</span>
918
+ <span id="ci-upper">--</span>
919
+ </div>
920
+ </div>
921
+
922
  <div class="metrics-list">
923
  <div class="metric-item">
924
  <span class="m-label">MELD Score</span>
 
927
  <span class="m-sub" id="meld-mort">3-mo mortality: --</span>
928
  </div>
929
  </div>
 
930
  <div class="metric-item">
931
  <span class="m-label">MELD-Na</span>
932
  <div>
 
934
  <span class="m-sub">Sodium adjusted</span>
935
  </div>
936
  </div>
 
937
  <div class="metric-item">
938
  <span class="m-label">Child-Pugh</span>
939
  <div>
 
941
  <span class="m-sub" id="cp-class">Class --</span>
942
  </div>
943
  </div>
 
944
  <div class="metric-item">
945
  <span class="m-label">ALBI Score</span>
946
  <div>
 
949
  </div>
950
  </div>
951
  </div>
952
+
953
+ <!-- Model Comparison -->
954
+ <div id="comparison-section" style="display:none;">
955
+ <div style="font-size:0.72rem; color:var(--accent-primary); text-transform:uppercase; letter-spacing:1px; margin-bottom:8px;">Model Performance</div>
956
+ <table class="comparison-table">
957
+ <thead>
958
+ <tr><th>Model</th><th>AUC</th></tr>
959
+ </thead>
960
+ <tbody>
961
+ <tr><td>Random Forest (this model)</td><td>0.915</td></tr>
962
+ <tr><td>MELD-Na</td><td>0.742</td></tr>
963
+ <tr><td>MELD</td><td>0.726</td></tr>
964
+ <tr><td>Child-Pugh</td><td>0.685</td></tr>
965
+ </tbody>
966
+ </table>
967
+ </div>
968
  </div>
969
 
970
+ <button class="btn-export" id="btn-export">&#128202; Export Results (JSON)</button>
971
 
972
+ <p style="font-size: 0.62rem; color: var(--text-dim); line-height: 1.5; text-align: center; margin-top: 8px;">
973
+ <strong>Citation:</strong> Rech MM, Soldera J, Corso LL et al. Development, Internal and Prospective validation of a machine learning model for the prediction of mortality in cirrhotic patients with acute esophageal variceal bleeding. <em>World J Hepatol</em>. 2025.<br>
974
+ <strong>Contact:</strong> mmrech@ucs.br
975
  </p>
976
  </section>
977
  </div>
978
 
979
  <section class="references-section">
980
+ <h3>&#128218; Clinical References</h3>
981
  <ol>
982
+ <li>Kamath PS, et al. A model to predict survival in patients with end-stage liver disease. <em>Hepatology</em>. 2001;33(2):464-470.</li>
983
+ <li>Biggins SW, et al. Serum sodium predicts mortality in patients listed for liver transplantation. <em>Hepatology</em>. 2005;41(1):32-39.</li>
984
+ <li>Pugh RN, et al. Transection of the oesophagus for bleeding oesophageal varices. <em>Br J Surg</em>. 1973;60(8):646-649.</li>
985
+ <li>Johnson PJ, et al. Assessment of liver function in patients with hepatocellular carcinoma: the ALBI grade. <em>J Clin Oncol</em>. 2015;33(6):550-558.</li>
986
+ <li>Garcia-Tsao G, et al. Prevention and management of gastroesophageal varices and variceal hemorrhage in cirrhosis. <em>Hepatology</em>. 2007;46(3):922-938.</li>
987
+ <li>de Franchis R, et al. Baveno VII &ndash; Renewing consensus in portal hypertension. <em>J Hepatol</em>. 2022;76(4):959-974.</li>
988
  </ol>
989
  </section>
990
  </main>
991
 
992
  <script>
993
  const $ = (id) => document.getElementById(id);
994
+ const API_BASE = window.location.origin;
995
 
996
  function clamp(n, a, b) { return Math.min(Math.max(n, a), b); }
997
 
998
  function syncDisplay(el) {
999
  const out = $(el.id + '-val');
1000
+ if (out) out.innerText = parseFloat(el.value).toFixed(el.step && el.step.includes('.') ? el.step.split('.')[1].length : 0);
1001
  }
1002
 
1003
+ // ============= TRADITIONAL SCORE CALCULATIONS (local) =============
1004
 
1005
+ function meldScore(bili, inr, creat) {
 
1006
  const b = Math.max(bili, 1);
1007
  const i = Math.max(inr, 1);
1008
+ const c = Math.min(Math.max(creat, 1), 4.0);
 
 
 
 
 
 
 
 
1009
  let meld = 3.78 * Math.log(b) + 11.2 * Math.log(i) + 9.57 * Math.log(c) + 6.43;
1010
+ return clamp(Math.round(meld), 6, 40);
 
1011
  }
1012
 
1013
  function meldNaScore(meld, sodium) {
 
1014
  const s = clamp(sodium, 125, 137);
1015
  const delta = 137 - s;
1016
  let meldNa = meld + 1.32 * delta - (0.033 * meld * delta);
1017
+ return clamp(Math.round(meldNa), 6, 40);
 
1018
  }
1019
 
1020
+ function childPugh(bili, alb, inr, asc) {
1021
  let pts = 0;
 
 
1022
  pts += (bili < 2) ? 1 : (bili <= 3 ? 2 : 3);
 
 
1023
  pts += (alb > 3.5) ? 1 : (alb >= 2.8 ? 2 : 3);
 
 
1024
  pts += (inr < 1.7) ? 1 : (inr <= 2.3 ? 2 : 3);
1025
+ pts += (asc === 'no') ? 1 : 3;
1026
+ // Encephalopathy not in the 34-feature model, default to 1 point
1027
+ pts += 1;
 
 
 
 
1028
  const cls = (pts <= 6) ? 'A' : (pts <= 9 ? 'B' : 'C');
1029
  return { pts, cls };
1030
  }
1031
 
1032
  function albiScore(albumin, bilirubin) {
 
1033
  const score = (Math.log10(bilirubin) * 0.66) + (albumin * -0.085);
 
1034
  let grade;
1035
  if (score <= -2.60) grade = '1 (Best)';
1036
  else if (score <= -1.39) grade = '2';
1037
  else grade = '3 (Worst)';
 
1038
  return { score: score.toFixed(2), grade };
1039
  }
1040
 
1041
+ // ============= GRADIO API CALL =============
1042
+
1043
+ async function callGradioAPI() {
1044
+ // Collect all 34 inputs in the exact order the Gradio function expects
1045
+ const data = [
1046
+ parseInt($('age').value), // age
1047
+ $('sex').value, // sex
1048
+ $('race').value, // race
1049
+ $('etiology_cirrosis').value, // etiology_cirrosis
1050
+ $('hepatorenal_syndrome').value, // hepatorenal_syndrome
1051
+ $('omeprazole').value, // omeprazole
1052
+ $('spironolactone').value, // spironolactone
1053
+ $('furosemide').value, // furosemide
1054
+ $('propanolol').value, // propanolol
1055
+ $('dialisis').value, // dialisis
1056
+ $('portal_vein_thrombosis').value, // portal_vein_thrombosis
1057
+ $('ascitis').value, // ascitis
1058
+ $('hepatocellular_carcinoma').value, // hepatocellular_carcinoma
1059
+ parseFloat($('albumin').value), // albumin
1060
+ parseFloat($('total_bilirrubin').value), // total_bilirrubin
1061
+ parseFloat($('direct_bilirrubina').value), // direct_bilirrubina
1062
+ parseFloat($('inr').value), // inr
1063
+ parseFloat($('creatinine').value), // creatinine
1064
+ parseFloat($('platelets').value), // platelets
1065
+ parseFloat($('ast').value), // ast
1066
+ parseFloat($('alt').value), // alt
1067
+ parseFloat($('hemoglobin').value), // hemoglobin
1068
+ parseFloat($('hematocrit').value), // hematocrit
1069
+ parseFloat($('leucocytes').value), // leucocytes
1070
+ parseFloat($('sodium').value), // sodium
1071
+ parseFloat($('potassium').value), // potassium
1072
+ $('varices').value, // varices
1073
+ $('red_wale_marks').value, // red_wale_marks
1074
+ $('rupture_point').value, // rupture_point
1075
+ $('active_bleeding').value, // active_bleeding
1076
+ $('therapy').value, // therapy
1077
+ parseFloat($('terlipressin_dose').value), // terlipressin_dose
1078
+ parseFloat($('time_to_endoscophy_hours').value), // time_to_endoscophy_hours
1079
+ $('rebleeding').value // rebleeding
1080
+ ];
1081
+
1082
+ // Gradio 4.x API: POST /call/predict_patient_outcome then GET event stream
1083
+ const callResp = await fetch(`${API_BASE}/call/predict_patient_outcome`, {
1084
+ method: 'POST',
1085
+ headers: { 'Content-Type': 'application/json' },
1086
+ body: JSON.stringify({ data })
1087
+ });
1088
+
1089
+ if (!callResp.ok) {
1090
+ throw new Error(`API call failed: ${callResp.status} ${callResp.statusText}`);
1091
  }
1092
+
1093
+ const callResult = await callResp.json();
1094
+ const eventId = callResult.event_id;
1095
+
1096
+ // Poll the event stream for the result
1097
+ const resultResp = await fetch(`${API_BASE}/call/predict_patient_outcome/${eventId}`);
1098
+ const text = await resultResp.text();
1099
+
1100
+ // Parse SSE response: find the "data:" line with the JSON array
1101
+ const lines = text.split('\n');
1102
+ for (const line of lines) {
1103
+ if (line.startsWith('data: ')) {
1104
+ const payload = JSON.parse(line.substring(6));
1105
+ return payload; // Array of 3 markdown strings
1106
+ }
1107
  }
1108
+ throw new Error('No data received from API');
1109
+ }
1110
+
1111
+ // ============= PARSE ML OUTPUT =============
1112
+
1113
+ function parseMLOutput(mlMarkdown) {
1114
+ const result = { probability: null, ciLower: null, ciUpper: null, prediction: null, riskCategory: null };
1115
+
1116
+ // Extract probability: "**Mortality Probability:** 45.2% (95% CI: 30.2% - 60.2%)"
1117
+ const probMatch = mlMarkdown.match(/Mortality Probability:\*\*\s*([\d.]+)%\s*\(95% CI:\s*([\d.]+)%\s*-\s*([\d.]+)%\)/);
1118
+ if (probMatch) {
1119
+ result.probability = parseFloat(probMatch[1]) / 100;
1120
+ result.ciLower = parseFloat(probMatch[2]) / 100;
1121
+ result.ciUpper = parseFloat(probMatch[3]) / 100;
1122
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1123
 
1124
+ // Extract prediction
1125
+ if (mlMarkdown.includes('Death within 1 year')) {
1126
+ result.prediction = 'Death within 1 year';
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1127
  } else {
1128
+ result.prediction = 'Survival beyond 1 year';
 
1129
  }
1130
 
1131
+ // Extract risk category
1132
+ const riskMatch = mlMarkdown.match(/(Low Risk|Moderate Risk|High Risk)/);
1133
+ if (riskMatch) result.riskCategory = riskMatch[1];
 
 
1134
 
1135
+ return result;
1136
+ }
1137
+
1138
+ function parseTraditionalScores(tradMarkdown) {
1139
+ const result = { meld: null, meldNa: null, childPugh: null, cpClass: null };
1140
+
1141
+ const meldMatch = tradMarkdown.match(/MELD Score:\*\*\s*(\d+)/);
1142
+ if (meldMatch) result.meld = parseInt(meldMatch[1]);
1143
+
1144
+ const meldNaMatch = tradMarkdown.match(/MELD-Na Score:\*\*\s*(\d+)/);
1145
+ if (meldNaMatch) result.meldNa = parseInt(meldNaMatch[1]);
1146
+
1147
+ const cpMatch = tradMarkdown.match(/Child-Pugh Score:\*\*\s*(\d+)\s*\(Class\s*([ABC])\)/);
1148
+ if (cpMatch) {
1149
+ result.childPugh = parseInt(cpMatch[1]);
1150
+ result.cpClass = cpMatch[2];
1151
+ }
1152
+
1153
+ return result;
1154
+ }
1155
+
1156
+ // ============= MAIN RENDER =============
1157
+
1158
+ async function render() {
1159
+ const btn = $('btn-run');
1160
+ const errorBanner = $('error-banner');
1161
+ errorBanner.style.display = 'none';
1162
+
1163
+ btn.disabled = true;
1164
+ btn.textContent = '⏳ Calculating...';
1165
+
1166
+ try {
1167
+ // Call the ML model API
1168
+ const apiResult = await callGradioAPI();
1169
+ const [mlOutput, tradOutput, compOutput] = apiResult;
1170
+
1171
+ const ml = parseMLOutput(mlOutput);
1172
+ const trad = parseTraditionalScores(tradOutput);
1173
+
1174
+ // Animate results
1175
+ const results = $('results-content');
1176
+ results.classList.remove('fade-in');
1177
+ void results.offsetWidth;
1178
+ results.classList.add('fade-in');
1179
+
1180
+ // Update mortality display
1181
+ if (ml.probability !== null) {
1182
+ const pct = (ml.probability * 100).toFixed(1);
1183
+ $('mortality-pct').innerText = pct + '%';
1184
+ $('score-ring').style.setProperty('--p', pct + '%');
1185
+ $('prediction-label').innerText = ml.prediction || '';
1186
+
1187
+ // Color the ring based on risk
1188
+ if (ml.probability < 0.3) {
1189
+ $('score-ring').style.setProperty('--ring-color', 'var(--success)');
1190
+ } else if (ml.probability < 0.6) {
1191
+ $('score-ring').style.setProperty('--ring-color', 'var(--warning)');
1192
+ } else {
1193
+ $('score-ring').style.setProperty('--ring-color', 'var(--danger)');
1194
+ }
1195
+
1196
+ // Confidence interval
1197
+ if (ml.ciLower !== null) {
1198
+ $('ci-section').style.display = 'block';
1199
+ $('ci-lower').innerText = (ml.ciLower * 100).toFixed(1) + '%';
1200
+ $('ci-upper').innerText = (ml.ciUpper * 100).toFixed(1) + '%';
1201
+ $('ci-fill').style.left = (ml.ciLower * 100) + '%';
1202
+ $('ci-fill').style.width = ((ml.ciUpper - ml.ciLower) * 100) + '%';
1203
+ $('ci-marker').style.left = (ml.probability * 100) + '%';
1204
+ }
1205
+ }
1206
+
1207
+ // Risk badge
1208
+ const badge = $('risk-badge');
1209
+ if (ml.riskCategory === 'Low Risk') {
1210
+ badge.innerText = 'Low Risk';
1211
+ badge.className = 'status-badge status-low';
1212
+ } else if (ml.riskCategory === 'Moderate Risk') {
1213
+ badge.innerText = 'Moderate Risk';
1214
+ badge.className = 'status-badge status-mid';
1215
+ } else {
1216
+ badge.innerText = 'High Risk';
1217
+ badge.className = 'status-badge status-high';
1218
+ }
1219
+
1220
+ // Traditional scores — use API values if available, else compute locally
1221
+ const bili = parseFloat($('total_bilirrubin').value);
1222
+ const inr = parseFloat($('inr').value);
1223
+ const creat = parseFloat($('creatinine').value);
1224
+ const sod = parseFloat($('sodium').value);
1225
+ const alb = parseFloat($('albumin').value);
1226
+ const asc = $('ascitis').value;
1227
+
1228
+ const localMeld = meldScore(bili, inr, creat);
1229
+ const localMeldNa = meldNaScore(localMeld, sod);
1230
+ const localCp = childPugh(bili, alb, inr, asc);
1231
+ const localAlbi = albiScore(alb, bili);
1232
+
1233
+ $('meld-score').innerText = trad.meld || localMeld;
1234
+ $('meld-na-score').innerText = trad.meldNa || localMeldNa;
1235
+ $('cp-score').innerText = trad.childPugh || localCp.pts;
1236
+ $('cp-class').innerText = 'Class ' + (trad.cpClass || localCp.cls);
1237
+ $('albi-score').innerText = localAlbi.score;
1238
+ $('albi-grade').innerText = 'Grade ' + localAlbi.grade;
1239
+
1240
+ const meldVal = trad.meld || localMeld;
1241
+ const meldMort = meldVal < 10 ? '<10%' : meldVal < 20 ? '10-19%' : meldVal < 30 ? '20-50%' : '>50%';
1242
+ $('meld-mort').innerText = '3-mo mortality: ' + meldMort;
1243
+
1244
+ // Show comparison
1245
+ $('comparison-section').style.display = 'block';
1246
+
1247
+ } catch (err) {
1248
+ console.error('API Error:', err);
1249
+ errorBanner.textContent = '⚠ API Error: ' + err.message + '. Falling back to local score calculation.';
1250
+ errorBanner.style.display = 'block';
1251
+
1252
+ // Fallback: compute local scores only
1253
+ const bili = parseFloat($('total_bilirrubin').value);
1254
+ const inr = parseFloat($('inr').value);
1255
+ const creat = parseFloat($('creatinine').value);
1256
+ const sod = parseFloat($('sodium').value);
1257
+ const alb = parseFloat($('albumin').value);
1258
+ const asc = $('ascitis').value;
1259
+
1260
+ const meld = meldScore(bili, inr, creat);
1261
+ const meldNa = meldNaScore(meld, sod);
1262
+ const cp = childPugh(bili, alb, inr, asc);
1263
+ const albi = albiScore(alb, bili);
1264
+
1265
+ $('meld-score').innerText = meld;
1266
+ $('meld-na-score').innerText = meldNa;
1267
+ $('cp-score').innerText = cp.pts;
1268
+ $('cp-class').innerText = 'Class ' + cp.cls;
1269
+ $('albi-score').innerText = albi.score;
1270
+ $('albi-grade').innerText = 'Grade ' + albi.grade;
1271
+ $('meld-mort').innerText = '3-mo mortality: ' + (meld < 10 ? '<10%' : meld < 20 ? '10-19%' : meld < 30 ? '20-50%' : '>50%');
1272
+
1273
+ $('mortality-pct').innerText = 'N/A';
1274
+ $('risk-badge').innerText = 'API Unavailable';
1275
+ $('risk-badge').className = 'status-badge';
1276
+ } finally {
1277
+ btn.disabled = false;
1278
+ btn.textContent = '🔮 Calculate Risk (ML Model)';
1279
+ }
1280
  }
1281
 
1282
  // ============= PRESETS =============
1283
 
1284
  const PRESETS = {
1285
  compensated: {
1286
+ age: 55, sex: 'male', race: 'white', etiology_cirrosis: 'alcohol',
1287
+ hepatorenal_syndrome: 'no', omeprazole: 'no', spironolactone: 'no', furosemide: 'no',
1288
+ propanolol: 'yes', dialisis: 'no', portal_vein_thrombosis: 'no',
1289
+ ascitis: 'no', hepatocellular_carcinoma: 'no',
1290
+ albumin: 3.8, total_bilirrubin: 1.2, direct_bilirrubina: 0.3, inr: 1.1,
1291
+ creatinine: 0.9, platelets: 180, ast: 28, alt: 22,
1292
+ hemoglobin: 14, hematocrit: 42, leucocytes: 5.5, sodium: 140, potassium: 4.2,
1293
+ varices: 'yes', red_wale_marks: 'no', rupture_point: 'no',
1294
+ active_bleeding: 'no', rebleeding: 'no', therapy: 'Banding',
1295
+ terlipressin_dose: 2, time_to_endoscophy_hours: 8
1296
  },
1297
  decompensated: {
1298
+ age: 62, sex: 'male', race: 'white', etiology_cirrosis: 'alcohol',
1299
+ hepatorenal_syndrome: 'no', omeprazole: 'yes', spironolactone: 'yes', furosemide: 'yes',
1300
+ propanolol: 'no', dialisis: 'no', portal_vein_thrombosis: 'no',
1301
+ ascitis: 'yes', hepatocellular_carcinoma: 'no',
1302
+ albumin: 3.0, total_bilirrubin: 2.8, direct_bilirrubina: 1.2, inr: 1.6,
1303
+ creatinine: 1.3, platelets: 95, ast: 52, alt: 38,
1304
+ hemoglobin: 10.5, hematocrit: 32, leucocytes: 7.2, sodium: 134, potassium: 4.5,
1305
+ varices: 'yes', red_wale_marks: 'yes', rupture_point: 'no',
1306
+ active_bleeding: 'yes', rebleeding: 'no', therapy: 'Banding',
1307
+ terlipressin_dose: 2, time_to_endoscophy_hours: 14
1308
  },
1309
  advanced: {
1310
+ age: 58, sex: 'male', race: 'white', etiology_cirrosis: 'alcohol+hcv',
1311
+ hepatorenal_syndrome: 'no', omeprazole: 'yes', spironolactone: 'yes', furosemide: 'yes',
1312
+ propanolol: 'no', dialisis: 'no', portal_vein_thrombosis: 'yes',
1313
+ ascitis: 'yes', hepatocellular_carcinoma: 'no',
1314
+ albumin: 2.4, total_bilirrubin: 5.2, direct_bilirrubina: 2.8, inr: 2.4,
1315
+ creatinine: 2.1, platelets: 55, ast: 120, alt: 85,
1316
+ hemoglobin: 8.5, hematocrit: 26, leucocytes: 12.0, sodium: 128, potassium: 5.1,
1317
+ varices: 'yes', red_wale_marks: 'yes', rupture_point: 'yes',
1318
+ active_bleeding: 'yes', rebleeding: 'yes', therapy: 'Banding',
1319
+ terlipressin_dose: 4, time_to_endoscophy_hours: 18
1320
  },
1321
  hrs: {
1322
+ age: 64, sex: 'male', race: 'white', etiology_cirrosis: 'alcohol',
1323
+ hepatorenal_syndrome: 'yes', omeprazole: 'yes', spironolactone: 'yes', furosemide: 'yes',
1324
+ propanolol: 'no', dialisis: 'yes', portal_vein_thrombosis: 'no',
1325
+ ascitis: 'yes', hepatocellular_carcinoma: 'no',
1326
+ albumin: 2.7, total_bilirrubin: 3.5, direct_bilirrubina: 1.8, inr: 1.8,
1327
+ creatinine: 3.2, platelets: 72, ast: 68, alt: 45,
1328
+ hemoglobin: 9.2, hematocrit: 28, leucocytes: 9.5, sodium: 130, potassium: 5.3,
1329
+ varices: 'yes', red_wale_marks: 'yes', rupture_point: 'no',
1330
+ active_bleeding: 'yes', rebleeding: 'no', therapy: 'Banding',
1331
+ terlipressin_dose: 4, time_to_endoscophy_hours: 10
1332
  }
1333
  };
1334
 
1335
  function loadPreset(name) {
1336
  if (!name || !PRESETS[name]) return;
 
1337
  const preset = PRESETS[name];
1338
  Object.keys(preset).forEach(key => {
1339
  const el = $(key);
 
1349
 
1350
  function exportResults() {
1351
  const timestamp = new Date().toISOString();
1352
+ const allInputIds = [
1353
+ 'age','sex','race','etiology_cirrosis','hepatorenal_syndrome','omeprazole',
1354
+ 'spironolactone','furosemide','propanolol','dialisis','portal_vein_thrombosis',
1355
+ 'ascitis','hepatocellular_carcinoma','albumin','total_bilirrubin',
1356
+ 'direct_bilirrubina','inr','creatinine','platelets','ast','alt','hemoglobin',
1357
+ 'hematocrit','leucocytes','sodium','potassium','varices','red_wale_marks',
1358
+ 'rupture_point','active_bleeding','therapy','terlipressin_dose',
1359
+ 'time_to_endoscophy_hours','rebleeding'
1360
+ ];
1361
+
1362
+ const inputs = {};
1363
+ allInputIds.forEach(id => {
1364
+ const el = $(id);
1365
+ if (el) {
1366
+ const val = el.value;
1367
+ inputs[id] = isNaN(val) || val === '' ? val : parseFloat(val);
1368
+ }
1369
+ });
1370
+
1371
  const data = {
1372
  timestamp,
1373
  disclaimer: "FOR RESEARCH/EDUCATIONAL USE ONLY - NOT MEDICAL ADVICE",
1374
+ calculatorVersion: "3.0-ML-Clinical",
1375
+ model: "Random Forest with Isotonic Calibration (AUC 0.915)",
1376
+ inputs,
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1377
  results: {
1378
  estimatedMortality1Year: $('mortality-pct').innerText,
1379
  riskCategory: $('risk-badge').innerText,
1380
+ confidenceInterval: {
1381
+ lower: $('ci-lower').innerText,
1382
+ upper: $('ci-upper').innerText
1383
+ },
1384
  scores: {
1385
  MELD: $('meld-score').innerText,
1386
  MELDNa: $('meld-na-score').innerText,
 
1390
  ALBIGrade: $('albi-grade').innerText.replace('Grade ', '')
1391
  }
1392
  },
1393
+ citation: "Rech MM, Soldera J, Corso LL et al. World J Hepatol. 2025."
1394
  };
1395
+
1396
  const blob = new Blob([JSON.stringify(data, null, 2)], {type: 'application/json'});
1397
  const url = URL.createObjectURL(blob);
1398
  const a = document.createElement('a');
1399
  a.href = url;
1400
+ a.download = `evb_ml_risk_assessment_${timestamp.split('T')[0]}_${Date.now()}.json`;
 
1401
  a.click();
1402
  URL.revokeObjectURL(url);
1403
  }
 
1405
  // ============= INITIALIZATION =============
1406
 
1407
  function wireLiveUpdates() {
1408
+ const allIds = [
1409
+ 'age','sex','race','etiology_cirrosis','hepatorenal_syndrome','omeprazole',
1410
+ 'spironolactone','furosemide','propanolol','dialisis','portal_vein_thrombosis',
1411
+ 'ascitis','hepatocellular_carcinoma','albumin','total_bilirrubin',
1412
+ 'direct_bilirrubina','inr','creatinine','platelets','ast','alt','hemoglobin',
1413
+ 'hematocrit','leucocytes','sodium','potassium','varices','red_wale_marks',
1414
+ 'rupture_point','active_bleeding','therapy','terlipressin_dose',
1415
+ 'time_to_endoscophy_hours','rebleeding'
1416
  ];
1417
 
1418
+ allIds.forEach(id => {
1419
  const el = $(id);
1420
  if (!el) return;
 
1421
  const handler = () => {
1422
  if (el.type === 'range') syncDisplay(el);
1423
  };
 
1424
  el.addEventListener('input', handler);
1425
  el.addEventListener('change', handler);
 
1426
  if (el.type === 'range') syncDisplay(el);
1427
  });
1428