Spaces:
Sleeping
Sleeping
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width,initial-scale=1"> | |
| <title>RetViM — Deep Research & Clinical Intelligence</title> | |
| <link href="https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@300;400;500;600;700&family=Space+Mono:wght@400;700&family=Playfair+Display:ital,wght@0,400;0,700;1,400&display=swap" rel="stylesheet"> | |
| <style> | |
| *,*::before,*::after{box-sizing:border-box;margin:0;padding:0} | |
| :root{ | |
| --bg:#080B0F;--bg2:#0D1117;--bg3:#111820;--bg4:#151E28; | |
| --teal:#00C9B8;--teal2:rgba(0,201,184,.15);--teal3:rgba(0,201,184,.08); | |
| --amber:#FFB547;--red:#FF5E5E;--grn:#52E58A;--blue:#5EA0FF;--purple:#B47FFF; | |
| --w100:rgba(255,255,255,1);--w70:rgba(255,255,255,.7);--w40:rgba(255,255,255,.4);--w15:rgba(255,255,255,.15);--w08:rgba(255,255,255,.08);--w04:rgba(255,255,255,.04); | |
| --border:rgba(255,255,255,.08);--border2:rgba(255,255,255,.15); | |
| --mono:'Space Mono',monospace;--sans:'Space Grotesk',sans-serif;--serif:'Playfair Display',serif; | |
| } | |
| html{scroll-behavior:smooth} | |
| body{background:#080B0F;font-family:'Space Grotesk',system-ui,sans-serif;color:rgba(255,255,255,.7);-webkit-font-smoothing:antialiased;overflow-x:hidden} | |
| /* ── NAV ── */ | |
| nav{position:fixed;top:0;left:0;right:0;z-index:999;height:56px;display:flex;align-items:center;justify-content:space-between;padding:0 40px;background:rgba(8,11,15,.88);backdrop-filter:blur(20px);border-bottom:1px solid rgba(255,255,255,.08)} | |
| .logo{font-family:'Playfair Display',Georgia,serif;font-size:22px;font-weight:700;text-decoration:none;color:#ffffff;letter-spacing:-.02em}.logo b{color:#00C9B8;font-weight:400;font-style:italic} | |
| .nav-r{display:flex;align-items:center;gap:28px} | |
| .nav-r a{font-size:13px;color:rgba(255,255,255,.4);text-decoration:none;letter-spacing:.04em;text-transform:uppercase;transition:color .2s} | |
| .nav-r a:hover{color:#ffffff} | |
| .nav-cta{color:#080B0F!important;background:#00C9B8;padding:7px 20px;border-radius:20px;font-weight:600;letter-spacing:.02em!important;text-transform:none!important} | |
| .nav-cta:hover{opacity:.85} | |
| /* ── API STATUS INDICATOR ── */ | |
| .api-status{display:inline-flex;align-items:center;gap:6px;font-family:'Space Mono',monospace;font-size:10px;letter-spacing:.05em;padding:4px 10px;border-radius:20px;border:1px solid rgba(255,255,255,.08);background:rgba(255,255,255,.03);color:rgba(255,255,255,.5);transition:all .4s;cursor:default;white-space:nowrap} | |
| .api-status.live{border-color:rgba(82,229,138,.3);background:rgba(82,229,138,.06);color:#52E58A} | |
| .api-status.demo{border-color:rgba(255,255,255,.08);color:rgba(255,255,255,.35)} | |
| .api-pulse{width:6px;height:6px;border-radius:50%;background:currentColor;flex-shrink:0} | |
| .api-status.live .api-pulse{animation:blink 2s infinite} | |
| .live-model-badge{display:none;align-items:center;gap:4px;font-family:'Space Mono',monospace;font-size:9px;color:#52E58A;letter-spacing:.05em;background:rgba(82,229,138,.1);border:1px solid rgba(82,229,138,.2);border-radius:5px;padding:2px 6px;margin-top:5px} | |
| .live-model-badge.show{display:inline-flex} | |
| /* ── HERO ── */ | |
| .hero{min-height:100vh;position:relative;display:flex;flex-direction:column;align-items:center;justify-content:center;padding:80px 40px 60px;overflow:hidden;background:#080B0F} | |
| .hero-bg{position:absolute;inset:0;background:radial-gradient(ellipse 80% 60% at 50% 40%,rgba(0,201,184,.06) 0%,transparent 70%)} | |
| #heroCanvas{position:absolute;inset:0;width:100%;height:100%;pointer-events:none;opacity:.4} | |
| .hero-inner{position:relative;z-index:1;text-align:center;max-width:1000px} | |
| .hero-eyebrow{display:inline-flex;align-items:center;gap:8px;font-family:'Space Mono',monospace;font-size:11px;color:#00C9B8;letter-spacing:.12em;text-transform:uppercase;margin-bottom:28px;padding:6px 16px;border:1px solid rgba(0,201,184,.3);border-radius:20px;background:rgba(0,201,184,.06)} | |
| .live-dot{width:6px;height:6px;background:#00C9B8;border-radius:50%;animation:blink 2s infinite} | |
| @keyframes blink{0%,100%{opacity:1}50%{opacity:.2}} | |
| h1.hero-title{font-family:'Playfair Display',Georgia,serif;font-size:clamp(52px,8vw,108px);font-weight:700;line-height:.9;letter-spacing:-.03em;color:#ffffff;margin-bottom:28px} | |
| h1.hero-title em{font-style:italic;color:#00C9B8} | |
| .hero-sub{font-size:18px;font-weight:300;line-height:1.7;color:rgba(255,255,255,.4);max-width:580px;margin:0 auto 52px} | |
| .hero-metrics{display:flex;align-items:center;justify-content:center;gap:44px;margin-bottom:56px;flex-wrap:wrap} | |
| .hm{text-align:center}.hm-v{font-family:'Space Mono',monospace;font-size:38px;font-weight:700;color:#ffffff;line-height:1}.hm-v span{font-size:18px;color:rgba(255,255,255,.4)}.hm-l{font-size:10px;color:rgba(255,255,255,.4);letter-spacing:.1em;text-transform:uppercase;margin-top:5px} | |
| .hm-sep{width:1px;height:44px;background:rgba(255,255,255,.15)} | |
| .hero-btns{display:flex;align-items:center;justify-content:center;gap:12px} | |
| .btn{display:inline-flex;align-items:center;gap:8px;font-family:'Space Grotesk',system-ui,sans-serif;font-size:14px;font-weight:600;padding:13px 28px;border-radius:30px;text-decoration:none;border:none;cursor:pointer;transition:all .2s;letter-spacing:.01em} | |
| .btn-primary{background:#00C9B8;color:#080B0F}.btn-primary:hover{opacity:.85;transform:translateY(-1px)} | |
| .btn-ghost{border:1px solid rgba(255,255,255,.15);color:rgba(255,255,255,.7);background:transparent}.btn-ghost:hover{background:rgba(255,255,255,.08)} | |
| .scroll-hint{position:absolute;bottom:32px;left:50%;transform:translateX(-50%);display:flex;flex-direction:column;align-items:center;gap:8px;color:rgba(255,255,255,.4);font-size:11px;letter-spacing:.08em;text-transform:uppercase} | |
| .scroll-line{width:1px;height:40px;background:linear-gradient(to bottom,transparent,rgba(255,255,255,.4));animation:scrollPulse 2s ease-in-out infinite} | |
| @keyframes scrollPulse{0%,100%{opacity:.3}50%{opacity:1}} | |
| /* ── SECTION LAYOUT ── */ | |
| .section{padding:100px 40px;background:#080B0F} | |
| .section-dark{background:#0D1117 !important} | |
| .section-darker{background:#111820 !important} | |
| .section-inner{max-width:1400px;margin:0 auto} | |
| .sec-eye{font-family:'Space Mono',monospace;font-size:11px;color:#00C9B8;letter-spacing:.1em;text-transform:uppercase;margin-bottom:12px} | |
| .sec-head{font-family:'Playfair Display',Georgia,serif;font-size:clamp(32px,4vw,56px);font-weight:700;color:#ffffff;line-height:1.05;letter-spacing:-.02em;margin-bottom:16px} | |
| .sec-head em{font-style:italic;color:#00C9B8} | |
| .sec-sub{font-size:16px;color:rgba(255,255,255,.4);line-height:1.7;max-width:600px;margin-bottom:56px} | |
| /* ── DEMO SECTION ── */ | |
| .mode-bar{display:flex;align-items:center;gap:16px;margin-bottom:28px;flex-wrap:wrap} | |
| /* ── DUAL-MODE INTELLIGENCE SWITCH ───────────────────────── */ | |
| .mode-switch{position:relative;display:inline-flex;background:rgba(8,14,20,.7);border:1px solid rgba(255,255,255,.1);border-radius:22px;padding:4px;gap:0;backdrop-filter:blur(24px);-webkit-backdrop-filter:blur(24px);box-shadow:0 4px 24px rgba(0,0,0,.4),inset 0 1px 0 rgba(255,255,255,.07)} | |
| .mode-switch::before{content:'';position:absolute;inset:0;border-radius:22px;background:linear-gradient(135deg,rgba(0,201,184,.06) 0%,transparent 55%);pointer-events:none;z-index:0} | |
| .ms-slider{position:absolute;top:4px;height:calc(100% - 8px);background:linear-gradient(135deg,#00C9B8 0%,#00b8a9 100%);border-radius:18px;transition:left .38s cubic-bezier(.4,0,.2,1),width .38s cubic-bezier(.4,0,.2,1);box-shadow:0 0 28px rgba(0,201,184,.38),0 0 8px rgba(0,201,184,.55),inset 0 1px 0 rgba(255,255,255,.25);z-index:0} | |
| .ms-opt{position:relative;z-index:1;display:flex;align-items:center;gap:10px;padding:10px 22px;border:none;background:transparent;cursor:pointer;border-radius:18px;white-space:nowrap;transition:all .28s;user-select:none} | |
| .ms-opt:hover:not(.on) .ms-name{color:rgba(255,255,255,.75)} | |
| .ms-opt:hover:not(.on) .ms-ico-w{background:rgba(255,255,255,.1)} | |
| .ms-ico-w{width:30px;height:30px;display:flex;align-items:center;justify-content:center;border-radius:9px;background:rgba(255,255,255,.07);transition:all .28s;flex-shrink:0} | |
| .ms-opt.on .ms-ico-w{background:rgba(8,11,15,.18)} | |
| .ms-ico{color:rgba(255,255,255,.38);transition:color .28s,transform .28s} | |
| .ms-opt.on .ms-ico{color:#080B0F} | |
| .ms-opt:hover:not(.on) .ms-ico{color:rgba(255,255,255,.65);transform:scale(1.08)} | |
| .ms-labels{display:flex;flex-direction:column;align-items:flex-start;gap:2px} | |
| .ms-name{font-family:'Space Grotesk',system-ui,sans-serif;font-size:13px;font-weight:600;color:rgba(255,255,255,.4);transition:color .28s;line-height:1;letter-spacing:.01em} | |
| .ms-opt.on .ms-name{color:#080B0F;font-weight:700} | |
| .ms-sub{font-family:'Space Mono',monospace;font-size:8px;color:rgba(255,255,255,.2);letter-spacing:.05em;transition:color .28s;line-height:1.3;text-transform:uppercase} | |
| .ms-opt.on .ms-sub{color:rgba(8,11,15,.55)} | |
| /* legacy compat */ | |
| .mtab{display:none} | |
| .mode-hint{font-family:'Space Mono',monospace;font-size:10px;color:rgba(255,255,255,.32);letter-spacing:.05em;transition:all .3s} | |
| .demo-grid{display:grid;grid-template-columns:300px 1fr;gap:16px;align-items:start} | |
| /* PANEL */ | |
| .panel{background:#111820;border-radius:16px;border:1px solid rgba(255,255,255,.08);overflow:hidden} | |
| .ph{padding:12px 16px;border-bottom:1px solid rgba(255,255,255,.08);display:flex;align-items:center;justify-content:space-between} | |
| .ph-t{font-family:'Space Mono',monospace;font-size:10px;color:rgba(255,255,255,.4);letter-spacing:.08em;text-transform:uppercase} | |
| .ph-s{display:flex;align-items:center;gap:5px;font-family:'Space Mono',monospace;font-size:10px;color:rgba(255,255,255,.4)} | |
| .sdot{width:5px;height:5px;border-radius:50%;background:rgba(255,255,255,.2);flex-shrink:0} | |
| .sdot.on{background:#52E58A}.sdot.go{background:#FFB547;animation:blink .8s infinite} | |
| .pb{padding:14px} | |
| /* Upload */ | |
| .upzone{border:1.5px dashed rgba(255,255,255,.15);border-radius:10px;padding:28px 14px;text-align:center;cursor:pointer;position:relative;transition:all .2s;margin-bottom:10px} | |
| .upzone:hover,.upzone.drag{border-color:#00C9B8;background:rgba(0,201,184,.05)} | |
| .upzone input{position:absolute;inset:0;opacity:0;cursor:pointer;font-size:0} | |
| .upzone-ico{width:32px;height:32px;background:rgba(0,201,184,.12);border-radius:8px;margin:0 auto 9px;display:grid;place-items:center} | |
| .upzone-t{font-size:12px;font-weight:600;color:rgba(255,255,255,.7);margin-bottom:3px}.upzone-s{font-size:11px;color:rgba(255,255,255,.4)} | |
| .samp-lbl{font-family:'Space Mono',monospace;font-size:9px;color:rgba(255,255,255,.4);letter-spacing:.08em;text-transform:uppercase;margin-bottom:7px} | |
| .samp-g{display:grid;grid-template-columns:repeat(4,1fr);gap:5px;margin-bottom:10px} | |
| .samp-i{cursor:pointer} | |
| .samp-cv{width:100%;aspect-ratio:4/3;border-radius:6px;display:block;border:1.5px solid transparent;transition:all .15s;object-fit:cover;background:#0d1620} | |
| .samp-cv:hover{border-color:#00C9B8} | |
| .samp-cv.sel{border-color:#00C9B8;box-shadow:0 0 0 3px rgba(0,201,184,.2)} | |
| .samp-cl{font-family:'Space Mono',monospace;font-size:8px;text-align:center;color:rgba(255,255,255,.4);text-transform:uppercase;margin-top:3px} | |
| .analyze-btn{width:100%;font-family:'Space Grotesk',system-ui,sans-serif;font-size:13px;font-weight:700;color:#080B0F;background:#00C9B8;border:none;border-radius:9px;padding:11px;cursor:pointer;display:flex;align-items:center;justify-content:center;gap:6px;transition:all .2s} | |
| .analyze-btn:hover{opacity:.85}.analyze-btn:disabled{background:rgba(255,255,255,.08);color:rgba(255,255,255,.4);cursor:not-allowed} | |
| #prevWrap{display:none;margin-bottom:10px} | |
| #prevWrap canvas{width:100%;border-radius:8px;display:block;border:1px solid rgba(255,255,255,.08)} | |
| #prevMeta{font-family:'Space Mono',monospace;font-size:9px;color:rgba(255,255,255,.4);margin-top:4px} | |
| .inf-prog{display:none;margin-top:10px;padding:9px;background:#151E28;border-radius:8px} | |
| .inf-st{font-family:'Space Mono',monospace;font-size:9px;color:rgba(255,255,255,.4);padding:2px 0;opacity:.3;display:flex;align-items:center;gap:5px} | |
| .inf-st.cur{opacity:1;color:#00C9B8}.inf-st.done{opacity:.5} | |
| .inf-dot{width:3px;height:3px;border-radius:50%;background:currentColor;flex-shrink:0} | |
| /* Results tabs — Apple / Linear precision */ | |
| .tab-bar{display:flex;padding:0;border-bottom:1px solid rgba(255,255,255,.07);overflow-x:auto;scrollbar-width:none;gap:0;background:transparent} | |
| .tab-bar::-webkit-scrollbar{display:none} | |
| .rtab{font-family:'Space Grotesk',system-ui,sans-serif;font-size:11px;font-weight:500;padding:11px 16px;border:none;background:transparent;cursor:pointer;color:rgba(255,255,255,.35);border-bottom:2px solid transparent;transition:color .16s,border-color .16s;white-space:nowrap;letter-spacing:.07em;text-transform:uppercase;display:flex;align-items:center;gap:6px;margin-bottom:-1px;position:relative} | |
| .rtab svg{flex-shrink:0;opacity:.55;transition:opacity .16s} | |
| .rtab.on{color:rgba(255,255,255,.92);border-bottom-color:#00C9B8} | |
| .rtab.on svg{opacity:1} | |
| .rtab:hover:not(.on){color:rgba(255,255,255,.62)} | |
| .rtab:hover:not(.on) svg{opacity:.75} | |
| .rtab.tab-computing{position:relative} | |
| .rtab.tab-computing::after{content:'';position:absolute;top:4px;right:4px;width:5px;height:5px;border-radius:50%;border:1.5px solid rgba(0,201,184,.5);border-top-color:#00C9B8;animation:rtabspin .6s linear infinite} | |
| @keyframes rtabspin{to{transform:rotate(360deg)}} | |
| /* State panels */ | |
| .st-empty{display:grid;place-items:center;padding:80px;min-height:400px;text-align:center;color:rgba(255,255,255,.4);min-height:500px;gap:10px} | |
| .st-load{display:none;flex-direction:column;align-items:center;justify-content:center;padding:40px 20px;min-height:500px;gap:0;position:relative;overflow:hidden} | |
| .neural-terminal{width:100%;max-width:520px;background:#000;border:1px solid rgba(0,201,184,.3);border-radius:12px;padding:0;overflow:hidden;box-shadow:0 0 40px rgba(0,201,184,.08),0 0 80px rgba(0,201,184,.04)} | |
| .nt-header{background:rgba(0,201,184,.07);border-bottom:1px solid rgba(0,201,184,.15);padding:10px 16px;display:flex;align-items:center;gap:10px} | |
| .nt-dots{display:flex;gap:5px}.nt-dot{width:9px;height:9px;border-radius:50%} | |
| .nt-title{font-family:'Space Mono',monospace;font-size:10px;color:rgba(0,201,184,.7);letter-spacing:.1em;text-transform:uppercase;flex:1;text-align:center} | |
| .nt-body{padding:16px 20px;min-height:220px} | |
| .nt-cmd{font-family:'Space Mono',monospace;font-size:10px;color:rgba(0,201,184,.5);letter-spacing:.04em;margin-bottom:8px} | |
| .nt-steps{display:flex;flex-direction:column;gap:3px} | |
| .inf-st{font-family:'Space Mono',monospace;font-size:10px;color:rgba(255,255,255,.3);padding:3px 0;display:flex;align-items:center;gap:8px;transition:all .2s} | |
| .inf-st.cur{color:#00C9B8}.inf-st.done{color:rgba(255,255,255,.35)} | |
| .inf-dot{font-size:9px;flex-shrink:0;width:12px} | |
| .inf-st.cur .inf-dot::before{content:'>';color:#00C9B8;animation:blink .5s infinite} | |
| .inf-st.done .inf-dot::before{content:'[OK]';font-size:8px;color:rgba(82,229,138,.7)} | |
| .inf-st:not(.cur):not(.done) .inf-dot::before{content:'...';color:rgba(255,255,255,.2)} | |
| .nt-prog{margin-top:12px;padding-top:10px;border-top:1px solid rgba(255,255,255,.06)} | |
| .nt-bar-bg{height:2px;background:rgba(255,255,255,.08);border-radius:1px;overflow:hidden;margin-bottom:5px} | |
| .nt-bar-f{height:100%;background:linear-gradient(90deg,#00C9B8,#52E58A);border-radius:1px;transition:width .4s ease;width:0%} | |
| .nt-stat{font-family:'Space Mono',monospace;font-size:9px;color:rgba(255,255,255,.3);display:flex;justify-content:space-between} | |
| .lmsg{font-family:'Space Mono',monospace;font-size:10px;color:#00C9B8;margin-top:8px;letter-spacing:.04em} | |
| .lsteps{width:100%;display:flex;flex-direction:column;gap:3px} | |
| @keyframes scan{0%{transform:translateY(-100%)}100%{transform:translateY(100%)}} | |
| @keyframes ntpulse{0%,100%{opacity:.6}50%{opacity:1}} | |
| /* TAB PANES */ | |
| .tab-pane{display:none;animation:fu .3s ease;position:relative} | |
| .tab-pane.on{display:block} | |
| @keyframes fu{from{opacity:0;transform:translateY(4px)}to{opacity:1;transform:none}} | |
| /* In-tab computing overlay */ | |
| .tco{position:absolute;inset:0;display:flex;flex-direction:column;align-items:center;justify-content:center;background:rgba(8,11,15,.88);backdrop-filter:blur(6px);border-radius:10px;z-index:20;gap:18px;pointer-events:none} | |
| .tco-ring{width:48px;height:48px;border:3px solid rgba(0,201,184,.15);border-top-color:#00C9B8;border-radius:50%;animation:rtabspin .75s linear infinite} | |
| .tco-title{font-family:'Space Mono',monospace;font-size:12px;color:rgba(255,255,255,.75);letter-spacing:.08em;text-transform:uppercase} | |
| .tco-sub{font-size:11px;color:rgba(255,255,255,.38);font-family:'Space Grotesk',sans-serif;text-align:center;max-width:260px;line-height:1.55} | |
| .tco-dots{display:flex;gap:6px}.tco-dots span{width:6px;height:6px;border-radius:50%;background:#00C9B8;animation:tcobounce 1.2s ease-in-out infinite} | |
| .tco-dots span:nth-child(2){animation-delay:.2s}.tco-dots span:nth-child(3){animation-delay:.4s} | |
| @keyframes tcobounce{0%,80%,100%{opacity:.2;transform:scale(.7)}40%{opacity:1;transform:scale(1)}} | |
| .pane-body{padding:16px} | |
| /* CARD GRIDS */ | |
| .cg{display:grid;gap:10px;margin-bottom:12px} | |
| .cg2{grid-template-columns:1fr 1fr}.cg3{grid-template-columns:1fr 1fr 1fr} | |
| .card{background:#151E28;border-radius:10px;padding:12px;border:1px solid rgba(255,255,255,.08)} | |
| .card-t{font-family:'Space Mono',monospace;font-size:9px;color:rgba(255,255,255,.4);letter-spacing:.07em;text-transform:uppercase;margin-bottom:9px} | |
| .card-sub{font-size:10px;color:rgba(255,255,255,.4);line-height:1.5;margin-top:6px} | |
| /* Confidence bars */ | |
| .cbar{display:flex;align-items:center;gap:7px;margin-bottom:6px} | |
| .cbar-n{font-family:'Space Mono',monospace;font-size:10px;width:52px;color:rgba(255,255,255,.7);flex-shrink:0} | |
| .cbar-bg{flex:1;height:3px;background:rgba(255,255,255,.08);border-radius:2px;overflow:hidden} | |
| .cbar-f{height:100%;border-radius:2px;transition:width 1s cubic-bezier(.16,1,.3,1)} | |
| .cbar-v{font-family:'Space Mono',monospace;font-size:10px;width:46px;text-align:right;color:rgba(255,255,255,.4)} | |
| .mrow{display:flex;align-items:center;justify-content:space-between;padding:4px 0;border-bottom:1px solid rgba(255,255,255,.04)} | |
| .mrow:last-child{border:none} | |
| .mk{font-family:'Space Mono',monospace;font-size:10px;color:rgba(255,255,255,.4)}.mv{font-family:'Space Mono',monospace;font-size:10.5px;font-weight:700;color:#ffffff} | |
| .mv.hi{color:#00C9B8} | |
| /* RESEARCHER - XAI tabs */ | |
| .xai-row{display:flex;gap:5px;flex-wrap:wrap;margin-bottom:10px} | |
| .xtab{font-size:10px;font-weight:600;padding:4px 9px;border-radius:5px;border:1px solid rgba(255,255,255,.08);background:transparent;cursor:pointer;color:rgba(255,255,255,.4);transition:all .18s} | |
| .xtab.on{background:#00C9B8;color:#080B0F;border-color:#00C9B8} | |
| .pred-h{display:flex;align-items:flex-start;justify-content:space-between;padding-bottom:12px;margin-bottom:12px;border-bottom:1px solid rgba(255,255,255,.08)} | |
| .pred-name{font-family:'Playfair Display',Georgia,serif;font-size:22px;font-weight:700;color:#ffffff;line-height:1.1} | |
| .pred-pct{font-family:'Space Mono',monospace;font-size:13px;font-weight:700;color:#00C9B8} | |
| .pred-k{font-family:'Space Mono',monospace;font-size:10px;color:rgba(255,255,255,.4);margin-top:2px} | |
| /* DOCTOR specific */ | |
| .dx-banner{border-radius:11px;padding:16px;display:flex;align-items:flex-start;gap:12px;margin-bottom:12px} | |
| .dx-banner.cnv{background:rgba(255,94,94,.12);border:1px solid rgba(255,94,94,.2)} | |
| .dx-banner.dme{background:rgba(255,181,71,.12);border:1px solid rgba(255,181,71,.2)} | |
| .dx-banner.drusen{background:rgba(255,196,100,.1);border:1px solid rgba(255,196,100,.2)} | |
| .dx-banner.normal{background:rgba(82,229,138,.1);border:1px solid rgba(82,229,138,.2)} | |
| .dx-ico{width:44px;height:44px;border-radius:10px;display:grid;place-items:center;flex-shrink:0;font-family:'Space Mono',monospace;font-size:9px;font-weight:700;letter-spacing:.08em;color:rgba(255,255,255,.85)} | |
| .dx-ico.cnv{background:rgba(255,94,94,.22);border:1px solid rgba(255,94,94,.35)}.dx-ico.dme{background:rgba(255,181,71,.22);border:1px solid rgba(255,181,71,.35)}.dx-ico.drusen{background:rgba(200,168,48,.2);border:1px solid rgba(200,168,48,.32)}.dx-ico.normal{background:rgba(82,229,138,.18);border:1px solid rgba(82,229,138,.3)} | |
| .dx-name{font-family:'Space Grotesk',system-ui,sans-serif;font-size:18px;font-weight:700;color:#ffffff;margin-bottom:3px;letter-spacing:-.01em} | |
| .dx-conf{font-size:12px;font-weight:600;color:#00C9B8}.dx-icd{font-family:'Space Mono',monospace;font-size:9.5px;color:rgba(255,255,255,.4);margin-top:2px} | |
| .sev-bg{height:6px;background:rgba(255,255,255,.08);border-radius:3px;overflow:hidden;margin:7px 0 4px} | |
| .sev-f{height:100%;border-radius:3px;transition:width 1.1s cubic-bezier(.16,1,.3,1)} | |
| .sev-ticks{display:flex;justify-content:space-between;font-family:'Space Mono',monospace;font-size:8px;color:rgba(255,255,255,.4)} | |
| .clin-t{font-family:'Space Mono',monospace;font-size:9px;color:#00C9B8;letter-spacing:.08em;text-transform:uppercase;margin-bottom:6px} | |
| .clin-p{font-size:12.5px;line-height:1.7;color:rgba(255,255,255,.7)} | |
| .rec-box{background:rgba(0,201,184,.08);border:1px solid rgba(0,201,184,.2);border-radius:9px;padding:12px 14px;display:flex;gap:9px;align-items:flex-start;margin-top:10px} | |
| .rec-ht{font-size:11px;font-weight:700;color:#00C9B8;margin-bottom:3px} | |
| .rec-txt{font-size:12px;color:rgba(0,201,184,.8);line-height:1.55} | |
| .anat-i{display:flex;align-items:center;gap:8px;padding:6px 8px;border-radius:7px;border:1px solid rgba(255,255,255,.08);margin-bottom:4px} | |
| .anat-i.aff{background:rgba(255,94,94,.1);border-color:rgba(255,94,94,.2)} | |
| .anat-i.mild{background:rgba(255,181,71,.08);border-color:rgba(255,181,71,.2)} | |
| .anat-i.norm{background:rgba(82,229,138,.06);border-color:rgba(82,229,138,.15)} | |
| .anat-d{width:8px;height:8px;border-radius:2px;flex-shrink:0} | |
| .anat-n{font-size:11.5px;font-weight:600;color:#ffffff;flex:1}.anat-f{font-size:10.5px;color:rgba(255,255,255,.4);flex:2} | |
| .anat-tag{font-family:'Space Mono',monospace;font-size:8px;font-weight:700;padding:2px 6px;border-radius:4px} | |
| .anat-tag.aff{background:rgba(255,94,94,.3);color:#FF5E5E}.anat-tag.mild{background:rgba(255,181,71,.25);color:#FFB547}.anat-tag.norm{background:rgba(82,229,138,.2);color:#52E58A} | |
| .tp-step{display:flex;align-items:flex-start;gap:10px;padding:9px 0;border-bottom:1px solid rgba(255,255,255,.08)} | |
| .tp-step:last-child{border:none} | |
| .tp-ico{width:28px;height:28px;border-radius:7px;display:grid;place-items:center;flex-shrink:0;font-family:'Space Mono',monospace;font-size:9px;font-weight:700;color:rgba(255,255,255,.7);letter-spacing:.02em} | |
| .tp-t{font-size:12px;font-weight:700;color:#ffffff;display:flex;align-items:center;gap:7px;margin-bottom:3px} | |
| .tp-d{font-size:11px;color:rgba(255,255,255,.4);line-height:1.55} | |
| .upill{font-family:'Space Mono',monospace;font-size:8px;font-weight:700;padding:2px 6px;border-radius:6px} | |
| .u-urgent{background:rgba(255,94,94,.2);color:#FF5E5E}.u-moderate{background:rgba(255,181,71,.2);color:#FFB547}.u-routine{background:rgba(82,229,138,.18);color:#52E58A} | |
| /* ── RESEARCH GALLERY (real figures) ── */ | |
| .gallery-section{padding:100px 0} | |
| .fig-full { | |
| width: 100%; | |
| border-radius: 16px; | |
| border: 1px solid rgba(255,255,255,.08); | |
| display: block; | |
| object-fit: cover; | |
| background-color: #111820; /* dark fallback */ | |
| min-height: 280px; | |
| } | |
| .fig-caption{font-family:'Space Mono',monospace;font-size:11px;color:rgba(255,255,255,.4);margin-top:10px;text-align:center;letter-spacing:.04em} | |
| .fig-row{display:grid;gap:20px;margin-bottom:32px} | |
| .fig-row.c2{grid-template-columns:1fr 1fr} | |
| .fig-row.c3{grid-template-columns:1fr 1fr 1fr} | |
| .fig-wrap{position:relative;overflow:hidden;border-radius:16px;border:1px solid rgba(255,255,255,.08)} | |
| .fig-wrap img{width:100%;display:block;transition:transform .4s ease} | |
| .fig-wrap:hover img{transform:scale(1.02)} | |
| .fig-label{position:absolute;top:12px;left:12px;font-family:'Space Mono',monospace;font-size:10px;font-weight:700;letter-spacing:.08em;text-transform:uppercase;padding:4px 10px;border-radius:20px;backdrop-filter:blur(10px)} | |
| .fig-label.blue{background:rgba(0,201,184,.85);color:#080B0F} | |
| .fig-label.purple{background:rgba(180,127,255,.85);color:#080B0F} | |
| .fig-label.amber{background:rgba(255,181,71,.85);color:#080B0F} | |
| .fig-label.red{background:rgba(255,94,94,.85);color:#080B0F} | |
| .fig-label.green{background:rgba(82,229,138,.85);color:#080B0F} | |
| /* ── STATS ── */ | |
| .stats-section{background:linear-gradient(135deg,#050810 0%,#0A1520 50%,#050810 100%);padding:90px 40px;position:relative;overflow:hidden} | |
| .stats-glow{position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);width:600px;height:300px;background:radial-gradient(ellipse,rgba(0,201,184,.08) 0%,transparent 70%);pointer-events:none} | |
| .stats-inner{max-width:1240px;margin:0 auto;position:relative} | |
| .stats-head-wrap{margin-bottom:56px} | |
| .stats-g{display:grid;grid-template-columns:repeat(4,1fr);gap:1px;border:1px solid rgba(255,255,255,.08);border-radius:14px;overflow:hidden} | |
| .sc{background:rgba(255,255,255,.02);padding:32px 24px;text-align:center;transition:background .2s;cursor:default} | |
| .sc:hover{background:rgba(0,201,184,.04)} | |
| .sc-v{font-family:'Space Mono',monospace;font-size:48px;font-weight:700;color:#ffffff;line-height:1;margin-bottom:6px}.sc-u{font-size:22px;color:rgba(255,255,255,.4)} | |
| .sc-l{font-size:12px;color:rgba(255,255,255,.4);line-height:1.4;font-weight:400} | |
| /* ── LEGEND ── */ | |
| .legend{display:flex;gap:14px;flex-wrap:wrap;margin-top:6px} | |
| .li{display:flex;align-items:center;gap:5px;font-family:'Space Mono',monospace;font-size:10px;color:rgba(255,255,255,.7)} | |
| .li-dot{width:8px;height:8px;border-radius:50%;flex-shrink:0} | |
| .li-line{width:12px;height:2px;border-radius:1px;flex-shrink:0} | |
| /* ── FOOTER ── */ | |
| footer{background:#0D1117;border-top:1px solid rgba(255,255,255,.08);padding:28px 40px;display:flex;align-items:center;justify-content:space-between} | |
| .fi{font-family:'Space Mono',monospace;font-size:10px;color:rgba(255,255,255,.4);text-align:right;line-height:1.7;letter-spacing:.02em} | |
| /* SCROLL FADE */ | |
| .sf{opacity:0;transform:translateY(18px);transition:opacity .6s,transform .6s} | |
| .sf.vis{opacity:1;transform:none} | |
| .d1{transition-delay:.1s}.d2{transition-delay:.2s}.d3{transition-delay:.3s} | |
| /* RESEARCH SECTION TABS */ | |
| .res-tab-bar{display:flex;gap:0;margin-bottom:32px;border-bottom:1px solid rgba(255,255,255,.08);flex-wrap:wrap} | |
| .res-tab{font-family:'Space Grotesk',system-ui,sans-serif;font-size:12px;font-weight:500;padding:11px 18px;border:none;border-bottom:2px solid transparent;background:transparent;cursor:pointer;color:rgba(255,255,255,.38);transition:color .2s,border-color .2s;letter-spacing:.01em;margin-bottom:-1px;white-space:nowrap} | |
| .res-tab:hover{color:rgba(255,255,255,.72)} | |
| .res-tab.on{color:rgba(255,255,255,.95);border-bottom-color:#00C9B8;font-weight:600} | |
| .res-content{display:none}.res-content.on{display:block;animation:fu .3s ease} | |
| /* DIFF GRID */ | |
| .diff-g{display:grid;grid-template-columns:1fr 1fr;gap:6px} | |
| .diff-c{border-radius:7px;padding:9px;border:1px solid rgba(255,255,255,.08);background:#151E28} | |
| .diff-n{font-size:11px;font-weight:700;margin-bottom:3px} | |
| .diff-p{font-family:'Space Mono',monospace;font-size:11px;font-weight:700;margin-top:4px} | |
| .diff-note{font-size:10px;color:rgba(255,255,255,.4);line-height:1.4} | |
| /* MAMBA GRID */ | |
| #mambaGrid{display:grid;grid-template-columns:repeat(14,1fr);gap:2px;max-width:300px;margin:0 auto} | |
| .mg-cell{aspect-ratio:1;border-radius:2px;transition:background .06s} | |
| .scan-btns{display:flex;gap:4px;flex-wrap:wrap;margin-top:8px;justify-content:center} | |
| .sbtn{font-family:'Space Mono',monospace;font-size:9px;font-weight:700;padding:3px 8px;border-radius:5px;border:1px solid rgba(255,255,255,.08);background:transparent;cursor:pointer;color:rgba(255,255,255,.4);transition:all .18s} | |
| .sbtn.on{background:#00C9B8;color:#080B0F;border-color:#00C9B8} | |
| @media(max-width:960px){ | |
| .demo-grid{grid-template-columns:1fr} | |
| .stats-g{grid-template-columns:1fr 1fr} | |
| nav{padding:0 16px}.nav-r a:not(.nav-cta){display:none} | |
| .section,.stats-section{padding-left:16px;padding-right:16px} | |
| .cg2,.cg3,.fig-row.c2,.fig-row.c3{grid-template-columns:1fr} | |
| } | |
| html, body { background: #080B0F !important; } | |
| section { background: #080B0F; } | |
| .section-dark, section.section-dark { background: #0D1117 !important; } | |
| .section-darker, section.section-darker { background: #111820 !important; } | |
| .stats-section { background: linear-gradient(135deg,#050810 0%,#0A1520 50%,#050810 100%) !important; } | |
| footer { background: #0D1117 !important; } | |
| .panel { background: #111820 !important; } | |
| .card { background: #151E28 !important; } | |
| .upzone { border-color: rgba(255,255,255,.12) !important; } | |
| html.light-theme { transition: filter 0.4s ease; filter: invert(1) hue-rotate(180deg); } | |
| html.light-theme img, html.light-theme canvas.samp-cv, html.light-theme #prevCv, | |
| html.light-theme #r1orig, html.light-theme #r1map, html.light-theme #r1overlay, html.light-theme #r1toppx, | |
| html.light-theme #d1an, html.light-theme #d2or, html.light-theme #d2gc, | |
| html.light-theme #r2patch, html.light-theme #r3conv, html.light-theme #r3ssm { filter: invert(1) hue-rotate(180deg); } | |
| /* ── NEURAL JOURNEY DUAL-MODE ─────────────────────────────── */ | |
| .jcls-bar{display:flex;gap:7px;flex-wrap:wrap} | |
| .jcls-btn{font-family:'Space Grotesk',sans-serif;font-size:12px;font-weight:700;padding:8px 18px;border-radius:30px;border:1.5px solid rgba(255,255,255,.12);background:transparent;cursor:pointer;color:rgba(255,255,255,.45);transition:all .22s;letter-spacing:.04em;text-transform:uppercase} | |
| .jcls-btn:hover{color:rgba(255,255,255,.85);border-color:rgba(255,255,255,.3);transform:translateY(-1px)} | |
| .jcls-btn.on[data-cls=CNV]{background:rgba(255,94,94,.15);color:#FF8080;border-color:rgba(255,94,94,.5);box-shadow:0 0 16px rgba(255,94,94,.12)} | |
| .jcls-btn.on[data-cls=DME]{background:rgba(255,181,71,.15);color:#FFB547;border-color:rgba(255,181,71,.5);box-shadow:0 0 16px rgba(255,181,71,.12)} | |
| .jcls-btn.on[data-cls=DRUSEN]{background:rgba(180,127,255,.15);color:#C49FFF;border-color:rgba(180,127,255,.5);box-shadow:0 0 16px rgba(180,127,255,.12)} | |
| .jcls-btn.on[data-cls=NORMAL]{background:rgba(82,229,138,.1);color:#52E58A;border-color:rgba(82,229,138,.45);box-shadow:0 0 16px rgba(82,229,138,.1)} | |
| .jmode-bar{display:inline-flex;background:rgba(255,255,255,.04);border:1px solid rgba(255,255,255,.08);border-radius:40px;padding:3px;gap:2px} | |
| .jmode-btn{font-family:'Space Grotesk',sans-serif;font-size:12px;font-weight:600;padding:7px 18px;border-radius:30px;border:none;cursor:pointer;color:rgba(255,255,255,.4);background:transparent;transition:all .22s;letter-spacing:.01em;white-space:nowrap} | |
| .jmode-btn.on{background:#00C9B8;color:#080B0F} | |
| .jmode-btn:not(.on):hover{color:rgba(255,255,255,.7)} | |
| .jimg-wrap{position:relative;border-radius:18px;overflow:hidden;border:1px solid rgba(255,255,255,.07);background:#090D12;transition:border-color .3s} | |
| .jimg-wrap:hover{border-color:rgba(0,201,184,.3)} | |
| .jimg-wrap img{width:100%;display:block;transition:opacity .35s;min-height:200px;background:#0D1117} | |
| .jimg-wrap img.fading{opacity:0} | |
| .jbadge{position:absolute;top:14px;left:14px;font-family:'Space Mono',monospace;font-size:10px;font-weight:700;letter-spacing:.08em;text-transform:uppercase;padding:5px 13px;border-radius:20px;backdrop-filter:blur(14px);z-index:2} | |
| .jbadge.demo{background:rgba(255,179,0,.9);color:#080B0F} | |
| .jbadge.real{background:rgba(0,201,184,.9);color:#080B0F} | |
| .jcaption{font-family:'Space Mono',monospace;font-size:10.5px;color:rgba(255,255,255,.38);margin-top:10px;text-align:center;letter-spacing:.03em;line-height:1.6} | |
| .jlegend{display:flex;align-items:center;gap:20px;margin-top:22px;flex-wrap:wrap;padding:14px 20px;background:rgba(255,255,255,.02);border:1px solid rgba(255,255,255,.06);border-radius:12px} | |
| .jleg-item{display:flex;align-items:center;gap:8px;font-family:'Space Mono',monospace;font-size:10.5px;color:rgba(255,255,255,.55)} | |
| .jleg-dot{width:12px;height:12px;border-radius:3px;flex-shrink:0} | |
| .jrow-labels{display:flex;gap:0;margin-bottom:10px;padding:0 2px} | |
| .jrow-label{font-family:'Space Mono',monospace;font-size:9px;color:rgba(255,255,255,.4);letter-spacing:.08em;text-transform:uppercase;flex:1;text-align:center} | |
| .jcompare-grid{display:grid;grid-template-columns:1fr 1fr;gap:18px} | |
| @media(max-width:900px){.jcompare-grid{grid-template-columns:1fr}} | |
| .jglow-demo{box-shadow:0 4px 40px rgba(255,179,0,.06),0 0 0 1px rgba(255,179,0,.15)} | |
| .jglow-real{box-shadow:0 4px 40px rgba(0,201,184,.06),0 0 0 1px rgba(0,201,184,.15)} | |
| .jstat-row{display:flex;gap:12px;margin-top:20px;flex-wrap:wrap} | |
| .jstat{flex:1;min-width:140px;padding:12px 16px;background:rgba(255,255,255,.03);border:1px solid rgba(255,255,255,.07);border-radius:12px;text-align:center} | |
| .jstat-v{font-family:'Space Mono',monospace;font-size:18px;font-weight:700;color:#ffffff;line-height:1.2} | |
| .jstat-l{font-size:10px;color:rgba(255,255,255,.4);margin-top:3px;letter-spacing:.05em} | |
| /* ── RESEARCH WALKTHROUGH STYLES ── */ | |
| .rw-grid{display:grid;gap:20px} | |
| .rw-grid.c2{grid-template-columns:1fr 1fr} | |
| .rw-grid.c3{grid-template-columns:1fr 1fr 1fr} | |
| .rw-grid.c4{grid-template-columns:1fr 1fr 1fr 1fr} | |
| @media(max-width:900px){.rw-grid.c2,.rw-grid.c3,.rw-grid.c4{grid-template-columns:1fr}} | |
| @media(min-width:600px) and (max-width:900px){.rw-grid.c4{grid-template-columns:1fr 1fr}} | |
| .rw-card{background:rgba(255,255,255,.03);border:1px solid rgba(255,255,255,.08);border-radius:16px;padding:24px;transition:border-color .25s,background .25s} | |
| .rw-card:hover{border-color:rgba(0,201,184,.2);background:rgba(0,201,184,.02)} | |
| .rw-card-teal{border-color:rgba(0,201,184,.2);background:rgba(0,201,184,.04)} | |
| .rw-card-amber{border-color:rgba(255,181,71,.2);background:rgba(255,181,71,.03)} | |
| .rw-card-red{border-color:rgba(255,94,94,.2);background:rgba(255,94,94,.03)} | |
| .rw-card-green{border-color:rgba(82,229,138,.2);background:rgba(82,229,138,.03)} | |
| .rw-num{font-family:'Space Mono',monospace;font-size:11px;color:#00C9B8;letter-spacing:.06em;text-transform:uppercase;margin-bottom:8px;opacity:.8} | |
| .rw-h{font-size:16px;font-weight:700;color:#fff;margin-bottom:8px;line-height:1.3} | |
| .rw-p{font-size:13px;color:rgba(255,255,255,.5);line-height:1.65} | |
| .rw-tag{display:inline-block;font-family:'Space Mono',monospace;font-size:9px;padding:3px 8px;border-radius:10px;background:rgba(0,201,184,.1);border:1px solid rgba(0,201,184,.2);color:#00C9B8;letter-spacing:.04em;text-transform:uppercase;margin:3px 2px} | |
| .rw-divider{height:1px;background:linear-gradient(90deg,transparent,rgba(255,255,255,.08),transparent);margin:48px 0} | |
| .method-step{display:flex;gap:18px;align-items:flex-start;padding:20px 0;border-bottom:1px solid rgba(255,255,255,.06)} | |
| .method-step:last-child{border-bottom:none} | |
| .step-num{flex-shrink:0;width:36px;height:36px;border-radius:50%;background:rgba(0,201,184,.1);border:1px solid rgba(0,201,184,.25);display:flex;align-items:center;justify-content:center;font-family:'Space Mono',monospace;font-size:12px;font-weight:700;color:#00C9B8} | |
| .step-body h4{font-size:14px;font-weight:700;color:#fff;margin-bottom:4px} | |
| .step-body p{font-size:13px;color:rgba(255,255,255,.48);line-height:1.6} | |
| .step-body code{font-family:'Space Mono',monospace;font-size:11px;color:#00C9B8;background:rgba(0,201,184,.08);padding:1px 5px;border-radius:4px} | |
| .ds-bar-wrap{margin-top:6px} | |
| .ds-bar-row{display:flex;align-items:center;gap:10px;margin-bottom:8px} | |
| .ds-bar-lbl{font-family:'Space Mono',monospace;font-size:10px;color:rgba(255,255,255,.5);width:58px;flex-shrink:0} | |
| .ds-bar-track{flex:1;height:8px;background:rgba(255,255,255,.06);border-radius:4px;overflow:hidden} | |
| .ds-bar-fill{height:100%;border-radius:4px;transition:width .8s ease} | |
| .ds-bar-val{font-family:'Space Mono',monospace;font-size:10px;color:rgba(255,255,255,.4);flex-shrink:0;width:36px;text-align:right} | |
| .xai-pill{display:inline-flex;align-items:center;gap:6px;font-size:12px;font-weight:600;padding:6px 14px;border-radius:20px;border:1px solid;margin:4px} | |
| .cite-box{background:rgba(8,11,15,.6);border:1px solid rgba(255,255,255,.1);border-radius:14px;padding:24px 28px;font-family:'Space Mono',monospace;font-size:12px;color:rgba(255,255,255,.6);line-height:1.8;overflow-x:auto;white-space:pre-wrap;word-break:break-word} | |
| .ieee-notice{background:rgba(0,201,184,.04);border:1px solid rgba(0,201,184,.15);border-radius:12px;padding:18px 22px;font-size:12px;color:rgba(255,255,255,.5);line-height:1.7} | |
| .ieee-notice strong{color:#00C9B8} | |
| .training-row{display:grid;grid-template-columns:160px 1fr;gap:12px 20px;align-items:start;padding:10px 0;border-bottom:1px solid rgba(255,255,255,.05)} | |
| .training-row:last-child{border-bottom:none} | |
| .tr-key{font-family:'Space Mono',monospace;font-size:11px;color:rgba(255,255,255,.4);letter-spacing:.04em;padding-top:2px} | |
| .tr-val{font-size:13px;color:rgba(255,255,255,.75)} | |
| /* ═══════════════════════════════════════════════════════════ | |
| REFINED DESIGN SYSTEM — Confident Restraint | |
| Less decoration. More intention. Typography first. | |
| ═══════════════════════════════════════════════════════════ */ | |
| ::selection{background:rgba(0,201,184,.2);color:#fff} | |
| ::-webkit-scrollbar{width:4px} | |
| ::-webkit-scrollbar-thumb{background:rgba(255,255,255,.12);border-radius:2px} | |
| ::-webkit-scrollbar-thumb:hover{background:rgba(255,255,255,.22)} | |
| /* ── NAV refinement ── */ | |
| nav{background:rgba(8,11,15,.9);backdrop-filter:blur(24px);border-bottom:1px solid rgba(255,255,255,.06)} | |
| .nav-r a:not(.nav-cta){font-size:11px;letter-spacing:.06em;transition:color .18s} | |
| .nav-r a:not(.nav-cta).nav-active{color:rgba(255,255,255,.95)} | |
| /* ── HERO ── */ | |
| h1.hero-title em{color:#00C9B8;font-style:italic} | |
| .btn-primary{background:#00C9B8;color:#080B0F} | |
| .btn-primary:hover{opacity:.88;transform:translateY(-1px)} | |
| .btn-ghost:hover{border-color:rgba(255,255,255,.3);color:#fff} | |
| /* ── Section eyebrows — single teal dash prefix ── */ | |
| .sec-eye{display:flex;align-items:center;gap:10px;font-size:10px;letter-spacing:.14em;color:#00C9B8;margin-bottom:16px} | |
| .sec-eye::before{content:'';width:24px;height:1px;background:#00C9B8;flex-shrink:0} | |
| /* ── Section headings — strong serif, no gradients ── */ | |
| .sec-head{color:#fff;font-weight:700;letter-spacing:-.03em} | |
| .sec-head em{color:#00C9B8;font-style:italic} | |
| /* ── Divider — minimal line ── */ | |
| .rw-divider{height:1px;background:rgba(255,255,255,.08);margin:56px 0} | |
| /* ── CARDS — content-first, no decoration ── */ | |
| .rw-card{background:rgba(255,255,255,.03);border:1px solid rgba(255,255,255,.08);border-radius:14px;padding:26px;transition:border-color .2s,background .2s} | |
| .rw-card:hover{border-color:rgba(255,255,255,.14);background:rgba(255,255,255,.05)} | |
| .rw-card-teal{border-color:rgba(0,201,184,.2);background:rgba(0,201,184,.04)} | |
| .rw-card-amber{border-color:rgba(255,181,71,.15);background:rgba(255,181,71,.03)} | |
| .rw-card-red{border-color:rgba(255,94,94,.15);background:rgba(255,94,94,.03)} | |
| .rw-card-green{border-color:rgba(82,229,138,.15);background:rgba(82,229,138,.03)} | |
| .rw-num{font-family:'Space Mono',monospace;font-size:10px;color:#00C9B8;letter-spacing:.12em;text-transform:uppercase;margin-bottom:10px} | |
| .rw-h{font-size:15px;font-weight:700;color:#fff;margin-bottom:8px;line-height:1.3} | |
| .rw-p{font-size:13px;color:rgba(255,255,255,.5);line-height:1.7} | |
| .rw-tag{display:inline-block;font-family:'Space Mono',monospace;font-size:9px;padding:3px 8px;border-radius:4px;background:rgba(255,255,255,.06);border:1px solid rgba(255,255,255,.1);color:rgba(255,255,255,.5);letter-spacing:.04em;text-transform:uppercase;margin:3px 2px} | |
| /* ── DISEASE CARDS — clean with left color bar ── */ | |
| .disease-card{display:flex;gap:16px;align-items:flex-start;padding:18px 20px;border-radius:12px;border:1px solid rgba(255,255,255,.08);background:rgba(255,255,255,.025);transition:background .2s,border-color .2s} | |
| .disease-card:hover{background:rgba(255,255,255,.045);border-color:rgba(255,255,255,.13)} | |
| .disease-dot{flex-shrink:0;width:3px;height:100%;min-height:56px;border-radius:2px;margin-top:0;align-self:stretch} | |
| .disease-title{font-size:13px;font-weight:700;margin-bottom:5px;letter-spacing:-.01em} | |
| .disease-desc{font-size:12px;color:rgba(255,255,255,.44);line-height:1.6} | |
| /* ── GAP CARDS — problem/solution split ── */ | |
| .gap-card{padding:16px 20px;background:rgba(255,255,255,.025);border:1px solid rgba(255,255,255,.07);border-radius:10px;transition:background .2s} | |
| .gap-card:hover{background:rgba(255,255,255,.04)} | |
| .gap-card-bad{border-left:2px solid rgba(255,94,94,.4)} | |
| .gap-card-good{border-left:2px solid rgba(0,201,184,.5);background:rgba(0,201,184,.03);border-color:rgba(0,201,184,.14)} | |
| /* ── METHODOLOGY STEPS — numbered, vertical ── */ | |
| .method-step{display:grid;grid-template-columns:48px 1fr;gap:0 20px;padding:28px 0;border-bottom:1px solid rgba(255,255,255,.06)} | |
| .method-step:last-child{border-bottom:none;padding-bottom:0} | |
| .step-num{width:36px;height:36px;border-radius:50%;border:1px solid rgba(255,255,255,.12);display:flex;align-items:center;justify-content:center;font-family:'Space Mono',monospace;font-size:12px;color:rgba(255,255,255,.4);flex-shrink:0;margin-top:2px;transition:border-color .2s,color .2s} | |
| .method-step:hover .step-num{border-color:#00C9B8;color:#00C9B8} | |
| .step-body h4{font-size:15px;font-weight:700;color:#fff;margin-bottom:6px;letter-spacing:-.01em} | |
| .step-body p{font-size:13px;color:rgba(255,255,255,.48);line-height:1.65} | |
| .step-body code{font-family:'Space Mono',monospace;font-size:11px;color:#00C9B8;background:rgba(0,201,184,.07);padding:1px 6px;border-radius:4px} | |
| /* ── TRAINING TABLE ── */ | |
| .tr-key{font-size:11px;color:rgba(255,255,255,.38);letter-spacing:.03em} | |
| .tr-val{font-size:13px;color:rgba(255,255,255,.8)} | |
| .training-row:nth-child(even){background:rgba(255,255,255,.015);margin:0 -22px;padding:10px 22px;border-radius:3px} | |
| /* ── DATASET STAT ROW ── */ | |
| .ds-stat{text-align:center;padding:24px 16px;border-right:1px solid rgba(255,255,255,.07)} | |
| .ds-stat:last-child{border-right:none} | |
| .ds-stat-v{font-family:'Space Mono',monospace;font-size:36px;font-weight:700;color:#fff;line-height:1;letter-spacing:-.03em} | |
| .ds-stat-l{font-size:11px;color:rgba(255,255,255,.4);margin-top:6px;letter-spacing:.05em;text-transform:uppercase} | |
| .ds-bar-track{background:rgba(255,255,255,.06);height:6px;border-radius:3px;overflow:hidden} | |
| .ds-bar-fill{height:100%;border-radius:3px} | |
| .ds-bar-lbl{font-family:'Space Mono',monospace;font-size:10px;color:rgba(255,255,255,.45);width:62px;flex-shrink:0} | |
| .ds-bar-val{font-family:'Space Mono',monospace;font-size:10px;color:rgba(255,255,255,.4);flex-shrink:0;min-width:44px;text-align:right} | |
| /* ── XAI METHOD CARDS ── */ | |
| .xai-method-card{background:rgba(255,255,255,.03);border:1px solid rgba(255,255,255,.08);border-radius:14px;padding:26px;transition:border-color .2s,background .2s;--xai-color:#00C9B8} | |
| .xai-method-card:hover{border-color:rgba(255,255,255,.15);background:rgba(255,255,255,.05)} | |
| .xai-num{font-family:'Space Mono',monospace;font-size:11px;color:rgba(255,255,255,.25);letter-spacing:.06em;margin-bottom:14px} | |
| .xai-name{font-size:16px;font-weight:700;color:#fff;margin-bottom:10px;letter-spacing:-.02em} | |
| .xai-desc{font-size:13px;color:rgba(255,255,255,.48);line-height:1.7;margin-bottom:14px} | |
| .xai-answer{font-size:12px;font-style:italic;color:rgba(255,255,255,.55);line-height:1.55;padding:10px 14px;background:rgba(255,255,255,.04);border-radius:8px;border-left:2px solid var(--xai-color)} | |
| /* ── ABSTRACT ── */ | |
| .abstract-card{background:rgba(255,255,255,.03);border:1px solid rgba(255,255,255,.09);border-radius:16px;padding:30px 36px;line-height:1.85;font-size:15px;color:rgba(255,255,255,.58)} | |
| /* ── ARCHITECTURE WRAPPER ── */ | |
| .arch-wrap{background:rgba(255,255,255,.02);border:1px solid rgba(255,255,255,.07);border-radius:16px;padding:32px 24px;overflow-x:auto} | |
| /* ── TERMINAL CITATION ── */ | |
| .terminal-box{background:#060a0f;border:1px solid rgba(255,255,255,.1);border-radius:14px;overflow:hidden} | |
| .terminal-header{background:rgba(255,255,255,.04);padding:11px 18px;display:flex;align-items:center;gap:8px;border-bottom:1px solid rgba(255,255,255,.07)} | |
| .term-dot{width:10px;height:10px;border-radius:50%} | |
| .term-dot-r{background:#FF5F57}.term-dot-g{background:#28CA41}.term-dot-b{background:#FEBC2E} | |
| .term-title{font-family:'Space Mono',monospace;font-size:10px;color:rgba(255,255,255,.35);letter-spacing:.05em;margin-left:auto;margin-right:auto} | |
| .terminal-body{padding:20px 24px;font-family:'Space Mono',monospace;font-size:12px;color:rgba(255,255,255,.5);line-height:1.9;overflow-x:auto;white-space:pre-wrap;word-break:break-word} | |
| .term-key{color:#00C9B8}.term-val{color:#52E58A}.term-str{color:#FFB547}.term-comment{color:rgba(255,255,255,.28)} | |
| /* ── IEEE NOTICE ── */ | |
| .ieee-notice{background:rgba(255,255,255,.025);border:1px solid rgba(255,255,255,.08);border-radius:14px;padding:24px 28px;font-size:13px;color:rgba(255,255,255,.45);line-height:1.8} | |
| .ieee-notice strong{color:rgba(255,255,255,.8);font-weight:600} | |
| /* ── STATS ── */ | |
| .sc:hover{background:rgba(255,255,255,.035)!important} | |
| /* ── RESULTS TABLE ── */ | |
| table thead th{font-family:'Space Mono',monospace!important;font-size:9px!important;color:rgba(255,255,255,.4)!important;letter-spacing:.08em!important} | |
| table tbody tr:hover:not(:last-child){background:rgba(255,255,255,.02)!important} | |
| /* ── FOOTER ── */ | |
| footer{background:#080B0F!important;border-top:1px solid rgba(255,255,255,.07)!important} | |
| /* ── FADE IN — only essential ── */ | |
| @keyframes fadeInUp{from{opacity:0;transform:translateY(16px)}to{opacity:1;transform:translateY(0)}} | |
| .rv-reveal{opacity:0;transform:translateY(16px);transition:opacity .5s ease,transform .5s ease} | |
| .rv-reveal.visible{opacity:1;transform:none} | |
| /* ── Responsive ── */ | |
| @media(max-width:700px){.rw-grid.c2,.rw-grid.c3,.rw-grid.c4{grid-template-columns:1fr} | |
| .xai-method-card{padding:20px}.method-step{gap:14px}.ds-stat-v{font-size:26px}} | |
| </style> | |
| </head> | |
| <body> | |
| <nav> | |
| <a href="#" class="logo">Ret<b>ViM</b></a> | |
| <div class="nav-r"> | |
| <a href="#motivation">Problem</a> | |
| <a href="#dataset">Dataset</a> | |
| <a href="#methodology">Methods</a> | |
| <a href="#results-tables">Results</a> | |
| <a href="#xai-section">XAI</a> | |
| <a href="#demo">Demo</a> | |
| <a href="#citation">Cite</a> | |
| <div class="api-status demo" id="apiStatusBadge" title="Checking backend…"> | |
| <span class="api-pulse"></span> | |
| <span id="apiStatusText">Connecting…</span> | |
| </div> | |
| <a href="#" onclick="toggleTheme(); return false;" id="themeToggle">Light Theme</a> | |
| <a href="#demo" class="nav-cta">Try Now →</a> | |
| </div> | |
| </nav> | |
| <!-- ── HERO ── --> | |
| <section class="hero"> | |
| <div class="hero-bg"></div> | |
| <canvas id="heroCanvas"></canvas> | |
| <div class="hero-inner"> | |
| <div class="hero-eyebrow"><span class="live-dot"></span>OCT Retinal Classification · SCET · 2026</div> | |
| <h1 class="hero-title">See inside<br>the <em>machine</em><br>mind</h1> | |
| <p class="hero-sub">Microscopic-level AI analysis of OCT B-scans — real feature maps, neural journey visualization, Mamba state dynamics, and clinical-grade explainability.</p> | |
| <div class="hero-metrics"> | |
| <div class="hm"><div class="hm-v">99.90<span>%</span></div><div class="hm-l">Test Accuracy</div></div> | |
| <div class="hm-sep"></div> | |
| <div class="hm"><div class="hm-v">96.68<span>%</span></div><div class="hm-l">Val Accuracy</div></div> | |
| <div class="hm-sep"></div> | |
| <div class="hm"><div class="hm-v">1.0000</div><div class="hm-l">AUC-ROC</div></div> | |
| <div class="hm-sep"></div> | |
| <div class="hm"><div class="hm-v">101.2<span>M</span></div><div class="hm-l">Parameters</div></div> | |
| <div class="hm-sep"></div> | |
| <div class="hm"><div class="hm-v">0.9986</div><div class="hm-l">Cohen's κ</div></div> | |
| </div> | |
| <div class="hero-btns"> | |
| <a href="#motivation" class="btn btn-primary">Read the Paper →</a> | |
| <a href="#demo" class="btn btn-ghost">Live Demo</a> | |
| </div> | |
| </div> | |
| <div class="scroll-hint"><div class="scroll-line"></div><span>Scroll</span></div> | |
| </section> | |
| <!-- ── PAPER ABSTRACT ── --> | |
| <section class="section section-dark" id="abstract" style="background:#0D1117;padding-top:80px;padding-bottom:60px"> | |
| <div class="section-inner" style="max-width:960px"> | |
| <div class="sec-eye sf">IEEE Conference Paper · 2026</div> | |
| <h2 class="sec-head sf d1" style="font-size:clamp(24px,3.5vw,42px);line-height:1.15">RetViM: Sequential Hybrid Vision Transformer with MedMamba for <em>Retinal Disease Classification</em></h2> | |
| <p style="font-family:'Space Mono',monospace;font-size:12px;color:rgba(255,255,255,.5);margin-bottom:20px;letter-spacing:.02em" class="sf d2">Khamir Desai, Mayuri A. Mehta, Sree Saicharan Vadapalli · Sarvajanik College of Engineering and Technology, Surat, India</p> | |
| <div class="abstract-card sf d3"> | |
| Early detection of retinal diseases such as Diabetic Macular Edema (DME), Choroidal Neovascularization (CNV) and Drusen is crucial for preventing irreversible vision loss. This paper proposes <strong style="color:#fff;font-weight:700">RetViM</strong>, a novel sequential hybrid method combining Vision Transformers (ViT) and Modified MedMamba for retinal disease classification using OCT B-scan images. RetViM achieves <strong style="color:#00C9B8;font-weight:700">99.90% accuracy</strong>, 99.90% precision, 99.90% recall, 99.97% specificity, 99.90% F1-score, AUC-ROC of 1.00 on the test set and 0.9986 Cohen's kappa — with only <strong style="color:#fff;font-weight:700">1 misclassification out of 968 test images</strong>. | |
| </div> | |
| <div style="display:flex;gap:8px;flex-wrap:wrap;margin-top:16px" class="sf"> | |
| <span style="font-family:'Space Mono',monospace;font-size:10px;padding:4px 10px;border-radius:20px;background:rgba(0,201,184,.1);border:1px solid rgba(0,201,184,.2);color:#00C9B8">Deep Learning</span> | |
| <span style="font-family:'Space Mono',monospace;font-size:10px;padding:4px 10px;border-radius:20px;background:rgba(255,255,255,.04);border:1px solid rgba(255,255,255,.08);color:rgba(255,255,255,.5)">Vision Transformer</span> | |
| <span style="font-family:'Space Mono',monospace;font-size:10px;padding:4px 10px;border-radius:20px;background:rgba(255,255,255,.04);border:1px solid rgba(255,255,255,.08);color:rgba(255,255,255,.5)">MedMamba</span> | |
| <span style="font-family:'Space Mono',monospace;font-size:10px;padding:4px 10px;border-radius:20px;background:rgba(255,255,255,.04);border:1px solid rgba(255,255,255,.08);color:rgba(255,255,255,.5)">State Space Model</span> | |
| <span style="font-family:'Space Mono',monospace;font-size:10px;padding:4px 10px;border-radius:20px;background:rgba(255,255,255,.04);border:1px solid rgba(255,255,255,.08);color:rgba(255,255,255,.5)">Optical Coherence Tomography</span> | |
| <span style="font-family:'Space Mono',monospace;font-size:10px;padding:4px 10px;border-radius:20px;background:rgba(255,255,255,.04);border:1px solid rgba(255,255,255,.08);color:rgba(255,255,255,.5)">Retinal Disease</span> | |
| </div> | |
| </div> | |
| </section> | |
| <!-- ── MOTIVATION & PROBLEM ── --> | |
| <section class="section section-darker" id="motivation" style="background:#111820;padding-top:80px;padding-bottom:80px"> | |
| <div class="section-inner"> | |
| <div class="sec-eye sf">§ 1 — Motivation & Problem Statement</div> | |
| <h2 class="sec-head sf d1">Why Retinal Disease<br><em>Classification Matters</em></h2> | |
| <!-- Editorial statement --> | |
| <p class="sf d1" style="font-family:'Playfair Display',serif;font-size:clamp(18px,2.5vw,28px);color:rgba(255,255,255,.85);font-weight:400;line-height:1.45;letter-spacing:-.01em;max-width:820px;margin:0 0 52px;font-style:italic">"The retina is the only part of the central nervous system directly visible to an ophthalmologist. It is also where preventable blindness begins — and where AI can intervene before the window closes."</p> | |
| <div class="rw-grid c3 sf d3" style="margin-bottom:48px"> | |
| <div class="rw-card rw-card-red"> | |
| <div class="rw-num">Clinical Burden</div> | |
| <div class="rw-h">285 Million Visually Impaired</div> | |
| <div class="rw-p">The WHO estimates 285 million people suffer visual impairment, with 39 million blind. Retinal diseases — including DME, CNV (wet AMD) and Drusen (dry AMD) — account for a major fraction of preventable blindness.</div> | |
| </div> | |
| <div class="rw-card rw-card-amber"> | |
| <div class="rw-num">Diagnostic Gap</div> | |
| <div class="rw-h">Specialists Cannot Scale</div> | |
| <div class="rw-p">There is a critical global shortage of retinal specialists. Manual OCT interpretation is time-consuming, subjective, and costly. Rural and developing populations have near-zero access to specialist care.</div> | |
| </div> | |
| <div class="rw-card rw-card-teal"> | |
| <div class="rw-num">AI Opportunity</div> | |
| <div class="rw-h">OCT: Gold-Standard Imaging</div> | |
| <div class="rw-p">Optical Coherence Tomography (OCT) is the gold-standard, non-invasive cross-sectional scan of the retina. It reveals layer-by-layer microstructure at micrometer resolution — making it ideal for AI analysis.</div> | |
| </div> | |
| </div> | |
| <div class="rw-divider"></div> | |
| <div class="rw-grid c2 sf" style="gap:32px;align-items:start"> | |
| <div> | |
| <div style="font-family:'Space Mono',monospace;font-size:11px;color:#00C9B8;letter-spacing:.08em;text-transform:uppercase;margin-bottom:14px">The Four Classes</div> | |
| <div style="display:flex;flex-direction:column;gap:10px"> | |
| <div class="disease-card disease-card-cnv"> | |
| <div class="disease-dot" style="background:#FF5E5E;color:#FF5E5E"></div> | |
| <div><div class="disease-title" style="color:#FF8080">CNV — Choroidal Neovascularization</div><div class="disease-desc">Abnormal blood vessels grow beneath the retina, leaking fluid and causing rapid central vision loss. Associated with wet AMD. <strong style="color:rgba(255,255,255,.65)">Requires urgent anti-VEGF treatment.</strong></div></div> | |
| </div> | |
| <div class="disease-card disease-card-dme"> | |
| <div class="disease-dot" style="background:#FFB547;color:#FFB547"></div> | |
| <div><div class="disease-title" style="color:#FFB547">DME — Diabetic Macular Edema</div><div class="disease-desc">Fluid accumulation in the macula caused by diabetic retinopathy. Cystoid spaces form in inner retinal layers. <strong style="color:rgba(255,255,255,.65)">Leading cause of vision loss in working-age adults.</strong></div></div> | |
| </div> | |
| <div class="disease-card disease-card-drusen"> | |
| <div class="disease-dot" style="background:#B47FFF;color:#B47FFF"></div> | |
| <div><div class="disease-title" style="color:#C49FFF">DRUSEN — Dry AMD</div><div class="disease-desc">Sub-RPE lipid deposits (drusen) cause progressive photoreceptor atrophy. Intermediate stage of dry AMD. <strong style="color:rgba(255,255,255,.65)">Requires monitoring; 18–30% progress to advanced AMD.</strong></div></div> | |
| </div> | |
| <div class="disease-card disease-card-normal"> | |
| <div class="disease-dot" style="background:#52E58A;color:#52E58A"></div> | |
| <div><div class="disease-title" style="color:#52E58A">NORMAL — Healthy Retina</div><div class="disease-desc">All nine retinal layers intact with normal reflectivity. Foveal pit preserved. IS/OS junction continuous. <strong style="color:rgba(255,255,255,.65)">Correct ruling-out is equally important to avoid unnecessary treatment.</strong></div></div> | |
| </div> | |
| </div> | |
| </div> | |
| <div> | |
| <div style="font-family:'Space Mono',monospace;font-size:11px;color:#00C9B8;letter-spacing:.08em;text-transform:uppercase;margin-bottom:14px">Research Gap This Paper Addresses</div> | |
| <div style="display:flex;flex-direction:column;gap:10px"> | |
| <div class="gap-card gap-card-bad"> | |
| <div style="font-size:12px;font-weight:700;color:rgba(255,130,130,.9);margin-bottom:4px;display:flex;align-items:center;gap:7px"><span style="font-size:14px">✗</span> Existing CNNs lack global context</div> | |
| <div style="font-size:12px;color:rgba(255,255,255,.45)">ConvNets capture local patterns but miss long-range inter-layer relationships critical for retinal pathology.</div> | |
| </div> | |
| <div class="gap-card gap-card-bad"> | |
| <div style="font-size:12px;font-weight:700;color:rgba(255,130,130,.9);margin-bottom:4px;display:flex;align-items:center;gap:7px"><span style="font-size:14px">✗</span> Pure ViTs are data-hungry</div> | |
| <div style="font-size:12px;color:rgba(255,255,255,.45)">Vision Transformers need massive datasets. Medical imaging datasets are inherently small and imbalanced.</div> | |
| </div> | |
| <div class="gap-card gap-card-bad"> | |
| <div style="font-size:12px;font-weight:700;color:rgba(255,130,130,.9);margin-bottom:4px;display:flex;align-items:center;gap:7px"><span style="font-size:14px">✗</span> Black-box predictions lack clinical trust</div> | |
| <div style="font-size:12px;color:rgba(255,255,255,.45)">Clinical adoption requires explainability. Doctors need to understand <em>why</em> a prediction was made.</div> | |
| </div> | |
| <div class="gap-card gap-card-good"> | |
| <div style="font-size:12px;font-weight:700;color:#00C9B8;margin-bottom:4px;display:flex;align-items:center;gap:7px"><span style="font-size:14px">✓</span> RetViM: Hybrid Sequential Architecture</div> | |
| <div style="font-size:12px;color:rgba(255,255,255,.5)">ViT-B/16 for global context + MedMamba for local selective state-space modeling + 6 XAI methods for clinical explainability.</div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </section> | |
| <!-- ── ARCHITECTURE SHOWCASE ── --> | |
| <section class="section" id="architecture" style="background:#080B0F;padding-top:80px"> | |
| <div class="section-inner"> | |
| <div class="sec-eye sf">Architecture</div> | |
| <h2 class="sec-head sf d1">Sequential Hybrid<br><em>ViT-MedMamba</em></h2> | |
| <p class="sec-sub sf d2">ViT-B/16 captures global long-range dependencies. Modified MedMamba refines with local Conv + selective State-Space modeling.</p> | |
| <!-- SVG Architecture Pipeline Diagram --> | |
| <div class="sf d3" style="margin-bottom:40px"> | |
| <div class="arch-wrap"> | |
| <svg viewBox="0 0 1200 220" style="width:100%;max-width:1200px;display:block;margin:0 auto" xmlns="http://www.w3.org/2000/svg"> | |
| <!-- OCT Input --> | |
| <rect x="10" y="60" width="90" height="100" rx="8" fill="#151E28" stroke="rgba(255,255,255,.15)" stroke-width="1.5"/> | |
| <text x="55" y="98" fill="rgba(255,255,255,.7)" font-size="10" text-anchor="middle" font-family="Space Grotesk">OCT B-Scan</text> | |
| <text x="55" y="115" fill="rgba(255,255,255,.35)" font-size="9" text-anchor="middle" font-family="Space Mono">224×224×3</text> | |
| <text x="55" y="148" fill="rgba(0,201,184,.5)" font-size="8" text-anchor="middle" font-family="Space Mono">ImageNet Norm</text> | |
| <!-- Arrow --> | |
| <path d="M105 110 L140 110" stroke="rgba(0,201,184,.4)" stroke-width="1.5" marker-end="url(#arrowT)"/> | |
| <!-- Patch Embed --> | |
| <rect x="145" y="70" width="85" height="80" rx="8" fill="rgba(94,160,255,.08)" stroke="rgba(94,160,255,.3)" stroke-width="1.5"/> | |
| <text x="187" y="98" fill="#5EA0FF" font-size="9" text-anchor="middle" font-weight="600" font-family="Space Grotesk">Patch Embed</text> | |
| <text x="187" y="115" fill="rgba(255,255,255,.35)" font-size="8" text-anchor="middle" font-family="Space Mono">16×16 Conv2d</text> | |
| <text x="187" y="130" fill="rgba(255,255,255,.35)" font-size="8" text-anchor="middle" font-family="Space Mono">196 + [CLS]</text> | |
| <text x="187" y="143" fill="rgba(255,255,255,.25)" font-size="8" text-anchor="middle" font-family="Space Mono">768-dim</text> | |
| <!-- Arrow --> | |
| <path d="M235 110 L265 110" stroke="rgba(0,201,184,.4)" stroke-width="1.5" marker-end="url(#arrowT)"/> | |
| <!-- Frozen ViT blocks --> | |
| <rect x="270" y="50" width="150" height="120" rx="10" fill="rgba(180,180,180,.04)" stroke="rgba(180,180,180,.2)" stroke-width="1.5" stroke-dasharray="5,3"/> | |
| <text x="345" y="75" fill="rgba(180,180,180,.6)" font-size="9" text-anchor="middle" font-weight="600" font-family="Space Grotesk">ViT Blocks 1–6</text> | |
| <text x="345" y="92" fill="rgba(180,180,180,.35)" font-size="8" text-anchor="middle" font-family="Space Mono">FROZEN · ImageNet-21k</text> | |
| <rect x="286" y="102" width="55" height="22" rx="4" fill="rgba(180,180,180,.06)" stroke="rgba(180,180,180,.15)" stroke-width="1"/> | |
| <text x="313" y="117" fill="rgba(180,180,180,.5)" font-size="8" text-anchor="middle" font-family="Space Mono">MHSA</text> | |
| <rect x="349" y="102" width="55" height="22" rx="4" fill="rgba(180,180,180,.06)" stroke="rgba(180,180,180,.15)" stroke-width="1"/> | |
| <text x="376" y="117" fill="rgba(180,180,180,.5)" font-size="8" text-anchor="middle" font-family="Space Mono">FFN</text> | |
| <text x="345" y="153" fill="rgba(180,180,180,.3)" font-size="8" text-anchor="middle" font-family="Space Mono">Low/mid-level features</text> | |
| <!-- Arrow --> | |
| <path d="M425 110 L455 110" stroke="rgba(0,201,184,.4)" stroke-width="1.5" marker-end="url(#arrowT)"/> | |
| <!-- Trainable ViT blocks --> | |
| <rect x="460" y="50" width="150" height="120" rx="10" fill="rgba(0,201,184,.04)" stroke="rgba(0,201,184,.25)" stroke-width="1.5"/> | |
| <text x="535" y="75" fill="#00C9B8" font-size="9" text-anchor="middle" font-weight="600" font-family="Space Grotesk">ViT Blocks 7–12</text> | |
| <text x="535" y="92" fill="rgba(0,201,184,.5)" font-size="8" text-anchor="middle" font-family="Space Mono">TRAINABLE · OCT fine-tuned</text> | |
| <rect x="476" y="102" width="55" height="22" rx="4" fill="rgba(0,201,184,.06)" stroke="rgba(0,201,184,.2)" stroke-width="1"/> | |
| <text x="503" y="117" fill="rgba(0,201,184,.6)" font-size="8" text-anchor="middle" font-family="Space Mono">MHSA</text> | |
| <rect x="539" y="102" width="55" height="22" rx="4" fill="rgba(0,201,184,.06)" stroke="rgba(0,201,184,.2)" stroke-width="1"/> | |
| <text x="566" y="117" fill="rgba(0,201,184,.6)" font-size="8" text-anchor="middle" font-family="Space Mono">FFN</text> | |
| <text x="535" y="153" fill="rgba(0,201,184,.35)" font-size="8" text-anchor="middle" font-family="Space Mono">High-level OCT features</text> | |
| <!-- Arrow --> | |
| <path d="M615 110 L645 110" stroke="rgba(255,181,71,.4)" stroke-width="1.5" marker-end="url(#arrowA)"/> | |
| <!-- MedMamba Blocks --> | |
| <rect x="650" y="35" width="200" height="150" rx="12" fill="rgba(255,181,71,.04)" stroke="rgba(255,181,71,.25)" stroke-width="1.5"/> | |
| <text x="750" y="58" fill="#FFB547" font-size="9" text-anchor="middle" font-weight="700" font-family="Space Grotesk">Modified MedMamba · 2 Blocks</text> | |
| <!-- Conv branch --> | |
| <rect x="665" y="68" width="80" height="50" rx="6" fill="rgba(255,94,94,.06)" stroke="rgba(255,94,94,.2)" stroke-width="1"/> | |
| <text x="705" y="86" fill="#FF8080" font-size="8" text-anchor="middle" font-weight="600" font-family="Space Grotesk">Conv Branch</text> | |
| <text x="705" y="100" fill="rgba(255,255,255,.3)" font-size="7" text-anchor="middle" font-family="Space Mono">DW 7×7/5×5/1×1</text> | |
| <text x="705" y="112" fill="rgba(255,255,255,.25)" font-size="7" text-anchor="middle" font-family="Space Mono">Local texture</text> | |
| <!-- SSM branch --> | |
| <rect x="755" y="68" width="80" height="50" rx="6" fill="rgba(180,127,255,.06)" stroke="rgba(180,127,255,.2)" stroke-width="1"/> | |
| <text x="795" y="86" fill="#C49FFF" font-size="8" text-anchor="middle" font-weight="600" font-family="Space Grotesk">SSM Branch</text> | |
| <text x="795" y="100" fill="rgba(255,255,255,.3)" font-size="7" text-anchor="middle" font-family="Space Mono">SS2D + 4-dir scan</text> | |
| <text x="795" y="112" fill="rgba(255,255,255,.25)" font-size="7" text-anchor="middle" font-family="Space Mono">Global sequence</text> | |
| <!-- Fusion --> | |
| <rect x="690" y="125" width="120" height="24" rx="5" fill="rgba(255,181,71,.08)" stroke="rgba(255,181,71,.2)" stroke-width="1"/> | |
| <text x="750" y="141" fill="#FFB547" font-size="8" text-anchor="middle" font-weight="600" font-family="Space Mono">Concat → 2-layer MLP</text> | |
| <text x="750" y="170" fill="rgba(255,181,71,.3)" font-size="7" text-anchor="middle" font-family="Space Mono">+ DropPath residual</text> | |
| <!-- Arrow --> | |
| <path d="M855 110 L885 110" stroke="rgba(82,229,138,.4)" stroke-width="1.5" marker-end="url(#arrowG)"/> | |
| <!-- Multi-scale Pool --> | |
| <rect x="890" y="50" width="110" height="120" rx="10" fill="rgba(82,229,138,.04)" stroke="rgba(82,229,138,.25)" stroke-width="1.5"/> | |
| <text x="945" y="72" fill="#52E58A" font-size="9" text-anchor="middle" font-weight="600" font-family="Space Grotesk">Multi-Scale Pool</text> | |
| <text x="945" y="92" fill="rgba(255,255,255,.3)" font-size="8" text-anchor="middle" font-family="Space Mono">CLS token</text> | |
| <text x="945" y="106" fill="rgba(255,255,255,.3)" font-size="8" text-anchor="middle" font-family="Space Mono">Avg pool</text> | |
| <text x="945" y="120" fill="rgba(255,255,255,.3)" font-size="8" text-anchor="middle" font-family="Space Mono">Max pool</text> | |
| <text x="945" y="134" fill="rgba(255,255,255,.3)" font-size="8" text-anchor="middle" font-family="Space Mono">Attn pool</text> | |
| <text x="945" y="156" fill="rgba(82,229,138,.35)" font-size="7" text-anchor="middle" font-family="Space Mono">4×768 → 768</text> | |
| <!-- Arrow --> | |
| <path d="M1005 110 L1035 110" stroke="rgba(82,229,138,.4)" stroke-width="1.5" marker-end="url(#arrowG)"/> | |
| <!-- Classifier --> | |
| <rect x="1040" y="60" width="100" height="100" rx="10" fill="rgba(0,201,184,.06)" stroke="rgba(0,201,184,.3)" stroke-width="1.5"/> | |
| <text x="1090" y="86" fill="#00C9B8" font-size="9" text-anchor="middle" font-weight="700" font-family="Space Grotesk">MLP Classifier</text> | |
| <text x="1090" y="103" fill="rgba(255,255,255,.3)" font-size="8" text-anchor="middle" font-family="Space Mono">768→512→256→4</text> | |
| <text x="1090" y="118" fill="rgba(255,255,255,.25)" font-size="7" text-anchor="middle" font-family="Space Mono">GELU + Dropout</text> | |
| <text x="1090" y="145" fill="#00C9B8" font-size="10" text-anchor="middle" font-weight="700" font-family="Space Mono">Softmax → 4</text> | |
| <!-- Output labels --> | |
| <text x="1160" y="85" fill="#FF5E5E" font-size="9" font-weight="700" font-family="Space Mono">CNV</text> | |
| <text x="1160" y="102" fill="#FFB547" font-size="9" font-weight="700" font-family="Space Mono">DME</text> | |
| <text x="1160" y="119" fill="#C8A830" font-size="9" font-weight="700" font-family="Space Mono">DRS</text> | |
| <text x="1160" y="136" fill="#52E58A" font-size="9" font-weight="700" font-family="Space Mono">NRM</text> | |
| <!-- Arrow markers --> | |
| <defs> | |
| <marker id="arrowT" markerWidth="8" markerHeight="8" refX="7" refY="4" orient="auto"><path d="M0,0 L8,4 L0,8" fill="none" stroke="rgba(0,201,184,.6)" stroke-width="1.5"/></marker> | |
| <marker id="arrowA" markerWidth="8" markerHeight="8" refX="7" refY="4" orient="auto"><path d="M0,0 L8,4 L0,8" fill="none" stroke="rgba(255,181,71,.6)" stroke-width="1.5"/></marker> | |
| <marker id="arrowG" markerWidth="8" markerHeight="8" refX="7" refY="4" orient="auto"><path d="M0,0 L8,4 L0,8" fill="none" stroke="rgba(82,229,138,.6)" stroke-width="1.5"/></marker> | |
| </defs> | |
| </svg> | |
| </div> | |
| <p class="fig-caption" style="margin-top:14px">Fig 1 — RetViM Architecture: OCT B-scan → ViT-B/16 (6 frozen + 6 trainable blocks) → 2 Modified MedMamba blocks (Conv ∥ SSM → MLP fusion) → Multi-scale pooling → 4-class softmax</p> | |
| </div> | |
| <!-- Original figure below --> | |
| <div class="sf"> | |
| <div class="fig-wrap" style="max-width:100%"> | |
| <img src="images/fig1_epic_architecture.png" class="fig-full" alt="Sequential Hybrid ViT-MedMamba Architecture"> | |
| <div class="fig-label blue">Architecture</div> | |
| </div> | |
| <p class="fig-caption">Generated architecture visualization · 101.2M params · 12 ViT blocks (6 frozen) + 2 SS-Conv-SSM blocks · Multi-scale pooling fusion</p> | |
| </div> | |
| </div> | |
| </section> | |
| <!-- ── PAPER RESULTS TABLES ── --> | |
| <!-- ── DATASET ── --> | |
| <section class="section section-dark" id="dataset" style="background:#0D1117;padding-top:80px;padding-bottom:80px"> | |
| <div class="section-inner"> | |
| <div class="sec-eye sf">§ 2 — Dataset</div> | |
| <h2 class="sec-head sf d1">Kermany OCT 2017<br><em>84,484 Labeled Scans</em></h2> | |
| <p class="sec-sub sf d2" style="margin-bottom:36px">One of the largest publicly available retinal OCT datasets. Curated by Daniel S. Kermany et al. at UC San Diego, validated by board-certified retinal specialists.</p> | |
| <!-- Big stat row --> | |
| <div class="sf" style="display:grid;grid-template-columns:repeat(4,1fr);background:rgba(255,255,255,.02);border:1px solid rgba(255,255,255,.08);border-radius:16px;overflow:hidden;margin-bottom:48px"> | |
| <div class="ds-stat"><div class="ds-stat-v">84,484</div><div class="ds-stat-l">Training Images</div></div> | |
| <div class="ds-stat"><div class="ds-stat-v">4</div><div class="ds-stat-l">Disease Classes</div></div> | |
| <div class="ds-stat"><div class="ds-stat-v">968</div><div class="ds-stat-l">Test Images</div></div> | |
| <div class="ds-stat"><div class="ds-stat-v" style="font-size:28px">242×4</div><div class="ds-stat-l">Balanced test split</div></div> | |
| </div> | |
| <div class="rw-grid c2 sf d3" style="gap:32px;align-items:start;margin-bottom:48px"> | |
| <div> | |
| <div style="font-family:'Space Mono',monospace;font-size:11px;color:#00C9B8;letter-spacing:.08em;text-transform:uppercase;margin-bottom:14px">Split Distribution</div> | |
| <div class="rw-card" style="padding:22px"> | |
| <div class="ds-bar-wrap"> | |
| <div class="ds-bar-row"><span class="ds-bar-lbl">Train</span><div class="ds-bar-track"><div class="ds-bar-fill" style="width:100%;background:linear-gradient(90deg,#00C9B8,#00b0a2)"></div></div><span class="ds-bar-val">84,484</span></div> | |
| <div class="ds-bar-row"><span class="ds-bar-lbl">Val</span><div class="ds-bar-track"><div class="ds-bar-fill" style="width:0.4%;background:#FFB547;min-width:6px"></div></div><span class="ds-bar-val">32</span></div> | |
| <div class="ds-bar-row"><span class="ds-bar-lbl">Test</span><div class="ds-bar-track"><div class="ds-bar-fill" style="width:1.1%;background:#52E58A;min-width:10px"></div></div><span class="ds-bar-val">968</span></div> | |
| </div> | |
| <div style="margin-top:18px;font-size:12px;color:rgba(255,255,255,.4);line-height:1.6">Test set: <strong style="color:#fff">242 images per class</strong> (balanced). Completely held-out — never seen during training or validation. This strict split prevents any data leakage.</div> | |
| </div> | |
| <div class="rw-card" style="padding:22px;margin-top:16px"> | |
| <div style="font-family:'Space Mono',monospace;font-size:11px;color:#FFB547;letter-spacing:.06em;text-transform:uppercase;margin-bottom:12px">Training Class Distribution</div> | |
| <div class="ds-bar-wrap"> | |
| <div class="ds-bar-row"><span class="ds-bar-lbl" style="color:#FF8080">CNV</span><div class="ds-bar-track"><div class="ds-bar-fill" style="width:49%;background:#FF5E5E"></div></div><span class="ds-bar-val">37,205</span></div> | |
| <div class="ds-bar-row"><span class="ds-bar-lbl" style="color:#FFB547">DME</span><div class="ds-bar-track"><div class="ds-bar-fill" style="width:14%;background:#FFB547"></div></div><span class="ds-bar-val">11,348</span></div> | |
| <div class="ds-bar-row"><span class="ds-bar-lbl" style="color:#C49FFF">DRUSEN</span><div class="ds-bar-track"><div class="ds-bar-fill" style="width:13%;background:#B47FFF"></div></div><span class="ds-bar-val">8,616</span></div> | |
| <div class="ds-bar-row"><span class="ds-bar-lbl" style="color:#52E58A">NORMAL</span><div class="ds-bar-track"><div class="ds-bar-fill" style="width:21%;background:#52E58A"></div></div><span class="ds-bar-val">26,315</span></div> | |
| </div> | |
| <div style="margin-top:10px;font-size:11px;color:rgba(255,255,255,.35)">Class imbalance addressed via weighted CrossEntropyLoss during training.</div> | |
| </div> | |
| </div> | |
| <div> | |
| <div style="font-family:'Space Mono',monospace;font-size:11px;color:#00C9B8;letter-spacing:.08em;text-transform:uppercase;margin-bottom:14px">Dataset Properties</div> | |
| <div style="display:flex;flex-direction:column;gap:10px"> | |
| <div class="rw-card" style="padding:16px"><div style="display:flex;justify-content:space-between;align-items:center"><span style="font-size:13px;color:rgba(255,255,255,.6)">Imaging Modality</span><span style="font-family:'Space Mono',monospace;font-size:11px;color:#00C9B8">OCT B-scan</span></div></div> | |
| <div class="rw-card" style="padding:16px"><div style="display:flex;justify-content:space-between;align-items:center"><span style="font-size:13px;color:rgba(255,255,255,.6)">Input Resolution</span><span style="font-family:'Space Mono',monospace;font-size:11px;color:#00C9B8">224 × 224 px</span></div></div> | |
| <div class="rw-card" style="padding:16px"><div style="display:flex;justify-content:space-between;align-items:center"><span style="font-size:13px;color:rgba(255,255,255,.6)">Color Channels</span><span style="font-family:'Space Mono',monospace;font-size:11px;color:#00C9B8">3 (RGB converted)</span></div></div> | |
| <div class="rw-card" style="padding:16px"><div style="display:flex;justify-content:space-between;align-items:center"><span style="font-size:13px;color:rgba(255,255,255,.6)">Normalization</span><span style="font-family:'Space Mono',monospace;font-size:11px;color:#00C9B8">ImageNet μ/σ</span></div></div> | |
| <div class="rw-card" style="padding:16px"><div style="display:flex;justify-content:space-between;align-items:center"><span style="font-size:13px;color:rgba(255,255,255,.6)">Patch Size</span><span style="font-family:'Space Mono',monospace;font-size:11px;color:#00C9B8">16 × 16 → 196 tokens</span></div></div> | |
| <div class="rw-card" style="padding:16px"><div style="display:flex;justify-content:space-between;align-items:center"><span style="font-size:13px;color:rgba(255,255,255,.6)">Label Validation</span><span style="font-family:'Space Mono',monospace;font-size:11px;color:#00C9B8">Board-certified MDs</span></div></div> | |
| <div class="rw-card" style="padding:16px"><div style="display:flex;justify-content:space-between;align-items:center"><span style="font-size:13px;color:rgba(255,255,255,.6)">Source</span><span style="font-family:'Space Mono',monospace;font-size:11px;color:#00C9B8">Mendeley Data</span></div></div> | |
| </div> | |
| <div style="margin-top:16px;padding:14px 16px;background:rgba(255,181,71,.04);border:1px solid rgba(255,181,71,.15);border-radius:12px;font-size:12px;color:rgba(255,255,255,.45);line-height:1.6"> | |
| <strong style="color:#FFB547">Augmentation (training only):</strong> Random horizontal flip, random rotation ±15°, color jitter (brightness 0.1, contrast 0.1), random resized crop (scale 0.9–1.0). | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </section> | |
| <!-- ── METHODOLOGY ── --> | |
| <section class="section" id="methodology" style="background:#080B0F;padding-top:80px;padding-bottom:80px"> | |
| <div class="section-inner"> | |
| <div class="sec-eye sf">§ 3 — Methodology</div> | |
| <h2 class="sec-head sf d1">How RetViM Works:<br><em>Step-by-Step Pipeline</em></h2> | |
| <p class="sec-sub sf d2" style="margin-bottom:32px">A sequential hybrid architecture that first extracts global context via a frozen + fine-tuned ViT, then refines with selective state-space modeling via MedMamba blocks.</p> | |
| <!-- Architecture quick-stats --> | |
| <div class="sf" style="display:flex;gap:0;border:1px solid rgba(255,255,255,.08);border-radius:12px;overflow:hidden;margin-bottom:40px;flex-wrap:wrap"> | |
| <div style="padding:16px 24px;border-right:1px solid rgba(255,255,255,.07);flex:1;min-width:140px;text-align:center"> | |
| <div style="font-family:'Space Mono',monospace;font-size:22px;font-weight:700;color:#fff;line-height:1">101.2M</div> | |
| <div style="font-size:10px;color:rgba(255,255,255,.4);margin-top:4px;letter-spacing:.06em;text-transform:uppercase">Total Parameters</div> | |
| </div> | |
| <div style="padding:16px 24px;border-right:1px solid rgba(255,255,255,.07);flex:1;min-width:140px;text-align:center"> | |
| <div style="font-family:'Space Mono',monospace;font-size:22px;font-weight:700;color:#fff;line-height:1">58.1M</div> | |
| <div style="font-size:10px;color:rgba(255,255,255,.4);margin-top:4px;letter-spacing:.06em;text-transform:uppercase">Trainable</div> | |
| </div> | |
| <div style="padding:16px 24px;border-right:1px solid rgba(255,255,255,.07);flex:1;min-width:140px;text-align:center"> | |
| <div style="font-family:'Space Mono',monospace;font-size:22px;font-weight:700;color:#fff;line-height:1">12 + 2</div> | |
| <div style="font-size:10px;color:rgba(255,255,255,.4);margin-top:4px;letter-spacing:.06em;text-transform:uppercase">ViT + Mamba Blocks</div> | |
| </div> | |
| <div style="padding:16px 24px;border-right:1px solid rgba(255,255,255,.07);flex:1;min-width:140px;text-align:center"> | |
| <div style="font-family:'Space Mono',monospace;font-size:22px;font-weight:700;color:#fff;line-height:1">768</div> | |
| <div style="font-size:10px;color:rgba(255,255,255,.4);margin-top:4px;letter-spacing:.06em;text-transform:uppercase">Embedding Dim</div> | |
| </div> | |
| <div style="padding:16px 24px;flex:1;min-width:140px;text-align:center"> | |
| <div style="font-family:'Space Mono',monospace;font-size:22px;font-weight:700;color:#00C9B8;line-height:1">O(N)</div> | |
| <div style="font-size:10px;color:rgba(255,255,255,.4);margin-top:4px;letter-spacing:.06em;text-transform:uppercase">SSM Complexity</div> | |
| </div> | |
| </div> | |
| <div class="rw-grid c2 sf d3" style="gap:40px;align-items:start"> | |
| <div> | |
| <div style="font-family:'Space Mono',monospace;font-size:11px;color:#00C9B8;letter-spacing:.08em;text-transform:uppercase;margin-bottom:14px">Pipeline Walkthrough</div> | |
| <div> | |
| <div class="method-step"> | |
| <div class="step-num">1</div> | |
| <div class="step-body"> | |
| <h4>Input Preprocessing</h4> | |
| <p>OCT B-scan resized to <code>224×224</code>, converted to RGB (3-channel), normalized with ImageNet statistics (<code>μ=[0.485,0.456,0.406]</code>, <code>σ=[0.229,0.224,0.225]</code>) so pretrained weights are properly calibrated.</p> | |
| </div> | |
| </div> | |
| <div class="method-step"> | |
| <div class="step-num">2</div> | |
| <div class="step-body"> | |
| <h4>Patch Embedding (ViT-B/16)</h4> | |
| <p>Image split into <code>196</code> non-overlapping <code>16×16</code> patches. Each patch linearly projected to <code>768</code>-dim embedding. A learnable <code>[CLS]</code> token prepended. Positional embeddings added.</p> | |
| </div> | |
| </div> | |
| <div class="method-step"> | |
| <div class="step-num">3</div> | |
| <div class="step-body"> | |
| <h4>Frozen ViT Blocks L1–L6</h4> | |
| <p>First 6 transformer encoder blocks kept frozen from ImageNet pretraining. These extract generalizable low-level features (edges, textures, spatial structure) without overfitting to the small medical dataset.</p> | |
| </div> | |
| </div> | |
| <div class="method-step"> | |
| <div class="step-num">4</div> | |
| <div class="step-body"> | |
| <h4>Fine-Tuned ViT Blocks L7–L12</h4> | |
| <p>Last 6 transformer blocks fine-tuned end-to-end. Multi-head self-attention (<code>12 heads</code>, <code>64-dim/head</code>) captures global inter-patch relationships — critical for detecting diffuse pathologies like DME.</p> | |
| </div> | |
| </div> | |
| <div class="method-step"> | |
| <div class="step-num">5</div> | |
| <div class="step-body"> | |
| <h4>MedMamba SS-Conv-SSM Blocks × 2</h4> | |
| <p>Two sequential Modified MedMamba blocks refine the token sequence. Each block runs a parallel <strong>Conv branch</strong> (depthwise conv for local texture) and <strong>SSM branch</strong> (selective state-space scan for sequential dependencies), fused via MLP.</p> | |
| </div> | |
| </div> | |
| <div class="method-step"> | |
| <div class="step-num">6</div> | |
| <div class="step-body"> | |
| <h4>Multi-Scale Pooling & Classification</h4> | |
| <p>CLS token + mean-pooled patch tokens → dual-stream feature fusion → LayerNorm → <code>4-class</code> linear head with softmax. Final prediction: argmax over [CNV, DME, DRUSEN, NORMAL].</p> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <div> | |
| <div style="font-family:'Space Mono',monospace;font-size:11px;color:#00C9B8;letter-spacing:.08em;text-transform:uppercase;margin-bottom:14px">Training Configuration</div> | |
| <div class="rw-card" style="padding:22px;margin-bottom:20px"> | |
| <div style="display:flex;flex-direction:column;gap:0"> | |
| <div class="training-row"><div class="tr-key">Optimizer</div><div class="tr-val">AdamW (β₁=0.9, β₂=0.999, ε=1e-8)</div></div> | |
| <div class="training-row"><div class="tr-key">Learning Rate</div><div class="tr-val">2e-4 (frozen layers); 1e-5 (ViT fine-tune)</div></div> | |
| <div class="training-row"><div class="tr-key">LR Schedule</div><div class="tr-val">CosineAnnealingLR (T_max=20, η_min=1e-7)</div></div> | |
| <div class="training-row"><div class="tr-key">Weight Decay</div><div class="tr-val">1e-2 (AdamW regularization)</div></div> | |
| <div class="training-row"><div class="tr-key">Loss Function</div><div class="tr-val">Weighted CrossEntropyLoss (imbalance correction)</div></div> | |
| <div class="training-row"><div class="tr-key">Batch Size</div><div class="tr-val">32 (gradient accumulation × 2)</div></div> | |
| <div class="training-row"><div class="tr-key">Epochs</div><div class="tr-val">20 (best at epoch 19: 96.68% val acc)</div></div> | |
| <div class="training-row"><div class="tr-key">Precision</div><div class="tr-val">BF16 (Brain Float 16) mixed precision</div></div> | |
| <div class="training-row"><div class="tr-key">Hardware</div><div class="tr-val">NVIDIA A100 80GB SXM4</div></div> | |
| <div class="training-row"><div class="tr-key">Framework</div><div class="tr-val">PyTorch 2.x + PyTorch Lightning</div></div> | |
| <div class="training-row"><div class="tr-key">Pretrained</div><div class="tr-val">ViT-B/16 — ImageNet-21k → ImageNet-1k</div></div> | |
| </div> | |
| </div> | |
| <div style="font-family:'Space Mono',monospace;font-size:11px;color:#00C9B8;letter-spacing:.08em;text-transform:uppercase;margin-bottom:12px">Why This Architecture?</div> | |
| <div style="display:flex;flex-direction:column;gap:10px"> | |
| <div class="rw-card rw-card-teal" style="padding:14px"> | |
| <div style="font-size:12px;font-weight:700;color:#fff;margin-bottom:3px">Global + Local Synergy</div> | |
| <div style="font-size:12px;color:rgba(255,255,255,.45)">ViT's self-attention sees the whole retina at once. MedMamba's conv branch captures fine-grained layer boundaries. Together they dominate at 99.90%.</div> | |
| </div> | |
| <div class="rw-card" style="padding:14px"> | |
| <div style="font-size:12px;font-weight:700;color:#fff;margin-bottom:3px">Transfer Learning Efficiency</div> | |
| <div style="font-size:12px;color:rgba(255,255,255,.45)">Freezing 6 ViT blocks prevents overfitting on 84K images. Only 58.1M of 101.2M parameters are trainable.</div> | |
| </div> | |
| <div class="rw-card" style="padding:14px"> | |
| <div style="font-size:12px;font-weight:700;color:#fff;margin-bottom:3px">O(N) State-Space Complexity</div> | |
| <div style="font-size:12px;color:rgba(255,255,255,.45)">MedMamba SSM processes sequences in O(N) vs O(N²) for full attention — enabling efficient modeling of long token sequences.</div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </section> | |
| <!-- ── RESULTS TABLES ── --> | |
| <section class="section section-dark" id="results-tables" style="background:#0D1117;padding-top:60px;padding-bottom:80px"> | |
| <div class="section-inner" style="max-width:1100px"> | |
| <div class="sec-eye sf">§ 4 — Experimental Results · Kermany OCT 2017</div> | |
| <h2 class="sec-head sf d1">Benchmark<br><em>Performance</em></h2> | |
| <!-- Editorial result moment --> | |
| <div class="sf" style="display:flex;align-items:center;gap:40px;margin:36px 0 48px;flex-wrap:wrap"> | |
| <div style="flex-shrink:0"> | |
| <div style="font-family:'Space Mono',monospace;font-size:clamp(64px,10vw,112px);font-weight:700;color:#fff;line-height:1;letter-spacing:-.04em">1</div> | |
| <div style="font-family:'Space Grotesk',sans-serif;font-size:14px;color:rgba(255,255,255,.4);letter-spacing:.02em;margin-top:4px">misclassification<br>out of 968 test images</div> | |
| </div> | |
| <div style="width:1px;height:80px;background:rgba(255,255,255,.1);flex-shrink:0"></div> | |
| <div style="max-width:520px"> | |
| <div style="font-size:15px;color:rgba(255,255,255,.6);line-height:1.75">RetViM misclassified exactly <strong style="color:#fff">1 image</strong> across a completely held-out test set of 968 scans — 242 per class. This corresponds to <strong style="color:#00C9B8">99.90% accuracy</strong>, <strong style="color:#00C9B8">AUC-ROC 1.0000</strong>, and <strong style="color:#00C9B8">Cohen's κ = 0.9986</strong> — a near-perfect agreement with board-certified specialist labels.</div> | |
| </div> | |
| </div> | |
| <!-- Table VIII: Full-scale model comparison --> | |
| <div class="sf d3" style="margin-bottom:36px"> | |
| <div style="font-family:'Space Mono',monospace;font-size:10px;color:rgba(0,201,184,.6);letter-spacing:.08em;text-transform:uppercase;margin-bottom:10px">Table VIII — Full-Scale Model Comparison</div> | |
| <div style="overflow-x:auto;border-radius:12px;border:1px solid rgba(255,255,255,.08)"> | |
| <table style="width:100%;border-collapse:collapse;font-family:'Space Grotesk',sans-serif;font-size:12px"> | |
| <thead> | |
| <tr style="background:rgba(255,255,255,.04)"> | |
| <th style="padding:12px 14px;text-align:left;color:rgba(255,255,255,.5);font-weight:500;border-bottom:1px solid rgba(255,255,255,.08);font-size:10px;letter-spacing:.05em;text-transform:uppercase">Model</th> | |
| <th style="padding:12px 10px;text-align:center;color:rgba(255,255,255,.5);font-weight:500;border-bottom:1px solid rgba(255,255,255,.08);font-size:10px">Acc %</th> | |
| <th style="padding:12px 10px;text-align:center;color:rgba(255,255,255,.5);font-weight:500;border-bottom:1px solid rgba(255,255,255,.08);font-size:10px">Prec %</th> | |
| <th style="padding:12px 10px;text-align:center;color:rgba(255,255,255,.5);font-weight:500;border-bottom:1px solid rgba(255,255,255,.08);font-size:10px">Recall %</th> | |
| <th style="padding:12px 10px;text-align:center;color:rgba(255,255,255,.5);font-weight:500;border-bottom:1px solid rgba(255,255,255,.08);font-size:10px">Spec %</th> | |
| <th style="padding:12px 10px;text-align:center;color:rgba(255,255,255,.5);font-weight:500;border-bottom:1px solid rgba(255,255,255,.08);font-size:10px">F1 %</th> | |
| <th style="padding:12px 10px;text-align:center;color:rgba(255,255,255,.5);font-weight:500;border-bottom:1px solid rgba(255,255,255,.08);font-size:10px">AUC-ROC</th> | |
| <th style="padding:12px 10px;text-align:center;color:rgba(255,255,255,.5);font-weight:500;border-bottom:1px solid rgba(255,255,255,.08);font-size:10px">Kappa</th> | |
| <th style="padding:12px 10px;text-align:center;color:rgba(255,255,255,.5);font-weight:500;border-bottom:1px solid rgba(255,255,255,.08);font-size:10px">Errors</th> | |
| </tr> | |
| </thead> | |
| <tbody> | |
| <tr style="border-bottom:1px solid rgba(255,255,255,.05)"> | |
| <td style="padding:10px 14px;color:rgba(255,255,255,.6)">ViT-Base (Baseline)</td> | |
| <td style="padding:10px;text-align:center;color:rgba(255,255,255,.7);font-family:'Space Mono',monospace">99.38</td> | |
| <td style="padding:10px;text-align:center;color:rgba(255,255,255,.5);font-family:'Space Mono',monospace">99.38</td> | |
| <td style="padding:10px;text-align:center;color:rgba(255,255,255,.5);font-family:'Space Mono',monospace">99.38</td> | |
| <td style="padding:10px;text-align:center;color:rgba(255,255,255,.5);font-family:'Space Mono',monospace">99.79</td> | |
| <td style="padding:10px;text-align:center;color:rgba(255,255,255,.5);font-family:'Space Mono',monospace">99.39</td> | |
| <td style="padding:10px;text-align:center;color:rgba(255,255,255,.5);font-family:'Space Mono',monospace">1.0000</td> | |
| <td style="padding:10px;text-align:center;color:rgba(255,255,255,.5);font-family:'Space Mono',monospace">0.9917</td> | |
| <td style="padding:10px;text-align:center;color:rgba(255,255,255,.5);font-family:'Space Mono',monospace">6</td> | |
| </tr> | |
| <tr style="border-bottom:1px solid rgba(255,255,255,.05)"> | |
| <td style="padding:10px 14px;color:rgba(255,255,255,.6)">Hybrid ViT-MedMamba v1</td> | |
| <td style="padding:10px;text-align:center;color:rgba(255,255,255,.7);font-family:'Space Mono',monospace">99.07</td> | |
| <td style="padding:10px;text-align:center;color:rgba(255,255,255,.5);font-family:'Space Mono',monospace">99.10</td> | |
| <td style="padding:10px;text-align:center;color:rgba(255,255,255,.5);font-family:'Space Mono',monospace">99.07</td> | |
| <td style="padding:10px;text-align:center;color:rgba(255,255,255,.5);font-family:'Space Mono',monospace">99.69</td> | |
| <td style="padding:10px;text-align:center;color:rgba(255,255,255,.5);font-family:'Space Mono',monospace">99.07</td> | |
| <td style="padding:10px;text-align:center;color:rgba(255,255,255,.5);font-family:'Space Mono',monospace">0.9999</td> | |
| <td style="padding:10px;text-align:center;color:rgba(255,255,255,.5);font-family:'Space Mono',monospace">0.9876</td> | |
| <td style="padding:10px;text-align:center;color:rgba(255,255,255,.5);font-family:'Space Mono',monospace">9</td> | |
| </tr> | |
| <tr style="background:rgba(0,201,184,.06);border-left:3px solid #00C9B8"> | |
| <td style="padding:10px 14px;color:#00C9B8;font-weight:700">RetViM (Proposed) ★</td> | |
| <td style="padding:10px;text-align:center;color:#00C9B8;font-weight:700;font-family:'Space Mono',monospace">99.90</td> | |
| <td style="padding:10px;text-align:center;color:#00C9B8;font-weight:700;font-family:'Space Mono',monospace">99.90</td> | |
| <td style="padding:10px;text-align:center;color:#00C9B8;font-weight:700;font-family:'Space Mono',monospace">99.90</td> | |
| <td style="padding:10px;text-align:center;color:#00C9B8;font-weight:700;font-family:'Space Mono',monospace">99.97</td> | |
| <td style="padding:10px;text-align:center;color:#00C9B8;font-weight:700;font-family:'Space Mono',monospace">99.90</td> | |
| <td style="padding:10px;text-align:center;color:#00C9B8;font-weight:700;font-family:'Space Mono',monospace">1.0000</td> | |
| <td style="padding:10px;text-align:center;color:#00C9B8;font-weight:700;font-family:'Space Mono',monospace">0.9986</td> | |
| <td style="padding:10px;text-align:center;color:#00C9B8;font-weight:700;font-family:'Space Mono',monospace;font-size:14px">1</td> | |
| </tr> | |
| </tbody> | |
| </table> | |
| </div> | |
| </div> | |
| <!-- Table IX: Per-class performance --> | |
| <div class="sf" style="margin-bottom:36px"> | |
| <div style="font-family:'Space Mono',monospace;font-size:10px;color:rgba(0,201,184,.6);letter-spacing:.08em;text-transform:uppercase;margin-bottom:10px">Table IX — Per-Class Performance of RetViM</div> | |
| <div style="overflow-x:auto;border-radius:12px;border:1px solid rgba(255,255,255,.08)"> | |
| <table style="width:100%;border-collapse:collapse;font-family:'Space Grotesk',sans-serif;font-size:12px"> | |
| <thead> | |
| <tr style="background:rgba(255,255,255,.04)"> | |
| <th style="padding:10px 14px;text-align:left;color:rgba(255,255,255,.5);font-weight:500;border-bottom:1px solid rgba(255,255,255,.08);font-size:10px;letter-spacing:.05em">Class</th> | |
| <th style="padding:10px;text-align:center;color:rgba(255,255,255,.5);font-weight:500;border-bottom:1px solid rgba(255,255,255,.08);font-size:10px">Precision</th> | |
| <th style="padding:10px;text-align:center;color:rgba(255,255,255,.5);font-weight:500;border-bottom:1px solid rgba(255,255,255,.08);font-size:10px">Recall</th> | |
| <th style="padding:10px;text-align:center;color:rgba(255,255,255,.5);font-weight:500;border-bottom:1px solid rgba(255,255,255,.08);font-size:10px">Specificity</th> | |
| <th style="padding:10px;text-align:center;color:rgba(255,255,255,.5);font-weight:500;border-bottom:1px solid rgba(255,255,255,.08);font-size:10px">F1</th> | |
| <th style="padding:10px;text-align:center;color:rgba(255,255,255,.5);font-weight:500;border-bottom:1px solid rgba(255,255,255,.08);font-size:10px">AUC</th> | |
| <th style="padding:10px;text-align:center;color:rgba(255,255,255,.5);font-weight:500;border-bottom:1px solid rgba(255,255,255,.08);font-size:10px">Support</th> | |
| </tr> | |
| </thead> | |
| <tbody> | |
| <tr style="border-bottom:1px solid rgba(255,255,255,.05)"> | |
| <td style="padding:10px 14px;color:#FF5E5E;font-weight:600">CNV</td> | |
| <td style="padding:10px;text-align:center;color:#52E58A;font-family:'Space Mono',monospace">1.0000</td> | |
| <td style="padding:10px;text-align:center;color:#52E58A;font-family:'Space Mono',monospace">1.0000</td> | |
| <td style="padding:10px;text-align:center;color:#52E58A;font-family:'Space Mono',monospace">1.0000</td> | |
| <td style="padding:10px;text-align:center;color:#52E58A;font-family:'Space Mono',monospace">1.0000</td> | |
| <td style="padding:10px;text-align:center;color:#52E58A;font-family:'Space Mono',monospace">1.0000</td> | |
| <td style="padding:10px;text-align:center;color:rgba(255,255,255,.5);font-family:'Space Mono',monospace">242</td> | |
| </tr> | |
| <tr style="border-bottom:1px solid rgba(255,255,255,.05)"> | |
| <td style="padding:10px 14px;color:#FFB547;font-weight:600">DME</td> | |
| <td style="padding:10px;text-align:center;color:#52E58A;font-family:'Space Mono',monospace">1.0000</td> | |
| <td style="padding:10px;text-align:center;color:#52E58A;font-family:'Space Mono',monospace">1.0000</td> | |
| <td style="padding:10px;text-align:center;color:#52E58A;font-family:'Space Mono',monospace">1.0000</td> | |
| <td style="padding:10px;text-align:center;color:#52E58A;font-family:'Space Mono',monospace">1.0000</td> | |
| <td style="padding:10px;text-align:center;color:#52E58A;font-family:'Space Mono',monospace">1.0000</td> | |
| <td style="padding:10px;text-align:center;color:rgba(255,255,255,.5);font-family:'Space Mono',monospace">242</td> | |
| </tr> | |
| <tr style="border-bottom:1px solid rgba(255,255,255,.05)"> | |
| <td style="padding:10px 14px;color:#C8A830;font-weight:600">DRUSEN</td> | |
| <td style="padding:10px;text-align:center;color:rgba(255,255,255,.7);font-family:'Space Mono',monospace">0.9959</td> | |
| <td style="padding:10px;text-align:center;color:#52E58A;font-family:'Space Mono',monospace">1.0000</td> | |
| <td style="padding:10px;text-align:center;color:rgba(255,255,255,.7);font-family:'Space Mono',monospace">0.9986</td> | |
| <td style="padding:10px;text-align:center;color:rgba(255,255,255,.7);font-family:'Space Mono',monospace">0.9979</td> | |
| <td style="padding:10px;text-align:center;color:#52E58A;font-family:'Space Mono',monospace">1.0000</td> | |
| <td style="padding:10px;text-align:center;color:rgba(255,255,255,.5);font-family:'Space Mono',monospace">242</td> | |
| </tr> | |
| <tr style="border-bottom:1px solid rgba(255,255,255,.05)"> | |
| <td style="padding:10px 14px;color:#52E58A;font-weight:600">NORMAL</td> | |
| <td style="padding:10px;text-align:center;color:#52E58A;font-family:'Space Mono',monospace">1.0000</td> | |
| <td style="padding:10px;text-align:center;color:rgba(255,255,255,.7);font-family:'Space Mono',monospace">0.9959</td> | |
| <td style="padding:10px;text-align:center;color:#52E58A;font-family:'Space Mono',monospace">1.0000</td> | |
| <td style="padding:10px;text-align:center;color:rgba(255,255,255,.7);font-family:'Space Mono',monospace">0.9979</td> | |
| <td style="padding:10px;text-align:center;color:#52E58A;font-family:'Space Mono',monospace">1.0000</td> | |
| <td style="padding:10px;text-align:center;color:rgba(255,255,255,.5);font-family:'Space Mono',monospace">242</td> | |
| </tr> | |
| <tr style="background:rgba(0,201,184,.04)"> | |
| <td style="padding:10px 14px;color:#00C9B8;font-weight:700">Macro Avg</td> | |
| <td style="padding:10px;text-align:center;color:#00C9B8;font-weight:700;font-family:'Space Mono',monospace">0.9990</td> | |
| <td style="padding:10px;text-align:center;color:#00C9B8;font-weight:700;font-family:'Space Mono',monospace">0.9990</td> | |
| <td style="padding:10px;text-align:center;color:#00C9B8;font-weight:700;font-family:'Space Mono',monospace">0.9997</td> | |
| <td style="padding:10px;text-align:center;color:#00C9B8;font-weight:700;font-family:'Space Mono',monospace">0.9990</td> | |
| <td style="padding:10px;text-align:center;color:#00C9B8;font-weight:700;font-family:'Space Mono',monospace">1.0000</td> | |
| <td style="padding:10px;text-align:center;color:rgba(255,255,255,.5);font-weight:700;font-family:'Space Mono',monospace">968</td> | |
| </tr> | |
| </tbody> | |
| </table> | |
| </div> | |
| </div> | |
| <!-- Confusion Matrix + Misclassification Analysis --> | |
| <div style="display:grid;grid-template-columns:1fr 1fr;gap:20px;margin-bottom:36px" class="sf"> | |
| <div style="background:#151E28;border-radius:14px;border:1px solid rgba(255,255,255,.08);padding:20px"> | |
| <div style="font-family:'Space Mono',monospace;font-size:10px;color:rgba(0,201,184,.6);letter-spacing:.08em;text-transform:uppercase;margin-bottom:14px">Confusion Matrix — 968 Test Images</div> | |
| <canvas id="paperCM" style="width:100%;display:block;max-height:300px"></canvas> | |
| </div> | |
| <div style="background:#151E28;border-radius:14px;border:1px solid rgba(255,255,255,.08);padding:20px"> | |
| <div style="font-family:'Space Mono',monospace;font-size:10px;color:rgba(0,201,184,.6);letter-spacing:.08em;text-transform:uppercase;margin-bottom:14px">Misclassification Analysis</div> | |
| <div style="background:rgba(255,181,71,.06);border:1px solid rgba(255,181,71,.15);border-radius:10px;padding:16px;margin-bottom:14px"> | |
| <div style="font-family:'Space Grotesk',sans-serif;font-size:13px;font-weight:700;color:#FFB547;margin-bottom:6px">1 Error out of 968 Images</div> | |
| <div style="font-size:12px;color:rgba(255,255,255,.6);line-height:1.7"> | |
| Sample #862: <strong style="color:#52E58A">NORMAL</strong> misclassified as <strong style="color:#C8A830">DRUSEN</strong><br> | |
| Decision margin: <span style="font-family:'Space Mono',monospace;color:#FFB547">1.61%</span> | |
| </div> | |
| </div> | |
| <div style="font-size:12px;color:rgba(255,255,255,.5);line-height:1.8"> | |
| <strong style="color:rgba(255,255,255,.7)">Why this is clinically acceptable:</strong><br> | |
| 1. Decision margin is only 1.61% — genuine ambiguity<br> | |
| 2. Direction is conservative (false DRUSEN) — prompts monitoring rather than dismissal<br> | |
| 3. The image exhibits subtle RPE irregularities creating real ambiguity | |
| </div> | |
| <div style="margin-top:14px;display:flex;gap:10px;flex-wrap:wrap"> | |
| <div style="flex:1;min-width:100px;text-align:center;padding:10px;background:rgba(82,229,138,.06);border:1px solid rgba(82,229,138,.15);border-radius:8px"> | |
| <div style="font-family:'Space Mono',monospace;font-size:18px;font-weight:700;color:#52E58A">100%</div> | |
| <div style="font-size:9px;color:rgba(255,255,255,.4)">CNV & DME Recall</div> | |
| </div> | |
| <div style="flex:1;min-width:100px;text-align:center;padding:10px;background:rgba(0,201,184,.06);border:1px solid rgba(0,201,184,.15);border-radius:8px"> | |
| <div style="font-family:'Space Mono',monospace;font-size:18px;font-weight:700;color:#00C9B8">99.90%</div> | |
| <div style="font-size:9px;color:rgba(255,255,255,.4)">Overall Accuracy</div> | |
| </div> | |
| <div style="flex:1;min-width:100px;text-align:center;padding:10px;background:rgba(180,127,255,.06);border:1px solid rgba(180,127,255,.15);border-radius:8px"> | |
| <div style="font-family:'Space Mono',monospace;font-size:18px;font-weight:700;color:#B47FFF">0.9986</div> | |
| <div style="font-size:9px;color:rgba(255,255,255,.4)">Cohen's Kappa</div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Table VI: Ablation Study --> | |
| <div class="sf" style="margin-bottom:36px"> | |
| <div style="font-family:'Space Mono',monospace;font-size:10px;color:rgba(0,201,184,.6);letter-spacing:.08em;text-transform:uppercase;margin-bottom:10px">Table VI — Ablation Study (16,102 training images)</div> | |
| <div style="overflow-x:auto;border-radius:12px;border:1px solid rgba(255,255,255,.08)"> | |
| <table style="width:100%;border-collapse:collapse;font-family:'Space Grotesk',sans-serif;font-size:12px"> | |
| <thead> | |
| <tr style="background:rgba(255,255,255,.04)"> | |
| <th style="padding:10px 14px;text-align:left;color:rgba(255,255,255,.5);font-weight:500;border-bottom:1px solid rgba(255,255,255,.08);font-size:10px">Model</th> | |
| <th style="padding:10px;text-align:center;color:rgba(255,255,255,.5);font-weight:500;border-bottom:1px solid rgba(255,255,255,.08);font-size:10px">Accuracy %</th> | |
| <th style="padding:10px;text-align:center;color:rgba(255,255,255,.5);font-weight:500;border-bottom:1px solid rgba(255,255,255,.08);font-size:10px">F1 %</th> | |
| <th style="padding:10px;text-align:center;color:rgba(255,255,255,.5);font-weight:500;border-bottom:1px solid rgba(255,255,255,.08);font-size:10px">AUC-ROC</th> | |
| <th style="padding:10px;text-align:center;color:rgba(255,255,255,.5);font-weight:500;border-bottom:1px solid rgba(255,255,255,.08);font-size:10px">Epochs</th> | |
| <th style="padding:10px;text-align:left;color:rgba(255,255,255,.5);font-weight:500;border-bottom:1px solid rgba(255,255,255,.08);font-size:10px">Key Finding</th> | |
| </tr> | |
| </thead> | |
| <tbody> | |
| <tr style="border-bottom:1px solid rgba(255,255,255,.05);background:rgba(0,201,184,.04)"> | |
| <td style="padding:10px 14px;color:#00C9B8;font-weight:600">ViT-Small</td> | |
| <td style="padding:10px;text-align:center;color:#00C9B8;font-weight:700;font-family:'Space Mono',monospace">98.97</td> | |
| <td style="padding:10px;text-align:center;color:rgba(255,255,255,.7);font-family:'Space Mono',monospace">98.97</td> | |
| <td style="padding:10px;text-align:center;color:rgba(255,255,255,.7);font-family:'Space Mono',monospace">0.9999</td> | |
| <td style="padding:10px;text-align:center;color:rgba(255,255,255,.7);font-family:'Space Mono',monospace">50</td> | |
| <td style="padding:10px;color:rgba(255,255,255,.5);font-size:11px">Self-attention highly effective for OCT</td> | |
| </tr> | |
| <tr style="border-bottom:1px solid rgba(255,255,255,.05)"> | |
| <td style="padding:10px 14px;color:rgba(255,255,255,.6)">Hybrid (4 blk)</td> | |
| <td style="padding:10px;text-align:center;color:rgba(255,255,255,.7);font-family:'Space Mono',monospace">98.55</td> | |
| <td style="padding:10px;text-align:center;color:rgba(255,255,255,.5);font-family:'Space Mono',monospace">98.56</td> | |
| <td style="padding:10px;text-align:center;color:rgba(255,255,255,.5);font-family:'Space Mono',monospace">0.9998</td> | |
| <td style="padding:10px;text-align:center;color:rgba(255,255,255,.5);font-family:'Space Mono',monospace">80</td> | |
| <td style="padding:10px;color:rgba(255,94,94,.6);font-size:11px">Over-regularization from 4 blocks</td> | |
| </tr> | |
| <tr> | |
| <td style="padding:10px 14px;color:rgba(255,255,255,.6)">MedMamba</td> | |
| <td style="padding:10px;text-align:center;color:rgba(255,255,255,.7);font-family:'Space Mono',monospace">97.11</td> | |
| <td style="padding:10px;text-align:center;color:rgba(255,255,255,.5);font-family:'Space Mono',monospace">97.11</td> | |
| <td style="padding:10px;text-align:center;color:rgba(255,255,255,.5);font-family:'Space Mono',monospace">0.9995</td> | |
| <td style="padding:10px;text-align:center;color:rgba(255,255,255,.5);font-family:'Space Mono',monospace">150</td> | |
| <td style="padding:10px;color:rgba(255,255,255,.5);font-size:11px">Slow convergence, needs 3× epochs</td> | |
| </tr> | |
| </tbody> | |
| </table> | |
| </div> | |
| </div> | |
| <!-- Dataset Distribution --> | |
| <div class="sf"> | |
| <div style="font-family:'Space Mono',monospace;font-size:10px;color:rgba(0,201,184,.6);letter-spacing:.08em;text-transform:uppercase;margin-bottom:10px">Table IV — Kermany OCT 2017 Dataset Distribution</div> | |
| <div style="display:grid;grid-template-columns:repeat(4,1fr);gap:12px"> | |
| <div style="background:#151E28;border-radius:12px;border:1px solid rgba(255,94,94,.15);padding:16px;text-align:center"> | |
| <div style="font-family:'Space Mono',monospace;font-size:11px;color:#FF5E5E;font-weight:700;margin-bottom:6px">CNV</div> | |
| <div style="font-family:'Space Mono',monospace;font-size:22px;font-weight:700;color:#ffffff">33,509</div> | |
| <div style="font-size:10px;color:rgba(255,255,255,.4);margin-top:4px">30,258 train · 3,009 val · 242 test</div> | |
| </div> | |
| <div style="background:#151E28;border-radius:12px;border:1px solid rgba(255,181,71,.15);padding:16px;text-align:center"> | |
| <div style="font-family:'Space Mono',monospace;font-size:11px;color:#FFB547;font-weight:700;margin-bottom:6px">DME</div> | |
| <div style="font-family:'Space Mono',monospace;font-size:22px;font-weight:700;color:#ffffff">10,213</div> | |
| <div style="font-size:10px;color:rgba(255,255,255,.4);margin-top:4px">9,192 train · 779 val · 242 test</div> | |
| </div> | |
| <div style="background:#151E28;border-radius:12px;border:1px solid rgba(200,168,48,.15);padding:16px;text-align:center"> | |
| <div style="font-family:'Space Mono',monospace;font-size:11px;color:#C8A830;font-weight:700;margin-bottom:6px">DRUSEN</div> | |
| <div style="font-family:'Space Mono',monospace;font-size:22px;font-weight:700;color:#ffffff">7,768</div> | |
| <div style="font-size:10px;color:rgba(255,255,255,.4);margin-top:4px">6,992 train · 534 val · 242 test</div> | |
| </div> | |
| <div style="background:#151E28;border-radius:12px;border:1px solid rgba(82,229,138,.15);padding:16px;text-align:center"> | |
| <div style="font-family:'Space Mono',monospace;font-size:11px;color:#52E58A;font-weight:700;margin-bottom:6px">NORMAL</div> | |
| <div style="font-family:'Space Mono',monospace;font-size:22px;font-weight:700;color:#ffffff">33,006</div> | |
| <div style="font-size:10px;color:rgba(255,255,255,.4);margin-top:4px">28,731 train · 4,033 val · 242 test</div> | |
| </div> | |
| </div> | |
| <div style="text-align:center;font-family:'Space Mono',monospace;font-size:12px;color:rgba(255,255,255,.4);margin-top:12px">Total: <strong style="color:rgba(255,255,255,.7)">84,496</strong> OCT B-scan images · 75,173 train · 8,355 val · 968 test (balanced)</div> | |
| </div> | |
| </div> | |
| </section> | |
| <!-- ── XAI METHODS ── --> | |
| <section class="section section-darker" id="xai-section" style="background:#111820;padding-top:80px;padding-bottom:80px"> | |
| <div class="section-inner"> | |
| <div class="sec-eye sf">§ 5 — Explainability · Trustworthy AI</div> | |
| <h2 class="sec-head sf d1">Six XAI Methods for<br><em>Clinical Transparency</em></h2> | |
| <p class="sec-sub sf d2" style="margin-bottom:40px">A black-box 99.9% accurate model is not enough for clinical use. RetViM is paired with six complementary explainability methods — each answering a different question about the model's decision.</p> | |
| <!-- XAI opener — editorial statement --> | |
| <p class="sf" style="font-family:'Playfair Display',serif;font-size:clamp(20px,2.8vw,34px);font-weight:700;color:#fff;line-height:1.25;letter-spacing:-.02em;max-width:760px;margin:0 0 48px;border-left:3px solid #00C9B8;padding-left:24px">A 99.9% accurate model that a clinician cannot interrogate is not a clinical tool — it is a liability. RetViM ships six independent explanations with every prediction.</p> | |
| <div class="rw-grid c3 sf d3" style="margin-bottom:36px"> | |
| <div class="xai-method-card" style="--xai-color:#00C9B8"> | |
| <span class="xai-num">01 · GRADCAM</span> | |
| <div class="xai-name">GradCAM</div> | |
| <div class="xai-desc">Gradients of the target class flow into the final attention layer, weighting each spatial feature map. Produces a coarse heatmap highlighting the most discriminative retinal regions — fast enough for clinical screening.</div> | |
| <div class="xai-answer">"Which spatial regions most strongly activated this prediction?"</div> | |
| <div style="margin-top:12px"><span class="rw-tag">Backpropagation</span><span class="rw-tag">Spatial map</span><span class="rw-tag">~12ms</span></div> | |
| </div> | |
| <div class="xai-method-card" style="--xai-color:#FFB547"> | |
| <span class="xai-num" style="color:rgba(255,181,71,.5)">02 · GRADCAM++</span> | |
| <div class="xai-name">GradCAM++</div> | |
| <div class="xai-desc">Second-order derivatives allow finer localization of multiple distinct pathological instances. GradCAM++ down-weights diffuse background gradients and elevates sharp, high-confidence activation peaks.</div> | |
| <div class="xai-answer" style="border-left-color:#FFB547">"Where are the sharpest, highest-confidence hotspots?"</div> | |
| <div style="margin-top:12px"><span class="rw-tag" style="color:#FFB547;border-color:rgba(255,181,71,.2)">2nd-order</span><span class="rw-tag" style="color:#FFB547;border-color:rgba(255,181,71,.2)">Multi-instance</span></div> | |
| </div> | |
| <div class="xai-method-card" style="--xai-color:#B47FFF"> | |
| <span class="xai-num" style="color:rgba(180,127,255,.5)">03 · ATTENTION ROLLOUT</span> | |
| <div class="xai-name">Attention Rollout</div> | |
| <div class="xai-desc">Multiplies all 12 transformer attention matrices together (accounting for residual connections) to trace exactly how information from each input patch flows into the final CLS token — a direct read of ViT's global attention.</div> | |
| <div class="xai-answer" style="border-left-color:#B47FFF">"What does the full 12-layer attention graph attend to?"</div> | |
| <div style="margin-top:12px"><span class="rw-tag" style="color:#B47FFF;border-color:rgba(180,127,255,.2)">Transformer-native</span><span class="rw-tag" style="color:#B47FFF;border-color:rgba(180,127,255,.2)">12 layers</span></div> | |
| </div> | |
| <div class="xai-method-card" style="--xai-color:#FF5E5E"> | |
| <span class="xai-num" style="color:rgba(255,94,94,.5)">04 · OCCLUSION</span> | |
| <div class="xai-name">Occlusion Sensitivity</div> | |
| <div class="xai-desc">A sliding grey patch systematically occludes every region of the OCT scan. The drop in prediction confidence at each location reveals which pixels causally matter — no gradient required. Fully model-agnostic.</div> | |
| <div class="xai-answer" style="border-left-color:#FF5E5E">"Which region, if hidden, most confuses the model?"</div> | |
| <div style="margin-top:12px"><span class="rw-tag" style="color:#FF8080;border-color:rgba(255,94,94,.2)">Causal</span><span class="rw-tag" style="color:#FF8080;border-color:rgba(255,94,94,.2)">Model-agnostic</span></div> | |
| </div> | |
| <div class="xai-method-card" style="--xai-color:#5EA0FF"> | |
| <span class="xai-num" style="color:rgba(94,160,255,.5)">05 · INTEGRATED GRADIENTS</span> | |
| <div class="xai-name">Integrated Gradients</div> | |
| <div class="xai-desc">Integrates gradients along the straight interpolation path from a black baseline to the input, satisfying the completeness axiom: attributions sum exactly to the output score difference. The most theoretically principled pixel-level explanation.</div> | |
| <div class="xai-answer" style="border-left-color:#5EA0FF">"What is each pixel's mathematically fair contribution?"</div> | |
| <div style="margin-top:12px"><span class="rw-tag" style="color:#5EA0FF;border-color:rgba(94,160,255,.2)">Axiomatic</span><span class="rw-tag" style="color:#5EA0FF;border-color:rgba(94,160,255,.2)">25-step path</span></div> | |
| </div> | |
| <div class="xai-method-card" style="--xai-color:#52E58A"> | |
| <span class="xai-num" style="color:rgba(82,229,138,.5)">06 · RISE</span> | |
| <div class="xai-name">RISE</div> | |
| <div class="xai-desc">40 random binary masks are applied to the input; each mask's prediction score becomes its weight. The weighted average of all masks is a statistical saliency map robust to individual noise — ideal for validating other methods.</div> | |
| <div class="xai-answer" style="border-left-color:#52E58A">"Which pixels statistically correlate with correct predictions?"</div> | |
| <div style="margin-top:12px"><span class="rw-tag" style="color:#52E58A;border-color:rgba(82,229,138,.2)">Stochastic</span><span class="rw-tag" style="color:#52E58A;border-color:rgba(82,229,138,.2)">40 masks</span></div> | |
| </div> | |
| </div> | |
| <div class="rw-card rw-card-teal sf" style="padding:28px 32px"> | |
| <div style="font-family:'Space Mono',monospace;font-size:10px;color:#00C9B8;letter-spacing:.12em;text-transform:uppercase;margin-bottom:14px">Multi-Method Clinical Consensus</div> | |
| <div style="font-size:14px;color:rgba(255,255,255,.58);line-height:1.8">When <strong style="color:#fff;font-weight:600">GradCAM</strong>, <strong style="color:#fff;font-weight:600">Attention Rollout</strong>, and <strong style="color:#fff;font-weight:600">Integrated Gradients</strong> all independently highlight the same anatomical region — for instance, the RPE/Bruch's membrane interface in a CNV prediction — a clinician can trust the model's decision because three methods using fundamentally different mathematical principles arrived at the same answer. This multi-method consensus is a core contribution of this paper.</div> | |
| </div> | |
| </div> | |
| </section> | |
| <!-- ── NEURAL JOURNEY DUAL-MODE SECTION ── --> | |
| <section class="section" id="journey" style="background:#080B0F;padding-top:80px;padding-bottom:90px"> | |
| <div class="section-inner"> | |
| <div class="sec-eye sf">§ 6 — Neural Journey · Feature Transformation · PyTorch</div> | |
| <h2 class="sec-head sf d1">The Neural<br><em>Journey</em></h2> | |
| <p class="sec-sub sf d2">Watch how a retinal OCT scan transforms through every layer of the hybrid ViT-Mamba pipeline. Row 1: smoothed feature heatmaps · Row 2: raw single-channel activations · Row 3: attention overlays (where the network looks). Compare <strong style="color:#FFB300">Demo</strong> vs <strong style="color:#00C9B8">Real</strong> model weights side-by-side.</p> | |
| <!-- Controls row --> | |
| <div class="sf d3" style="display:flex;align-items:center;justify-content:space-between;flex-wrap:wrap;gap:16px;margin-bottom:32px"> | |
| <!-- Class pills --> | |
| <div class="jcls-bar"> | |
| <button class="jcls-btn on" data-cls="CNV" onclick="setJCls('CNV')">CNV</button> | |
| <button class="jcls-btn" data-cls="DME" onclick="setJCls('DME')">DME</button> | |
| <button class="jcls-btn" data-cls="DRUSEN" onclick="setJCls('DRUSEN')">Drusen</button> | |
| <button class="jcls-btn" data-cls="NORMAL" onclick="setJCls('NORMAL')">Normal</button> | |
| </div> | |
| <!-- Mode toggle --> | |
| <div class="jmode-bar"> | |
| <button class="jmode-btn" onclick="setJMode('demo')" id="jmDemo"> | |
| <svg width="11" height="11" fill="none" viewBox="0 0 11 11" style="display:inline;vertical-align:middle;margin-right:5px"><rect x="1" y="1" width="9" height="9" rx="2" stroke="currentColor" stroke-width="1.3"/></svg>Demo Weights | |
| </button> | |
| <button class="jmode-btn on" onclick="setJMode('both')" id="jmBoth"> | |
| <svg width="11" height="11" fill="none" viewBox="0 0 11 11" style="display:inline;vertical-align:middle;margin-right:5px"><rect x="1" y="1" width="4" height="9" rx="1" stroke="currentColor" stroke-width="1.3"/><rect x="6" y="1" width="4" height="9" rx="1" stroke="currentColor" stroke-width="1.3"/></svg>Side by Side | |
| </button> | |
| <button class="jmode-btn" onclick="setJMode('real')" id="jmReal"> | |
| <svg width="11" height="11" fill="none" viewBox="0 0 11 11" style="display:inline;vertical-align:middle;margin-right:5px"><circle cx="5.5" cy="5.5" r="4" stroke="currentColor" stroke-width="1.3"/></svg>Real Weights | |
| </button> | |
| </div> | |
| </div> | |
| <!-- Stats row --> | |
| <div class="jstat-row sf"> | |
| <div class="jstat" id="jStatArch"> | |
| <div class="jstat-v" id="jStatArchV">Demo vs Real</div> | |
| <div class="jstat-l">Model Architecture</div> | |
| </div> | |
| <div class="jstat"> | |
| <div class="jstat-v">12 ViT</div> | |
| <div class="jstat-l">Transformer Blocks</div> | |
| </div> | |
| <div class="jstat" id="jStatMamba"> | |
| <div class="jstat-v" id="jStatMambaV">4 vs 2</div> | |
| <div class="jstat-l">Mamba Blocks</div> | |
| </div> | |
| <div class="jstat" id="jStatDim"> | |
| <div class="jstat-v" id="jStatDimV">384 vs 768</div> | |
| <div class="jstat-l">Embedding Dim</div> | |
| </div> | |
| <div class="jstat"> | |
| <div class="jstat-v" id="jStatAcc">96.68%</div> | |
| <div class="jstat-l">Real Model Val Acc</div> | |
| </div> | |
| </div> | |
| <!-- Journey images (single mode) --> | |
| <div id="jSingle" style="display:none;margin-top:26px" class="sf"> | |
| <div class="jimg-wrap" id="jSingleWrap"> | |
| <img id="jSingleImg" src="" alt="Neural Journey" loading="lazy"> | |
| <div class="jbadge" id="jSingleBadge">Demo</div> | |
| </div> | |
| <p class="jcaption" id="jSingleCaption">Loading…</p> | |
| </div> | |
| <!-- Journey images (compare mode) --> | |
| <div id="jCompare" style="margin-top:26px" class="sf"> | |
| <div class="jcompare-grid"> | |
| <!-- Demo --> | |
| <div> | |
| <div style="display:flex;align-items:center;gap:10px;margin-bottom:10px"> | |
| <div style="width:10px;height:10px;background:#FFB300;border-radius:50%;flex-shrink:0"></div> | |
| <span style="font-family:'Space Mono',monospace;font-size:11px;font-weight:700;color:#FFB300;letter-spacing:.08em;text-transform:uppercase">Demo · RetViMNet</span> | |
| <span style="font-family:'Space Mono',monospace;font-size:9px;color:rgba(255,255,255,.35)">dim=384 · 12ViT + 4Mamba</span> | |
| </div> | |
| <div class="jimg-wrap jglow-demo"> | |
| <img id="jDemoImg" src="" alt="Demo Neural Journey" loading="lazy"> | |
| <div class="jbadge demo">Demo Weights</div> | |
| </div> | |
| <p class="jcaption" id="jDemoCaption"></p> | |
| </div> | |
| <!-- Real --> | |
| <div> | |
| <div style="display:flex;align-items:center;gap:10px;margin-bottom:10px"> | |
| <div style="width:10px;height:10px;background:#00C9B8;border-radius:50%;flex-shrink:0"></div> | |
| <span style="font-family:'Space Mono',monospace;font-size:11px;font-weight:700;color:#00C9B8;letter-spacing:.08em;text-transform:uppercase">Real · ImprovedMedMamba</span> | |
| <span style="font-family:'Space Mono',monospace;font-size:9px;color:rgba(255,255,255,.35)">dim=768 · 12ViT + 2Mamba</span> | |
| </div> | |
| <div class="jimg-wrap jglow-real"> | |
| <img id="jRealImg" src="" alt="Real Neural Journey" loading="lazy"> | |
| <div class="jbadge real">Real Weights · 96.68%</div> | |
| </div> | |
| <p class="jcaption" id="jRealCaption"></p> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Row legend --> | |
| <div class="jlegend sf" style="margin-top:24px"> | |
| <span style="font-family:'Space Mono',monospace;font-size:9px;color:rgba(255,255,255,.3);text-transform:uppercase;letter-spacing:.1em;margin-right:4px">Rows:</span> | |
| <div class="jleg-item"><div class="jleg-dot" style="background:linear-gradient(135deg,#7C3AED,#06B6D4)"></div>Feature Heatmaps (smoothed spatial mean)</div> | |
| <div class="jleg-item"><div class="jleg-dot" style="background:linear-gradient(135deg,#16A34A,#FACC15)"></div>Raw Activations (high-variance channel)</div> | |
| <div class="jleg-item"><div class="jleg-dot" style="background:linear-gradient(135deg,#DC2626,#F97316)"></div>Attention Overlays (jet blend + contours)</div> | |
| <span style="flex:1"></span> | |
| <div class="jleg-item"><div class="jleg-dot" style="background:#4FC3F7"></div>ViT blocks</div> | |
| <div class="jleg-item"><div class="jleg-dot" style="background:#FFB300"></div>Mamba blocks</div> | |
| </div> | |
| </div> | |
| </section> | |
| <!-- ── DEMO SECTION ── --> | |
| <section class="section section-darker" id="demo" style="background:#111820"> | |
| <div class="section-inner"> | |
| <div class="sec-eye sf">Live Analysis</div> | |
| <h2 class="sec-head sf d1">Two-mode<br><em>Intelligence</em></h2> | |
| <div class="mode-bar sf d2"> | |
| <div class="mode-switch" id="modeSwitch"> | |
| <div class="ms-slider" id="msSlider"></div> | |
| <button class="ms-opt on" id="mR" onclick="setMode('r')"> | |
| <div class="ms-ico-w"> | |
| <svg class="ms-ico" width="15" height="15" fill="none" viewBox="0 0 15 15"> | |
| <circle cx="7.5" cy="7.5" r="2" stroke="currentColor" stroke-width="1.4"/> | |
| <ellipse cx="7.5" cy="7.5" rx="6" ry="2.8" stroke="currentColor" stroke-width="1.2"/> | |
| <ellipse cx="7.5" cy="7.5" rx="6" ry="2.8" stroke="currentColor" stroke-width="1.2" transform="rotate(60 7.5 7.5)"/> | |
| <ellipse cx="7.5" cy="7.5" rx="6" ry="2.8" stroke="currentColor" stroke-width="1.2" transform="rotate(120 7.5 7.5)"/> | |
| </svg> | |
| </div> | |
| <div class="ms-labels"> | |
| <span class="ms-name">Researcher</span> | |
| <span class="ms-sub">XAI · Mamba · Attention Maps</span> | |
| </div> | |
| </button> | |
| <button class="ms-opt" id="mD" onclick="setMode('d')"> | |
| <div class="ms-ico-w"> | |
| <svg class="ms-ico" width="15" height="15" fill="none" viewBox="0 0 15 15"> | |
| <circle cx="7.5" cy="4.5" r="2.2" stroke="currentColor" stroke-width="1.4"/> | |
| <path d="M2.5 13c0-2.76 2.24-5 5-5s5 2.24 5 5" stroke="currentColor" stroke-width="1.4" stroke-linecap="round"/> | |
| <path d="M10 9.5v2m-1-1h2" stroke="currentColor" stroke-width="1.3" stroke-linecap="round"/> | |
| </svg> | |
| </div> | |
| <div class="ms-labels"> | |
| <span class="ms-name">Doctor</span> | |
| <span class="ms-sub">Diagnosis · Anatomy · Report</span> | |
| </div> | |
| </button> | |
| </div> | |
| <span class="mode-hint" id="modeHint">XAI Suite · CKA Matrix · Mamba Engine · Layer Inspector · Feature Space · Performance</span> | |
| </div> | |
| <div class="demo-grid sf d3"> | |
| <!-- INPUT --> | |
| <div class="panel"> | |
| <div class="ph"><span class="ph-t">OCT B-Scan Input</span><span class="ph-s"><span class="sdot" id="inDot"></span><span id="inTxt">Select image</span></span></div> | |
| <div class="pb"> | |
| <div class="upzone" id="upZone"> | |
| <input type="file" accept="image/*" id="fileInp"> | |
| <div class="upzone-ico"><svg width="16" height="16" fill="none" viewBox="0 0 16 16"><path d="M8 12V3m0 0L4.5 6.5M8 3l3.5 3.5" stroke="#00C9B8" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/><path d="M2 12.5v.5A1.5 1.5 0 003.5 14.5h9A1.5 1.5 0 0014 13v-.5" stroke="#00C9B8" stroke-width="1.5" stroke-linecap="round"/></svg></div> | |
| <div class="upzone-t">Drop OCT B-scan here</div> | |
| <div class="upzone-s">PNG, JPG · 224×224+</div> | |
| </div> | |
| <div id="prevWrap"><canvas id="prevCv"></canvas><div id="prevMeta"></div></div> | |
| <div class="samp-lbl">Sample scans:</div> | |
| <div class="samp-g" id="sampG"></div> | |
| <button class="analyze-btn" id="aBtn" disabled onclick="runInf()"> | |
| <svg width="13" height="13" fill="none" viewBox="0 0 13 13"><circle cx="6.5" cy="6.5" r="5" stroke="currentColor" stroke-width="1.4"/><path d="M4.5 4l4 2.5-4 2.5V4z" fill="currentColor"/></svg> | |
| Analyze with RetViM | |
| </button> | |
| <div class="inf-prog" id="infProg"><div id="infSteps"></div></div> | |
| </div> | |
| </div> | |
| <!-- RESULTS --> | |
| <div class="panel" id="resPanel"> | |
| <div id="rTabBar" class="tab-bar"> | |
| <button class="rtab on" data-ri="0"><svg width="13" height="13" fill="none" viewBox="0 0 13 13"><rect x="1" y="1" width="4.5" height="4.5" rx=".8" stroke="currentColor" stroke-width="1.25"/><rect x="7.5" y="1" width="4.5" height="4.5" rx=".8" stroke="currentColor" stroke-width="1.25"/><rect x="1" y="7.5" width="4.5" height="4.5" rx=".8" stroke="currentColor" stroke-width="1.25"/><rect x="7.5" y="7.5" width="4.5" height="4.5" rx=".8" stroke="currentColor" stroke-width="1.25"/></svg>Overview</button> | |
| <button class="rtab" data-ri="1"><svg width="13" height="13" fill="none" viewBox="0 0 13 13"><ellipse cx="6.5" cy="6.5" rx="5.5" ry="3.2" stroke="currentColor" stroke-width="1.25"/><circle cx="6.5" cy="6.5" r="1.6" stroke="currentColor" stroke-width="1.25"/></svg>XAI Suite</button> | |
| <button class="rtab" data-ri="2"><svg width="13" height="13" fill="none" viewBox="0 0 13 13"><circle cx="2.5" cy="6.5" r="1.5" stroke="currentColor" stroke-width="1.2"/><circle cx="10.5" cy="2.5" r="1.5" stroke="currentColor" stroke-width="1.2"/><circle cx="10.5" cy="10.5" r="1.5" stroke="currentColor" stroke-width="1.2"/><path d="M4 6.5h2.5m0 0L10.5 4m-4 2.5L10.5 9" stroke="currentColor" stroke-width="1.2" stroke-linecap="round"/></svg>CKA + Features</button> | |
| <button class="rtab" data-ri="3"><svg width="13" height="13" fill="none" viewBox="0 0 13 13"><path d="M1 6.5c1-2.5 1.5-4 2.5-4s2 3 3 3 2-3 3-3 1.5 1.5 2.5 4" stroke="currentColor" stroke-width="1.3" stroke-linecap="round"/></svg>Mamba</button> | |
| <button class="rtab" data-ri="4"><svg width="13" height="13" fill="none" viewBox="0 0 13 13"><rect x="1.5" y="1.5" width="10" height="2.5" rx=".6" stroke="currentColor" stroke-width="1.2"/><rect x="1.5" y="5.2" width="10" height="2.5" rx=".6" stroke="currentColor" stroke-width="1.2"/><rect x="1.5" y="9" width="10" height="2.5" rx=".6" stroke="currentColor" stroke-width="1.2"/></svg>Layers</button> | |
| <button class="rtab" data-ri="5"><svg width="13" height="13" fill="none" viewBox="0 0 13 13"><path d="M1.5 10.5l2.5-3.5 2.5 1.5 2.5-4 2.5-3" stroke="currentColor" stroke-width="1.3" stroke-linecap="round" stroke-linejoin="round"/></svg>Performance</button> | |
| </div> | |
| <div id="dTabBar" class="tab-bar" style="display:none"> | |
| <button class="rtab on" data-di="0"><svg width="13" height="13" fill="none" viewBox="0 0 13 13"><circle cx="6.5" cy="6.5" r="5" stroke="currentColor" stroke-width="1.25"/><path d="M4.5 6.5l1.5 1.5 2.5-3" stroke="currentColor" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/></svg>Diagnosis</button> | |
| <button class="rtab" data-di="1"><svg width="13" height="13" fill="none" viewBox="0 0 13 13"><rect x="1.5" y="2" width="10" height="1.8" rx=".4" stroke="currentColor" stroke-width="1.15"/><rect x="1.5" y="4.8" width="10" height="1.8" rx=".4" stroke="currentColor" stroke-width="1.15"/><rect x="1.5" y="7.6" width="10" height="1.8" rx=".4" stroke="currentColor" stroke-width="1.15"/><rect x="1.5" y="10.2" width="5" height="1" rx=".4" stroke="currentColor" stroke-width="1.15"/></svg>Anatomy</button> | |
| <button class="rtab" data-di="2"><svg width="13" height="13" fill="none" viewBox="0 0 13 13"><circle cx="6.5" cy="2.5" r="1.5" stroke="currentColor" stroke-width="1.2"/><path d="M6.5 4v2.5m0 0l-2 2.5m2-2.5l2 2.5" stroke="currentColor" stroke-width="1.2" stroke-linecap="round"/><circle cx="4.5" cy="10" r="1.2" stroke="currentColor" stroke-width="1.2"/><circle cx="8.5" cy="10" r="1.2" stroke="currentColor" stroke-width="1.2"/></svg>Reasoning</button> | |
| <button class="rtab" data-di="3"><svg width="13" height="13" fill="none" viewBox="0 0 13 13"><rect x="2" y="1.5" width="9" height="10" rx="1" stroke="currentColor" stroke-width="1.25"/><path d="M4.5 4.5h4M4.5 6.5h4M4.5 8.5h2.5" stroke="currentColor" stroke-width="1.15" stroke-linecap="round"/></svg>Report</button> | |
| </div> | |
| <div id="stEmpty" class="st-empty"> | |
| <svg width="40" height="40" fill="none" viewBox="0 0 40 40" opacity=".2"><rect x="5" y="5" width="30" height="30" rx="7" stroke="currentColor" stroke-width="2"/><path d="M13 20h14M20 13v14" stroke="currentColor" stroke-width="2" stroke-linecap="round"/></svg> | |
| <div style="font-size:13px;color:rgba(255,255,255,.7)">Select a sample · click Analyze</div> | |
| <div style="font-size:11px;font-family:'Space Mono',monospace;color:rgba(255,255,255,.4)">Researcher: 6 panels · Doctor: 4 panels</div> | |
| </div> | |
| <div id="stLoad" class="st-load"> | |
| <div class="neural-terminal"> | |
| <div class="nt-header"> | |
| <div class="nt-dots"> | |
| <div class="nt-dot" style="background:#FF5E5E"></div> | |
| <div class="nt-dot" style="background:#FFB547"></div> | |
| <div class="nt-dot" style="background:#52E58A"></div> | |
| </div> | |
| <div class="nt-title">RetViM Neural Engine v4.0</div> | |
| <div style="font-family:'Space Mono',monospace;font-size:9px;color:rgba(0,201,184,.5)" id="ntDevice">CPU</div> | |
| </div> | |
| <div class="nt-body"> | |
| <div class="nt-cmd" id="lMsg">$ retvim --mode researcher --input scan.png</div> | |
| <div class="nt-steps lsteps" id="lSteps"></div> | |
| <div class="nt-prog"> | |
| <div class="nt-bar-bg"><div class="nt-bar-f" id="ntBarF"></div></div> | |
| <div class="nt-stat"> | |
| <span id="ntStepLbl">Initializing...</span> | |
| <span id="ntPct">0%</span> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- RESEARCHER PANES --> | |
| <div id="rPanes" style="display:none"> | |
| <div class="tab-pane on pane-body" id="rp0"> | |
| <div class="pred-h"> | |
| <div><div style="font-size:10px;color:rgba(255,255,255,.4);font-family:'Space Mono',monospace;margin-bottom:3px">PREDICTION</div><div class="pred-name" id="r0n"></div></div> | |
| <div style="text-align:right"><div class="pred-pct" id="r0p"></div><div class="pred-k" id="r0k"></div></div> | |
| </div> | |
| <div class="cg cg2"> | |
| <div class="card"><div class="card-t">Class probabilities</div><div id="r0bars"></div></div> | |
| <div class="card" style="display:flex;flex-direction:column;align-items:center;justify-content:center"> | |
| <div class="card-t">Probability donut</div> | |
| <canvas id="r0donut" width="120" height="120" style="max-width:120px"></canvas> | |
| </div> | |
| </div> | |
| <div class="cg cg2"> | |
| <div class="card"><div class="card-t">Metric radar</div><canvas id="r0radar" style="width:100%;height:180px;display:block"></canvas></div> | |
| <div class="card"><div class="card-t">Threshold curve</div><canvas id="r0thresh" style="width:100%;height:140px"></canvas></div> | |
| </div> | |
| <div class="cg"><div class="card"><div class="card-t">Full metrics</div><div id="r0mets"></div></div></div> | |
| </div> | |
| <div class="tab-pane pane-body" id="rp1"> | |
| <div class="card-t" style="margin-bottom:8px">Explainability method:</div> | |
| <div class="xai-row"> | |
| <button class="xtab on" data-xai="gcam">GradCAM</button> | |
| <button class="xtab" data-xai="gcam2">GradCAM++</button> | |
| <button class="xtab" data-xai="rollout">Attn Rollout</button> | |
| <button class="xtab" data-xai="occ">Occlusion</button> | |
| <button class="xtab" data-xai="ig">Integr. Grad.</button> | |
| <button class="xtab" data-xai="rise">RISE</button> | |
| </div> | |
| <div class="cg cg2" style="margin-top:8px"> | |
| <div class="card" style="padding:9px"><div class="card-t">Original OCT</div><canvas id="r1orig" style="width:100%;border-radius:7px"></canvas></div> | |
| <div class="card" style="padding:9px;position:relative"><div class="card-t" id="r1maptitle">GradCAM heatmap</div><canvas id="r1map" style="width:100%;border-radius:7px;display:block"></canvas> | |
| <div class="tco" id="r1maptco" style="display:none;border-radius:6px;min-height:120px"> | |
| <img id="r1maptco-img" style="position:absolute;inset:0;width:100%;height:100%;object-fit:cover;opacity:0.22;border-radius:6px;pointer-events:none"> | |
| <div class="tco-ring"></div> | |
| <div id="r1maptco-lbl" class="tco-title">Computing XAI</div> | |
| <div class="tco-sub">RetViM processing · results appear automatically</div> | |
| <div class="tco-dots"><span></span><span></span><span></span></div> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="cg"><div class="card" style="padding:9px"> | |
| <div class="card-t">Overlay (opacity: <span id="r1oplbl">50%</span>)</div> | |
| <canvas id="r1overlay" style="width:100%;border-radius:7px"></canvas> | |
| <div style="display:flex;align-items:center;gap:8px;margin-top:7px"> | |
| <span style="font-family:'Space Mono',monospace;font-size:10px;color:rgba(255,255,255,.4)">0%</span> | |
| <input type="range" min="10" max="90" value="50" id="r1op" style="flex:1" oninput="onOp()"> | |
| <span style="font-family:'Space Mono',monospace;font-size:10px;color:rgba(255,255,255,.4)">90%</span> | |
| </div> | |
| </div></div> | |
| <div class="cg cg2"> | |
| <div class="card" style="padding:9px"><div class="card-t">Top-10% attribution</div><canvas id="r1toppx" style="width:100%;border-radius:7px"></canvas><div class="card-sub">Red = positive · Blue = negative attribution</div></div> | |
| <div class="card" style="padding:9px"><div class="card-t">Attribution histogram</div><canvas id="r1hist" style="width:100%;height:110px"></canvas></div> | |
| </div> | |
| </div> | |
| <div class="tab-pane pane-body" id="rp2"> | |
| <div style="font-family:'Space Mono',monospace;font-size:9px;color:rgba(255,255,255,.35);letter-spacing:.06em;text-transform:uppercase;margin-bottom:12px;padding:6px 10px;border:1px solid rgba(255,255,255,.06);border-radius:6px;display:inline-block">Precomputed reference · full validation set statistics</div> | |
| <div class="cg"><div class="card"><div class="card-t">CKA Similarity Matrix — ViT backbone × SS-Conv-SSM Mamba blocks</div><canvas id="r2cka" style="width:100%;display:block;max-height:420px"></canvas><div class="card-sub">Mamba Blk-1/2 CKA = 0.99: near-identical representations · ViT early layers are dissimilar from SSM · Dashed lines separate architectural boundaries</div></div></div> | |
| <div class="cg cg3"> | |
| <div class="card"><div class="card-t">Representation entropy (bits)</div><canvas id="r2ent" style="width:100%;height:160px;display:block"></canvas><div class="card-sub">Peak at ViT Blk-7 → collapse in SSM</div></div> | |
| <div class="card"><div class="card-t">Effective rank</div><canvas id="r2rank" style="width:100%;height:160px;display:block"></canvas><div class="card-sub">Drusen drops to <1 after SSM</div></div> | |
| <div class="card"><div class="card-t">Feature energy</div><canvas id="r2enrg" style="width:100%;height:160px;display:block"></canvas><div class="card-sub">Explosion at ViT Blk-11</div></div> | |
| </div> | |
| <div class="cg cg2"> | |
| <div class="card" style="padding:9px"><div class="card-t">Patch attention rollout 14×14</div><canvas id="r2patch" style="width:100%;aspect-ratio:1;border-radius:7px;image-rendering:pixelated;display:block"></canvas><div class="card-sub">Real attention rollout resampled to patch grid · teal = high attention</div></div> | |
| <div class="card"><div class="card-t">t-SNE feature space — last-layer embeddings</div><canvas id="r2tsne" style="width:100%;height:180px;display:block"></canvas> | |
| <div class="legend" style="margin-top:8px"><div class="li"><div class="li-dot" style="background:#FF5E5E"></div>CNV</div><div class="li"><div class="li-dot" style="background:#FFB547"></div>DME</div><div class="li"><div class="li-dot" style="background:#C8A830"></div>Drusen</div><div class="li"><div class="li-dot" style="background:#52E58A"></div>Normal</div><div class="li"><div class="li-dot" style="background:#00C9B8"></div>Current</div></div> | |
| </div> | |
| </div> | |
| <div class="cg cg2"> | |
| <div class="card"><div class="card-t">Patch token L2 magnitude</div><canvas id="r2emb" style="width:100%;height:120px;display:block"></canvas><div class="card-sub">L2 norm across 196 patch positions</div></div> | |
| <div class="card"><div class="card-t">CLS token PCA projection</div><canvas id="r2pca" style="width:100%;height:120px;display:block"></canvas><div class="card-sub">First two principal components per class</div></div> | |
| </div> | |
| </div> | |
| <div class="tab-pane pane-body" id="rp3"> | |
| <div class="cg"><div class="card"> | |
| <div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:9px"> | |
| <div class="card-t" style="margin:0">MedMamba Block Analysis</div> | |
| <div style="display:flex;gap:4px"><button class="xtab on" onclick="setMambaBlock(0)">Block 0</button><button class="xtab" onclick="setMambaBlock(1)">Block 1</button></div> | |
| </div> | |
| <div class="card-sub" style="margin-bottom:8px">Real computed outputs from each MedMamba block processing your uploaded image</div> | |
| </div></div> | |
| <div class="cg" style="display:grid;grid-template-columns:1fr 1fr 1fr;gap:8px" id="r3branchRow"> | |
| <div class="card" style="padding:9px"><div class="card-t">Conv Branch (local)</div><canvas id="r3conv" style="width:100%;border-radius:7px;image-rendering:pixelated"></canvas><div class="card-sub">DW-Conv 7x7/5x5/1x1 activation</div></div> | |
| <div class="card" style="padding:9px"><div class="card-t">SSM Branch (global)</div><canvas id="r3ssm" style="width:100%;border-radius:7px;image-rendering:pixelated"></canvas><div class="card-sub">Selective state-space activation</div></div> | |
| <div class="card" style="padding:9px"><div class="card-t">Fused Output</div><canvas id="r3fuse" style="width:100%;border-radius:7px;image-rendering:pixelated"></canvas><div class="card-sub">Combined conv + SSM features</div></div> | |
| </div> | |
| <div class="cg"><div class="card" style="padding:9px"><div class="card-t">Branch Dominance Map (warm=Conv, cool=SSM)</div><canvas id="r3ratio" style="width:100%;height:80px;border-radius:7px;image-rendering:pixelated"></canvas><div class="card-sub">Which branch contributes more at each spatial location</div></div></div> | |
| <div class="cg cg2"> | |
| <div class="card"><div class="card-t">Delta step size (per patch position)</div><canvas id="r3dlt" style="width:100%;height:140px"></canvas><div class="card-sub">Large delta = model allocates big state update at this position</div></div> | |
| <div class="card"><div class="card-t">Gate signal SiLU(z) (per patch position)</div><canvas id="r3gate" style="width:100%;height:140px"></canvas><div class="card-sub">Controls information flow: 0=suppressed, 1=fully passed</div></div> | |
| </div> | |
| <div class="tco" id="rp3tco" style="display:none"> | |
| <img id="rp3tco-img" style="position:absolute;inset:0;width:100%;height:100%;object-fit:cover;opacity:0.18;border-radius:10px;pointer-events:none"> | |
| <div class="tco-ring"></div> | |
| <div class="tco-title">Computing Mamba Analysis</div> | |
| <div class="tco-sub">Running MedMamba block forward pass & extracting internal state maps…</div> | |
| <div class="tco-dots"><span></span><span></span><span></span></div> | |
| </div> | |
| </div> | |
| <div class="tab-pane pane-body" id="rp4"> | |
| <div class="cg" style="margin-bottom:0"> | |
| <div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:12px"> | |
| <div style="font-family:'Space Mono',monospace;font-size:10px;color:rgba(255,255,255,.45);letter-spacing:.08em;text-transform:uppercase">Layer Inspector</div> | |
| <div style="display:flex;gap:4px"><button class="xtab on" onclick="setLyrView('h')">Heads</button><button class="xtab" onclick="setLyrView('e')">Entropy & Stats</button></div> | |
| </div> | |
| </div> | |
| <div id="r4hView"> | |
| <div class="cg"><div class="card"> | |
| <div class="card-t">12 layers × 12 heads — real attention patterns</div> | |
| <div id="headsG" style="display:grid;grid-template-columns:repeat(12,1fr);gap:2px;margin-top:6px"></div> | |
| <div class="card-sub" style="margin-top:6px">Frozen L1–6 (gray) · Trainable L7–12 (teal) · CLS-to-patch attention from real forward pass</div> | |
| </div></div> | |
| </div> | |
| <div id="r4eView" style="display:none"> | |
| <div class="cg cg2"> | |
| <div class="card"><div class="card-t">Attention entropy per layer</div><canvas id="r4ent" style="width:100%;height:150px"></canvas><div class="card-sub">Shannon entropy of attention distributions — lower = more focused</div></div> | |
| <div class="card"><div class="card-t">Activation magnitude (L2 norm)</div><canvas id="r4mag" style="width:100%;height:150px"></canvas><div class="card-sub">12 ViT layers + 2 MedMamba blocks</div></div> | |
| </div> | |
| <div class="cg"><div class="card"><div class="card-t">CLS token class trajectory</div><canvas id="r4cls" style="width:100%;height:160px"></canvas><div class="card-sub">Cosine similarity of CLS token to each class prototype across all layers — shows where the model commits to its decision</div></div></div> | |
| <div class="cg"><div class="card"><div class="card-t">Frozen vs trainable activation distributions</div><canvas id="r4dst" style="width:100%;height:150px"></canvas><div class="card-sub">Attention value histograms: gray = frozen layers L1–6, teal = trainable layers L7–12</div></div></div> | |
| </div> | |
| <div class="tco" id="rp4tco" style="display:none"> | |
| <img id="rp4tco-img" style="position:absolute;inset:0;width:100%;height:100%;object-fit:cover;opacity:0.18;border-radius:10px;pointer-events:none"> | |
| <div class="tco-ring"></div> | |
| <div class="tco-title">Computing Layer Inspector</div> | |
| <div class="tco-sub">Extracting 12×12 attention head patterns from real forward pass…</div> | |
| <div class="tco-dots"><span></span><span></span><span></span></div> | |
| </div> | |
| </div> | |
| <div class="tab-pane pane-body" id="rp5"> | |
| <div style="font-family:'Space Mono',monospace;font-size:9px;color:rgba(255,255,255,.35);letter-spacing:.06em;text-transform:uppercase;margin-bottom:12px;padding:6px 10px;border:1px solid rgba(255,255,255,.06);border-radius:6px;display:inline-block">Precomputed from validation run · epoch 19 · 968 images</div> | |
| <div class="cg cg2"> | |
| <div class="card"><div class="card-t">ROC curves — 4-class one-vs-rest</div><canvas id="r5roc" style="width:100%;height:200px;display:block"></canvas><div class="card-sub">AUC computed on 968 balanced validation images (242/class)</div></div> | |
| <div class="card"><div class="card-t">Precision-Recall curves</div><canvas id="r5pr" style="width:100%;height:200px;display:block"></canvas><div class="card-sub">Average Precision per class on validation set</div></div> | |
| </div> | |
| <div class="cg"><div class="card"><div class="card-t">Confusion matrix — 968 validation images (242 per class)</div><canvas id="r5cm" style="display:block;width:100%;height:280px"></canvas><div class="card-sub">Overall accuracy: 967/968 = 99.90%</div></div></div> | |
| <div class="cg cg2"> | |
| <div class="card"><div class="card-t">Ablation study — validation accuracy</div><canvas id="r5ab" style="width:100%;height:160px;display:block"></canvas><div class="card-sub">Architecture comparison on Kermany OCT · same train/val split</div></div> | |
| <div class="card"><div class="card-t">Training history — 20 epochs</div><canvas id="r5tr" style="width:100%;height:160px;display:block"></canvas><div class="card-sub">Best checkpoint at epoch 19 (val_acc = 96.68%)</div></div> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- DOCTOR PANES --> | |
| <div id="dPanes" style="display:none"> | |
| <div class="tab-pane on pane-body" id="dp0"> | |
| <div class="dx-banner" id="d0ban"><div class="dx-ico" id="d0ico"></div><div><div class="dx-name" id="d0nm"></div><div class="dx-conf" id="d0cf"></div><div class="dx-icd" id="d0ic"></div></div></div> | |
| <div class="cg"><div class="card"><div class="card-t">Severity</div><div style="display:flex;justify-content:space-between;margin-bottom:3px"><span style="font-size:12px;color:rgba(255,255,255,.7)" id="d0sl"></span><span style="font-family:'Space Mono',monospace;font-size:12px;font-weight:700" id="d0sp"></span></div><div class="sev-bg"><div class="sev-f" id="d0sf" style="width:0%"></div></div><div class="sev-ticks"><span>Normal</span><span>Mild</span><span>Moderate</span><span>Severe</span></div></div></div> | |
| <div class="cg cg2"> | |
| <div class="card"><div class="card-t">Confidence by class</div><div id="d0brs"></div></div> | |
| <div class="card"><div class="card-t">Differential diagnosis</div><div class="diff-g" id="d0dif"></div></div> | |
| </div> | |
| </div> | |
| <div class="tab-pane pane-body" id="dp1"> | |
| <div class="cg"><div class="card" style="padding:12px"><div class="card-t" style="margin-bottom:10px">Retinal Anatomy — Pathology Map</div><canvas id="d1an" style="width:100%;border-radius:8px;display:block"></canvas></div></div> | |
| <div class="cg"><div class="card"><div class="card-t">Layer status</div><div id="d1lst"></div></div></div> | |
| <div class="cg"><div class="card"><div class="card-t">Thickness: normal (dim) vs current (colored)</div><canvas id="d1th" style="width:100%;height:140px"></canvas></div></div> | |
| </div> | |
| <div class="tab-pane pane-body" id="dp2"> | |
| <div class="cg cg2"> | |
| <div class="card" style="padding:9px"><div class="card-t">Original scan</div><canvas id="d2or" style="width:100%;border-radius:7px"></canvas></div> | |
| <div class="card" style="padding:9px"><div class="card-t">AI focus (GradCAM)</div><canvas id="d2gc" style="width:100%;border-radius:7px"></canvas></div> | |
| </div> | |
| <div class="cg"><div class="card"><div class="card-t">Evidence narrative</div><div id="d2ev" style="font-size:13px;line-height:1.7;color:rgba(255,255,255,.7)"></div></div></div> | |
| <div class="cg cg2"> | |
| <div class="card"><div class="card-t">Key visual features</div><div id="d2ft"></div></div> | |
| <div class="card"><div class="card-t">Certainty vs threshold</div><canvas id="d2th" style="width:100%;height:120px"></canvas></div> | |
| </div> | |
| </div> | |
| <div class="tab-pane pane-body" id="dp3"> | |
| <div style="font-family:'Space Mono',monospace;font-size:8px;color:rgba(255,255,255,.32);letter-spacing:.06em;text-transform:uppercase;margin-bottom:14px;padding:6px 10px;border:1px solid rgba(255,94,94,.12);border-radius:6px;display:inline-block;line-height:1.5">Reference template · not AI-generated diagnosis · for research demonstration only</div> | |
| <div style="margin-bottom:12px"><div class="clin-t">Primary Finding</div><div class="clin-p" id="d3fi"></div></div> | |
| <div style="margin-bottom:12px"><div class="clin-t">Pathological Features</div><div class="clin-p" id="d3pf"></div></div> | |
| <div style="margin-bottom:12px"><div class="clin-t">Risk Stratification</div><div class="clin-p" id="d3rk"></div></div> | |
| <div class="rec-box"><div style="flex-shrink:0;width:32px;height:32px;border-radius:8px;background:rgba(0,201,184,.12);border:1px solid rgba(0,201,184,.25);display:grid;place-items:center"><svg width="14" height="14" fill="none" viewBox="0 0 14 14"><path d="M7 2v10M2 7h10" stroke="#00C9B8" stroke-width="1.5" stroke-linecap="round"/></svg></div><div><div class="rec-ht">Clinical Recommendation</div><div class="rec-txt" id="d3rc"></div></div></div> | |
| <div style="margin-top:14px"><div class="clin-t">Treatment Pathway</div><div id="d3pt"></div></div> | |
| </div> | |
| </div> | |
| </div><!-- end results panel --> | |
| </div><!-- end demo-grid --> | |
| </div> | |
| </section> | |
| <!-- ── RESEARCH VISUALIZATIONS ── --> | |
| <section class="section section-dark" id="visualizations" style="background:#0D1117"> | |
| <div class="section-inner"> | |
| <div class="sec-eye sf">Deep Analysis · Real Model Outputs</div> | |
| <h2 class="sec-head sf d1">Research<br><em>Visualizations</em></h2> | |
| <p class="sec-sub sf d2">Every figure generated from the actual trained RetViM model on real Kermany OCT data.</p> | |
| <!-- Visualization tab picker --> | |
| <div class="res-tab-bar sf d3"> | |
| <button class="res-tab on" onclick="showResTab(0)">Neural Journey</button> | |
| <button class="res-tab" onclick="showResTab(1)">Feature Evolution</button> | |
| <button class="res-tab" onclick="showResTab(2)">Class Discrimination</button> | |
| <button class="res-tab" onclick="showResTab(3)">Inside Blocks</button> | |
| <button class="res-tab" onclick="showResTab(4)">Feature Space</button> | |
| <button class="res-tab" onclick="showResTab(5)">Activation Overlays</button> | |
| <button class="res-tab" onclick="showResTab(6)">Feature Dictionary</button> | |
| <button class="res-tab" onclick="showResTab(7)">Feature Tree</button> | |
| <button class="res-tab" onclick="showResTab(8)">DeepDream</button> | |
| </div> | |
| <!-- Tab 0: Neural Journey — dual-mode live viewer --> | |
| <div class="res-content on" id="rt0" style="background:#0D1117"> | |
| <!-- Mini class + mode picker inside research tab --> | |
| <div style="display:flex;align-items:center;justify-content:space-between;flex-wrap:wrap;gap:12px;margin-bottom:20px;padding:14px 18px;background:rgba(255,255,255,.02);border:1px solid rgba(255,255,255,.07);border-radius:14px"> | |
| <div style="display:flex;gap:6px;flex-wrap:wrap" id="rtJcls"> | |
| <button class="jcls-btn on" data-cls="CNV" onclick="rtSetCls('CNV')">CNV</button> | |
| <button class="jcls-btn" data-cls="DME" onclick="rtSetCls('DME')">DME</button> | |
| <button class="jcls-btn" data-cls="DRUSEN" onclick="rtSetCls('DRUSEN')">Drusen</button> | |
| <button class="jcls-btn" data-cls="NORMAL" onclick="rtSetCls('NORMAL')">Normal</button> | |
| </div> | |
| <div class="jmode-bar" id="rtJmode"> | |
| <button class="jmode-btn" onclick="rtSetMode('demo')" id="rtmDemo">Demo</button> | |
| <button class="jmode-btn on" onclick="rtSetMode('both')" id="rtmBoth">Compare</button> | |
| <button class="jmode-btn" onclick="rtSetMode('real')" id="rtmReal">Real</button> | |
| </div> | |
| </div> | |
| <!-- Single view --> | |
| <div id="rtJSingle" style="display:none"> | |
| <div class="jimg-wrap"> | |
| <img id="rtSingleImg" src="" class="fig-full" alt="Neural Journey" loading="lazy"> | |
| <div class="jbadge" id="rtSingleBadge">Demo</div> | |
| </div> | |
| <p class="jcaption" id="rtSingleCap"></p> | |
| </div> | |
| <!-- Compare view (default) --> | |
| <div id="rtJCompare" class="jcompare-grid"> | |
| <div> | |
| <div style="display:flex;align-items:center;gap:8px;margin-bottom:8px"> | |
| <span style="width:8px;height:8px;background:#FFB300;border-radius:50%;display:inline-block;flex-shrink:0"></span> | |
| <span style="font-family:'Space Mono',monospace;font-size:10px;font-weight:700;color:#FFB300;letter-spacing:.06em">DEMO · RetViMNet 384d · 4×Mamba</span> | |
| </div> | |
| <div class="jimg-wrap jglow-demo"> | |
| <img id="rtDemoImg" src="images/neural_journey_demo_CNV.png" class="fig-full" alt="Demo Journey" loading="lazy"> | |
| <div class="jbadge demo">Demo Weights</div> | |
| </div> | |
| <p class="jcaption" id="rtDemoCap">RetViMNet · 384-dim · ViT-Small transfer + OCT fine-tune · 12 ViT + 4 Mamba blocks</p> | |
| </div> | |
| <div> | |
| <div style="display:flex;align-items:center;gap:8px;margin-bottom:8px"> | |
| <span style="width:8px;height:8px;background:#00C9B8;border-radius:50%;display:inline-block;flex-shrink:0"></span> | |
| <span style="font-family:'Space Mono',monospace;font-size:10px;font-weight:700;color:#00C9B8;letter-spacing:.06em">REAL · ImprovedMedMamba 768d · 2×Mamba</span> | |
| </div> | |
| <div class="jimg-wrap jglow-real"> | |
| <img id="rtRealImg" src="images/neural_journey_real_CNV.png" class="fig-full" alt="Real Journey" loading="lazy"> | |
| <div class="jbadge real">Real · 96.68% val acc</div> | |
| </div> | |
| <p class="jcaption" id="rtRealCap">ImprovedMedMamba · 768-dim · ViT-Base/16 + 2 MedMamba blocks · val_acc = 96.68%</p> | |
| </div> | |
| </div> | |
| <!-- Journey legend --> | |
| <div class="jlegend" style="margin-top:20px"> | |
| <span style="font-family:'Space Mono',monospace;font-size:9px;color:rgba(255,255,255,.3);text-transform:uppercase;letter-spacing:.1em">Rows:</span> | |
| <div class="jleg-item"><div class="jleg-dot" style="background:linear-gradient(135deg,#7C3AED,#06B6D4)"></div>Feature Heatmaps</div> | |
| <div class="jleg-item"><div class="jleg-dot" style="background:linear-gradient(135deg,#16A34A,#FACC15)"></div>Raw Activations</div> | |
| <div class="jleg-item"><div class="jleg-dot" style="background:linear-gradient(135deg,#DC2626,#F97316)"></div>Attention Overlays</div> | |
| <span style="flex:1"></span> | |
| <div class="jleg-item"><div class="jleg-dot" style="background:#4FC3F7"></div>ViT Layers</div> | |
| <div class="jleg-item"><div class="jleg-dot" style="background:#FFB300"></div>Mamba Layers</div> | |
| </div> | |
| </div> | |
| <!-- Tab 1: Feature Evolution --> | |
| <div class="res-content" id="rt1"> | |
| <div class="fig-row"> | |
| <div class="fig-wrap"> | |
| <img src="images/wow_1_feature_journey.png" class="fig-full" alt="Feature Journey All 4 Classes"> | |
| <div class="fig-label blue">All 4 Classes · Feature Journey</div> | |
| </div> | |
| <p class="fig-caption">Input → Patch Embed → ViT Blk 3/7/11 → Mamba Blk 0/1/2/3 · Each row = one disease class · ViT stages: cool tones · Mamba stages: warm inferno</p> | |
| </div> | |
| <div class="fig-row"> | |
| <div class="fig-wrap"> | |
| <img src="images/ultimate_2_feature_journey.png" class="fig-full" alt="Feature Journey v2"> | |
| <div class="fig-label purple">Feature Journey v2</div> | |
| </div> | |
| <p class="fig-caption">Alternative feature evolution view · cleaner layout showing progression from raw pixel features to diagnostic representations</p> | |
| </div> | |
| <div class="fig-row"> | |
| <div class="fig-wrap"> | |
| <img src="images/fig_flow_DME.png" class="fig-full" alt="DME Feature Evolution"> | |
| <div class="fig-label amber">DME Feature Evolution</div> | |
| </div> | |
| <p class="fig-caption">DME classification · attention focuses on INL/OPL cystoid spaces · Mamba blocks refine the central macular signal · attention progressively concentrates</p> | |
| </div> | |
| </div> | |
| <!-- Tab 2: Class Discrimination --> | |
| <div class="res-content" id="rt2"> | |
| <div class="fig-row"> | |
| <div class="fig-wrap"> | |
| <img src="images/ultimate_3_class_discrimination.png" class="fig-full" alt="Class Discrimination"> | |
| <div class="fig-label blue">How Network Distinguishes 4 Classes</div> | |
| </div> | |
| <p class="fig-caption">PCA across all pipeline stages (Patch Embed → ViT Blk 3/11 → Mamba Blk 0/1/2/3) + t-SNE final layer + average final-layer activations per class · Classes become perfectly separable by Mamba stage</p> | |
| </div> | |
| <div class="fig-row"> | |
| <div class="fig-wrap"> | |
| <img id="classCompDynImg" src="images/fig4_class_comparison.png" class="fig-full" alt="Disease-Specific Activation Patterns"> | |
| <span id="classCompLiveBadge" style="display:none;font-size:9px;color:#00C9B8;font-weight:600">● Live</span> | |
| <div class="fig-label red">Disease-Specific Activation Patterns</div> | |
| </div> | |
| <p class="fig-caption">Row = disease class · Columns = ViT Blk 3, ViT Blk 11, Mamba 0, Mamba 2, Mamba 3 · Each uses class-specific color: CNV=red, DME=blue, Drusen=gold, Normal=green</p> | |
| </div> | |
| <div class="fig-row"> | |
| <div class="fig-wrap"> | |
| <img src="images/wow_6_comparison.png" class="fig-full" alt="Class Visual Signatures"> | |
| <div class="fig-label purple">Class Visual Signatures</div> | |
| </div> | |
| <p class="fig-caption">Visual signatures of each disease — Original OCT → ViT early/late features → Mamba features → final GradCAM overlay · Each class has distinct network fingerprint</p> | |
| </div> | |
| </div> | |
| <!-- Tab 3: Inside Blocks --> | |
| <div class="res-content" id="rt3"> | |
| <div class="fig-row"> | |
| <div class="fig-wrap"> | |
| <img src="images/ultimate_4_inside_blocks.png" class="fig-full" alt="Inside Neural Blocks"> | |
| <div class="fig-label amber">Inside ViT Attention + Mamba State-Space</div> | |
| </div> | |
| <p class="fig-caption">Top: ViT self-attention architecture (Q·Kᵀ/√d) + SS-Conv-SSM dual branch (h'=Ah+Bx, y=Ch+Dx) · Bottom rows: actual feature maps at Early ViT/Middle ViT/Late ViT/Early Mamba/Middle Mamba/Final Mamba</p> | |
| </div> | |
| </div> | |
| <!-- Tab 4: PCA Feature Space --> | |
| <div class="res-content" id="rt4"> | |
| <div class="fig-row"> | |
| <div class="fig-wrap"> | |
| <img id="pcaDynImg" src="images/fig_pca_embeddings.png" class="fig-full" alt="PCA Feature Space Across Layers"> | |
| <div class="fig-label blue">PCA Feature Space Across 8 Layers <span id="pcaLiveBadge" style="display:none;font-size:9px;color:#00C9B8;margin-left:6px;font-weight:600">● Live</span></div> | |
| </div> | |
| <p class="fig-caption">8-panel PCA: Patch Embed → ViT Blk 3/7/11 → Mamba Blk 0/1/2/3 · Patch Embed: overlapping butterfly shape → ViT Blk 11: classes begin separating → Mamba: tight well-separated clusters · Var explained increases Blk-11 67.7% → Mamba 70.1%</p> | |
| </div> | |
| </div> | |
| <!-- Tab 5: Activation Overlays --> | |
| <div class="res-content" id="rt5"> | |
| <div class="fig-row"> | |
| <div class="fig-wrap"> | |
| <img src="images/wow_3_activation_overlays.png" class="fig-full" alt="Activation Heatmaps Across Classes"> | |
| <div class="fig-label red">Activation Heatmaps — All 4 Classes</div> | |
| </div> | |
| <p class="fig-caption">What the network sees: Original → Patch Embed → ViT Blk 7 → Mamba Blk 0 → Mamba Blk 3 · Color-coded by disease: CNV=red, DME=cyan, Drusen=gold, Normal=teal · Brighter = higher activation</p> | |
| </div> | |
| </div> | |
| <!-- Tab 6: Feature Dictionary --> | |
| <div class="res-content" id="rt6"> | |
| <div class="fig-row"> | |
| <div class="fig-wrap"> | |
| <img src="images/fig2_feature_grid.png" class="fig-full" alt="Neural Network Feature Dictionary"> | |
| <div class="fig-label purple">Feature Dictionary — Maximally Activating Patterns</div> | |
| </div> | |
| <p class="fig-caption">8×8 grid · Rows = Patch Embed, ViT Blk 0/3/7/11 (blue), Mamba Blk 0/2/3 (red) · Columns = 8 channels · Each cell = synthetic input that maximally activates that channel · Early: color grids → Late: curved retinal structures</p> | |
| </div> | |
| </div> | |
| <!-- Tab 7: Feature Tree --> | |
| <div class="res-content" id="rt7"> | |
| <div class="fig-row"> | |
| <div class="fig-wrap"> | |
| <img src="images/fig1_feature_tree.png" class="fig-full" alt="Hierarchical Feature Visualization"> | |
| <div class="fig-label blue">Hierarchical Feature Tree</div> | |
| </div> | |
| <p class="fig-caption">Feature lineage across patch-embed → vit-3 → vit-7 → vit-11 → mamba-1 → mamba-3 · Lines show which features are preserved/transformed · Blue = ViT self-attention · Orange = Mamba state-space</p> | |
| </div> | |
| </div> | |
| <!-- Tab 8: DeepDream --> | |
| <div class="res-content" id="rt8"> | |
| <div class="fig-row"> | |
| <div class="fig-wrap"> | |
| <img src="images/fig4_deepdream.png" class="fig-full" alt="DeepDream — Amplifying Network Perception"> | |
| <div class="fig-label amber">DeepDream — What Network Amplifies</div> | |
| </div> | |
| <p class="fig-caption">Enhancing features the network detects in real OCT images · Original → ViT Block 5 (frozen, mid-level) → ViT Block 11 (trainable, semantic) → Combined · Network amplifies retinal layer boundaries and pathological textures</p> | |
| </div> | |
| </div> | |
| </div> | |
| </section> | |
| <!-- ── STATS ── --> | |
| <section class="stats-section" id="paper"> | |
| <div class="stats-glow"></div> | |
| <div class="stats-inner"> | |
| <div class="stats-head-wrap"> | |
| <div class="sec-eye sf">Benchmark · Kermany OCT 2017 · 968 test images</div> | |
| <h2 class="sec-head sf d1">84,496 images trained.<br><em>99.90% test accuracy. 1 error in 968.</em></h2> | |
| </div> | |
| <div class="stats-g"> | |
| <div class="sc sf d1"><div class="sc-v">99.90<span class="sc-u">%</span></div><div class="sc-l">Test Accuracy<br>967/968 images</div></div> | |
| <div class="sc sf d2"><div class="sc-v">96.68<span class="sc-u">%</span></div><div class="sc-l">Val Accuracy<br>epoch 19 / 20</div></div> | |
| <div class="sc sf d3"><div class="sc-v">1.0000</div><div class="sc-l">AUC-ROC<br>4-class one-vs-rest</div></div> | |
| <div class="sc sf" style="transition-delay:.35s"><div class="sc-v">0.9986</div><div class="sc-l">Cohen's Kappa<br>near-perfect agreement</div></div> | |
| <div class="sc sf" style="transition-delay:.4s"><div class="sc-v">101.2<span class="sc-u">M</span></div><div class="sc-l">Parameters<br>58.1M trainable</div></div> | |
| </div> | |
| </div> | |
| </section> | |
| <!-- ── CITATION ── --> | |
| <section class="section section-dark" id="citation" style="background:#0D1117;padding-top:80px;padding-bottom:80px"> | |
| <div class="section-inner" style="max-width:900px"> | |
| <div class="sec-eye sf">§ 7 — Citation & Copyright</div> | |
| <h2 class="sec-head sf d1">Cite This<br><em>Paper</em></h2> | |
| <p class="sec-sub sf d2" style="margin-bottom:36px">If you use RetViM in your research, please cite the following. The paper is accepted/published in IEEE conference proceedings.</p> | |
| <div class="sf d3" style="margin-bottom:20px"> | |
| <div class="terminal-box"> | |
| <div class="terminal-header"> | |
| <div class="term-dot term-dot-r"></div><div class="term-dot term-dot-b"></div><div class="term-dot term-dot-g"></div> | |
| <div class="term-title">bibtex — retvim2026.bib</div> | |
| </div> | |
| <div class="terminal-body"><span class="term-key">@inproceedings</span><span style="color:rgba(255,255,255,.5)">{</span><span class="term-val">desai2026retvim</span><span style="color:rgba(255,255,255,.5)">,</span> | |
| <span class="term-key">title</span> = {<span class="term-str">RetViM: Sequential Hybrid Vision Transformer with MedMamba for Retinal Disease Classification</span>}, | |
| <span class="term-key">author</span> = {<span class="term-str">Desai, Khamir and Mehta, Mayuri A. and Vadapalli, Sree Saicharan</span>}, | |
| <span class="term-key">booktitle</span> = {<span class="term-str">IEEE Conference Proceedings</span>}, | |
| <span class="term-key">year</span> = {<span class="term-val">2026</span>}, | |
| <span class="term-key">institution</span> = {<span class="term-str">Sarvajanik College of Engineering and Technology, Surat, India</span>}, | |
| <span class="term-key">note</span> = {<span class="term-str">ViT-B/16 + 2 SS-Conv-SSM MedMamba blocks; 99.90% test accuracy on Kermany OCT 2017</span>} | |
| <span style="color:rgba(255,255,255,.5)">}</span></div> | |
| </div> | |
| </div> | |
| <div class="sf" style="margin-bottom:24px"> | |
| <div class="terminal-box"> | |
| <div class="terminal-header"> | |
| <div class="term-dot term-dot-r"></div><div class="term-dot term-dot-b"></div><div class="term-dot term-dot-g"></div> | |
| <div class="term-title">IEEE Citation Format</div> | |
| </div> | |
| <div class="terminal-body"><span class="term-val">K. Desai</span>, <span class="term-val">M. A. Mehta</span>, and <span class="term-val">S. S. Vadapalli</span>, <span class="term-str">"RetViM: Sequential Hybrid Vision Transformer with MedMamba for Retinal Disease Classification,"</span> in <span class="term-key">IEEE Conference Proceedings</span>, 2026.</div> | |
| </div> | |
| </div> | |
| <div class="sf ieee-notice"> | |
| <strong>IEEE Copyright Notice</strong><br> | |
| © 2026 IEEE. Personal use of this material is permitted. Permission from IEEE must be obtained for all other uses, in any current or future media, including reprinting/republishing this material for advertising or promotional purposes, creating new collective works, for resale or redistribution to servers or lists, or reuse of any copyrighted component of this work in other works.<br><br> | |
| This companion website is maintained by the authors for academic and educational purposes. All figures, diagrams, interactive demos, and code on this site are original author contributions. The abstract is reproduced with author permission consistent with IEEE author rights. For the published paper, please refer to IEEE Xplore.<br><br> | |
| <strong>Dataset:</strong> Kermany, D.S. et al. "Identifying Medical Diagnoses and Treatable Diseases by Image-Based Deep Learning." <em>Cell</em>, 172(5), 1122–1131.e9, 2018. Accessed via Mendeley Data. | |
| </div> | |
| </div> | |
| </section> | |
| <footer style="background:#080B0F;border-top:1px solid rgba(255,255,255,.08);padding:32px 40px"> | |
| <div style="max-width:1400px;margin:0 auto;display:flex;flex-direction:column;gap:20px"> | |
| <div style="display:flex;align-items:center;justify-content:space-between;flex-wrap:wrap;gap:16px"> | |
| <a href="#" class="logo" style="text-decoration:none">Ret<b>ViM</b></a> | |
| <div style="display:flex;gap:24px;flex-wrap:wrap"> | |
| <a href="#motivation" style="font-size:12px;color:rgba(255,255,255,.35);text-decoration:none;transition:color .2s" onmouseover="this.style.color='#fff'" onmouseout="this.style.color='rgba(255,255,255,.35)'">Problem</a> | |
| <a href="#dataset" style="font-size:12px;color:rgba(255,255,255,.35);text-decoration:none;transition:color .2s" onmouseover="this.style.color='#fff'" onmouseout="this.style.color='rgba(255,255,255,.35)'">Dataset</a> | |
| <a href="#methodology" style="font-size:12px;color:rgba(255,255,255,.35);text-decoration:none;transition:color .2s" onmouseover="this.style.color='#fff'" onmouseout="this.style.color='rgba(255,255,255,.35)'">Methods</a> | |
| <a href="#results-tables" style="font-size:12px;color:rgba(255,255,255,.35);text-decoration:none;transition:color .2s" onmouseover="this.style.color='#fff'" onmouseout="this.style.color='rgba(255,255,255,.35)'">Results</a> | |
| <a href="#xai-section" style="font-size:12px;color:rgba(255,255,255,.35);text-decoration:none;transition:color .2s" onmouseover="this.style.color='#fff'" onmouseout="this.style.color='rgba(255,255,255,.35)'">XAI</a> | |
| <a href="#demo" style="font-size:12px;color:rgba(255,255,255,.35);text-decoration:none;transition:color .2s" onmouseover="this.style.color='#fff'" onmouseout="this.style.color='rgba(255,255,255,.35)'">Demo</a> | |
| <a href="#citation" style="font-size:12px;color:rgba(255,255,255,.35);text-decoration:none;transition:color .2s" onmouseover="this.style.color='#fff'" onmouseout="this.style.color='rgba(255,255,255,.35)'">Cite</a> | |
| </div> | |
| </div> | |
| <div style="height:1px;background:rgba(255,255,255,.06)"></div> | |
| <div style="display:flex;justify-content:space-between;flex-wrap:wrap;gap:12px"> | |
| <div class="fi" style="text-align:left">Sarvajanik College of Engineering and Technology · Surat, India<br>K. Desai · M. A. Mehta · S. S. Vadapalli · 2026</div> | |
| <div class="fi" style="text-align:right">© 2026 IEEE. All rights reserved.<br>Kermany OCT 2017 · NVIDIA A100 · PyTorch BF16</div> | |
| </div> | |
| </div> | |
| </footer> | |
| <script> | |
| // ═══ API CONFIG ══════════════════════════════════════════════ | |
| const API_BASE = 'http://localhost:7860'; | |
| const API_LIVE = true; | |
| // ═══ DATA ═══════════════════════════════════════════════════ | |
| const CLS=['CNV','DME','DRUSEN','NORMAL']; | |
| const CLR={ | |
| CNV:{hex:'#FF5E5E',pale:'rgba(255,94,94,.12)',cls:'cnv',em:'CNV',icd:'H35.32 — Exudative AMD',sev:.91,sc:'#FF5E5E',sl:'Severe',k:'0.9712',prob:[.9712,.0138,.0121,.0029]}, | |
| DME:{hex:'#FFB547',pale:'rgba(255,181,71,.12)',cls:'dme',em:'DME',icd:'E11.311 — Type 2 DM + macular edema',sev:.67,sc:'#FFB547',sl:'Moderate',k:'0.9588',prob:[.0102,.9588,.0224,.0086]}, | |
| DRUSEN:{hex:'#C8A830',pale:'rgba(200,168,48,.1)',cls:'drusen',em:'DRS',icd:'H35.36 — Drusen of macula',sev:.33,sc:'#C8A830',sl:'Mild–Moderate',k:'0.9473',prob:[.0085,.0162,.9473,.0280]}, | |
| NORMAL:{hex:'#52E58A',pale:'rgba(82,229,138,.1)',cls:'normal',em:'NRM',icd:'Z01.01 — Eye exam, normal findings',sev:.04,sc:'#52E58A',sl:'Normal',k:'0.9883',prob:[.0012,.0031,.0074,.9883]} | |
| }; | |
| const NAMES={CNV:'Choroidal Neovascularization',DME:'Diabetic Macular Edema',DRUSEN:'Drusen (Dry AMD)',NORMAL:'Normal Retina'}; | |
| const CLIN={ | |
| CNV:{fi:'OCT demonstrates subretinal neovascular membrane with disruption of Bruch\'s membrane. Hyper-reflective sub-RPE material consistent with type II CNV. Active exudation with sub-retinal fluid.',pf:'1) Hyper-reflective fibrovascular PED. 2) Sub-retinal fluid. 3) IS/OS junction disruption. 4) Pachychoroid features.',rk:'HIGH RISK — Active neovascular AMD. VA loss: 3+ lines within 1–3 months untreated.',rc:'Urgent anti-VEGF within 48–72h. Ranibizumab 0.5mg or Aflibercept 2mg. Loading × 3 monthly then PRN.',ev:'RetViM identified sub-RPE hyper-reflective material (89.3% CNV-attributed). GradCAM hotspot precisely at RPE/Bruch\'s membrane interface — anatomically correct for CNV. Sub-retinal fluid pooling pattern 94.7% CNV-specific. Model entropy 0.012 bits — near-zero uncertainty.',ft:['Subretinal hyper-reflective material','RPE disruption at Bruch\'s membrane','Cystoid macular edema','Pigment epithelial detachment','IS/OS junction disruption'],pt:[{i:'01',u:'urgent',t:'Emergency Ophthalmology',d:'Same-day referral. FA + OCT-A to characterize CNV type.'},{i:'02',u:'urgent',t:'Anti-VEGF Injection',d:'Within 48–72h. Loading × 3 monthly. Reassess after each cycle.'},{i:'03',u:'moderate',t:'Response Monitoring',d:'OCT at 4 weeks. CMT, SRF resolution, VA change.'},{i:'04',u:'routine',t:'Genetic + Lifestyle',d:'CFH/ARMS2 genotyping. AREDS2. Smoking cessation.'}],an:{NFL:'norm',GCL:'norm',IPL:'norm',INL:'mild',OPL:'mild',ONL:'aff',IS_OS:'aff',RPE:'aff',CHOROID:'aff'}}, | |
| DME:{fi:'Significant intraretinal fluid with cystoid macular edema in INL and OPL. CMT markedly elevated (>350μm). Center-involving DME with blood-retinal barrier breakdown.',pf:'1) Cystoid spaces in INL/OPL. 2) CMT >350μm. 3) Hard exudates at OPL/INL. 4) Focal sub-retinal fluid.',rk:'MODERATE-HIGH — Center-involving DME. VA loss 40–50% over 3 years untreated. HbA1c critical.',rc:'Anti-VEGF first-line: ranibizumab 0.3mg monthly. Poor response → intravitreal steroid (Ozurdex). Endocrinology referral.',ev:'Cystoid fluid pockets in INL/OPL (96.2% DME-attributed). Hard exudate deposits at layer boundaries (81.4%). Petaloid cystoid pattern (88.9%). Attribution distributed across macula — consistent with DME vs focal CNV pattern.',ft:['Cystoid macular edema','Hard exudate deposits','Central macular thickening','Sub-retinal fluid (temporal)','Foveal contour disruption'],pt:[{i:'01',u:'moderate',t:'Urgent Retinal Referral',d:'Within 1–2 weeks. FA, baseline VA and OCT.'},{i:'02',u:'moderate',t:'Anti-VEGF Therapy',d:'Ranibizumab 0.3mg monthly × 6. PRN after loading.'},{i:'03',u:'moderate',t:'Systemic Optimization',d:'HbA1c <7%, BP <130/80. Statin therapy.'},{i:'04',u:'routine',t:'Long-term Monitoring',d:'Bilateral OCT every 3–6 months. Annual FFA.'}],an:{NFL:'norm',GCL:'mild',IPL:'mild',INL:'aff',OPL:'aff',ONL:'mild',IS_OS:'mild',RPE:'norm',CHOROID:'norm'}}, | |
| DRUSEN:{fi:'Multiple medium-to-large drusen (>125μm) in sub-RPE space. Intermediate dry AMD. Irregular RPE elevation. No CNV.',pf:'1) Soft confluent drusen >125μm. 2) Irregular RPE elevation. 3) No neovascularization. 4) Mild ONL thinning over drusen.',rk:'MODERATE — Intermediate dry AMD. 10-year advanced AMD risk: 18–30% with large drusen.',rc:'AREDS2 supplements immediately. Annual OCT. Smoking cessation mandatory. Daily Amsler grid.',ev:'Sub-RPE hyperreflective deposits with drusen morphology (99.4%). RPE undulation pattern (91.2%). No subretinal fluid or neovascular signal. GradCAM hotspots correctly localize to RPE layer — where drusen physically reside.',ft:['Soft drusen deposits (>125μm)','RPE irregularity/undulation','Sub-RPE material accumulation','No neovascularization','Mild photoreceptor changes'],pt:[{i:'01',u:'routine',t:'Ophthalmology Follow-up',d:'Annual or 6-monthly. OCT + fundus photography.'},{i:'02',u:'routine',t:'AREDS2 Supplementation',d:'25% reduction in progression to advanced AMD at 5 years.'},{i:'03',u:'moderate',t:'Lifestyle Modification',d:'Smoking cessation (2–4× risk). UV protection. Omega-3.'},{i:'04',u:'routine',t:'Self-Monitoring',d:'Daily Amsler. Report metamorphopsia immediately — may signal CNV.'}],an:{NFL:'norm',GCL:'norm',IPL:'norm',INL:'norm',OPL:'norm',ONL:'mild',IS_OS:'mild',RPE:'aff',CHOROID:'norm'}}, | |
| NORMAL:{fi:'Normal macular architecture. All retinal layers intact. Foveal contour preserved. No pathological fluid, deposits, or abnormalities.',pf:'1) Normal foveal pit. 2) Intact IS/OS junction. 3) Smooth uniform RPE. 4) Normal NFL. 5) Normal choroidal thickness.',rk:'LOW RISK — Normal retinal findings. Routine monitoring only.',rc:'Annual ophthalmic examination. No intervention required. Educate: report metamorphopsia or acute visual change.',ev:'All retinal layers intact with normal reflectivity (99.96% Normal). No anomalous hyperreflective regions. GradCAM attribution diffuse — absence of pathological hotspot. Model entropy 0.002 bits — near-zero uncertainty.',ft:['Normal foveal architecture','Intact IS/OS junction','Smooth RPE layer','Normal ONL thickness','No fluid or deposits'],pt:[{i:'01',u:'routine',t:'Annual Review',d:'Standard examination annually. Earlier if symptoms.'},{i:'02',u:'routine',t:'Baseline Documentation',d:'Store OCT for future comparison.'},{i:'03',u:'routine',t:'Patient Education',d:'Report new distortion or central blur promptly.'},{i:'04',u:'routine',t:'Systemic',d:'HbA1c <7% if diabetic. BP <130/80 if hypertensive.'}],an:{NFL:'norm',GCL:'norm',IPL:'norm',INL:'norm',OPL:'norm',ONL:'norm',IS_OS:'norm',RPE:'norm',CHOROID:'norm'}} | |
| }; | |
| const ALAYERS=[{k:'NFL',n:'Nerve Fiber Layer',ab:'NFL',th:10},{k:'GCL',n:'Ganglion Cell Layer',ab:'GCL',th:8},{k:'IPL',n:'Inner Plexiform Layer',ab:'IPL',th:14},{k:'INL',n:'Inner Nuclear Layer',ab:'INL',th:12},{k:'OPL',n:'Outer Plexiform Layer',ab:'OPL',th:8},{k:'ONL',n:'Outer Nuclear Layer',ab:'ONL',th:18},{k:'IS_OS',n:'IS/OS Junction',ab:'IS/OS',th:5},{k:'RPE',n:'Retinal Pigment Epithelium',ab:'RPE',th:6},{k:'CHOROID',n:'Choroid',ab:'Cho',th:24}]; | |
| const ACOL={aff:'#FF5E5E',mild:'#FFB547',norm:'#52E58A'}; | |
| const CKA=[[1.00,.57,.40,.09,.08,.07],[.57,1.00,.50,.06,.05,.05],[.40,.50,1.00,.47,.44,.44],[.09,.06,.47,1.00,.99,.99],[.08,.05,.44,.99,1.00,1.00],[.07,.05,.44,.99,1.00,1.00]]; | |
| const CKAL=['Patch Embed','ViT Blk-3','ViT Blk-7','ViT Blk-11','Mamba Blk-1','Mamba Blk-2']; | |
| const ENT={CNV:[1.60,2.50,2.55,1.60,1.60,1.50],DME:[1.55,1.50,2.50,1.10,1.00,1.00],DRUSEN:[1.75,1.75,2.40,.45,.30,.35],NORMAL:[1.85,1.80,2.45,1.10,.65,.85],Mean:[1.69,1.89,2.48,1.06,.89,.93]}; | |
| const RANK={CNV:[5.0,12.5,12.2,5.0,5.0,4.5],DME:[5.0,4.5,12.1,3.0,2.7,2.7],DRUSEN:[6.0,5.8,12.0,.45,.35,.35],NORMAL:[6.1,6.0,11.5,3.0,1.9,2.3],Mean:[5.5,7.2,12.5,3.2,2.6,2.5]}; | |
| const ENRG={CNV:[.1,.1,2,82,8,15],DME:[.1,.1,1,105,65,15],DRUSEN:[.1,.1,2,103,10,15],NORMAL:[.1,.1,1,100,15,20],Mean:[.1,.1,1.5,97.5,24.5,16.3]}; | |
| const STAGES=['Patch Embed','ViT Blk-3','ViT Blk-7','ViT Blk-11','SS-Conv1','SS-Conv2']; | |
| // ═══ STATE ═══════════════════════════════════════════════════ | |
| let mode='r',sel=null,currKey=null,analyzing=false; | |
| let xaiM='gcam'; | |
| const HC={}; | |
| // ═══ HERO CANVAS ═════════════════════════════════════════════ | |
| (()=>{ | |
| const cv=document.getElementById('heroCanvas');if(!cv)return; | |
| const c=cv.getContext('2d');let t=0,W,H; | |
| function r(){W=cv.width=cv.offsetWidth;H=cv.height=cv.offsetHeight} | |
| r();window.addEventListener('resize',r); | |
| const LS=[{y:.30,h:.04,col:[0,201,184],a:.08},{y:.38,h:.03,col:[0,180,200],a:.06},{y:.45,h:.05,col:[100,120,200],a:.06},{y:.53,h:.04,col:[0,201,184],a:.07},{y:.62,h:.03,col:[0,150,180],a:.06},{y:.68,h:.07,col:[0,201,184],a:.08},{y:.80,h:.02,col:[180,255,248],a:.12},{y:.84,h:.03,col:[200,170,140],a:.09}]; | |
| function draw(){ | |
| c.clearRect(0,0,W,H); | |
| LS.forEach(l=>{ | |
| const y0=H*l.y,pts=[]; | |
| for(let x=0;x<=W;x+=3){const w=Math.sin(x*.007+t*.2+l.y*4)*3+Math.sin(x*.018+t*.1)*1.5;const n=(Math.sin(x*.05+l.y*11)*.45+Math.sin(x*.022+l.y*7)*.28)*H*l.h*.5;pts.push([x,y0+w+n]);} | |
| c.beginPath();pts.forEach(([px,py],i)=>i?c.lineTo(px,py):c.moveTo(px,py)); | |
| for(let i=pts.length-1;i>=0;i--)c.lineTo(pts[i][0],pts[i][1]+H*l.h); | |
| c.closePath();c.fillStyle=`rgba(${l.col.join(',')},${l.a})`;c.fill(); | |
| c.beginPath();pts.forEach(([px,py],i)=>i?c.lineTo(px,py):c.moveTo(px,py)); | |
| c.strokeStyle=`rgba(${l.col.join(',')},${l.a*2})`;c.lineWidth=.7;c.stroke(); | |
| }); | |
| const sx=(t*.35)%(W+120)-60; | |
| const g=c.createLinearGradient(sx-60,0,sx+60,0); | |
| g.addColorStop(0,'rgba(0,201,184,0)');g.addColorStop(.5,'rgba(0,201,184,.06)');g.addColorStop(1,'rgba(0,201,184,0)'); | |
| c.fillStyle=g;c.fillRect(sx-60,0,120,H); | |
| t+=.3;requestAnimationFrame(draw); | |
| } | |
| draw(); | |
| })(); | |
| // ═══ SAMPLE THUMBNAILS ════════════════════════════════════════ | |
| function drawOCT(cv,type){ | |
| const W=cv.width||200,H=cv.height||Math.round(W*.7); | |
| cv.width=W;cv.height=H; | |
| const c=cv.getContext('2d'); | |
| c.fillStyle='#0D1520';c.fillRect(0,0,W,H); | |
| const LS=[{r:.20,h:.038,b:145},{r:.27,h:.028,b:170},{r:.33,h:.048,b:132},{r:.39,h:.038,b:192},{r:.44,h:.028,b:122},{r:.50,h:.065,b:182},{r:.60,h:.018,b:238},{r:.64,h:.028,b:222},{r:.76,h:.095,b:90}]; | |
| LS.forEach((l,i)=>{ | |
| for(let x=0;x<W;x++){ | |
| const y0=Math.round(H*l.r),yh=Math.max(1,Math.round(H*l.h)); | |
| const n=Math.sin(x*.16+i*3.1)*7+Math.sin(x*.042+i*.8)*4; | |
| const v=Math.min(255,Math.max(0,l.b+n)); | |
| c.fillStyle=`rgb(${v},${v},${v})`;c.fillRect(x,y0+Math.round(n*.12),1,yh); | |
| } | |
| }); | |
| if(type==='CNV'){const g=c.createRadialGradient(W*.5,H*.68,0,W*.5,H*.68,W*.2);g.addColorStop(0,'rgba(255,190,150,.8)');g.addColorStop(1,'rgba(255,190,150,0)');c.fillStyle=g;c.beginPath();c.ellipse(W*.5,H*.68,W*.19,H*.09,0,0,Math.PI*2);c.fill();} | |
| else if(type==='DME'){[[.33,.57],[.52,.54],[.46,.62],[.62,.55]].forEach(([rx,ry])=>{const g=c.createRadialGradient(W*rx,H*ry,0,W*rx,H*ry,W*.065);g.addColorStop(0,'rgba(200,240,255,.9)');g.addColorStop(1,'rgba(200,240,255,0)');c.fillStyle=g;c.beginPath();c.ellipse(W*rx,H*ry,W*.062,H*.042,0,0,Math.PI*2);c.fill();});} | |
| else if(type==='DRUSEN'){[[.28,.63],[.41,.62],[.53,.63],[.64,.62],[.72,.63]].forEach(([rx,ry])=>{c.fillStyle='rgba(255,230,160,.85)';c.beginPath();c.ellipse(W*rx,H*ry,W*.032,H*.016,0,0,Math.PI*2);c.fill();});} | |
| } | |
| function _sampImgUrl(cls){ | |
| return (window._resolvedBase||window.location.origin)+'/sample-image/'+cls; | |
| } | |
| function initSamples(){ | |
| const g=document.getElementById('sampG');if(!g)return;g.innerHTML=''; | |
| CLS.forEach((t,i)=>{ | |
| const d=document.createElement('div');d.className='samp-i'; | |
| // Use real JPEG from server; fall back to synthetic canvas on error | |
| d.innerHTML=`<img class="samp-cv" id="sc${i}" src="${_sampImgUrl(t)}" alt="${t} OCT" loading="lazy"><div class="samp-cl">${t}</div>`; | |
| g.appendChild(d); | |
| const im=d.querySelector('img'); | |
| im.addEventListener('error',()=>{ | |
| // Server offline — replace with synthetic canvas | |
| const cv=document.createElement('canvas'); | |
| cv.className='samp-cv'+(im.classList.contains('sel')?' sel':''); | |
| cv.id='sc'+i;im.replaceWith(cv); | |
| requestAnimationFrame(()=>{cv.style.height='56px';drawOCT(cv,t);}); | |
| cv.addEventListener('click',()=>pickSamp(i)); | |
| }); | |
| im.addEventListener('click',()=>pickSamp(i)); | |
| }); | |
| } | |
| function pickSamp(i){ | |
| document.querySelectorAll('.samp-cv').forEach(c=>c.classList.remove('sel')); | |
| document.getElementById('sc'+i).classList.add('sel'); | |
| sel=i; | |
| uploadedFile=null;window._uploadedImgSrc=null; | |
| document.getElementById('aBtn').disabled=false; | |
| document.getElementById('inDot').className='sdot on'; | |
| document.getElementById('inTxt').textContent=CLS[i]+' selected'; | |
| document.getElementById('prevWrap').style.display='block'; | |
| const pv=document.getElementById('prevCv'); | |
| // Show real sample image in preview | |
| const ri=new Image(); | |
| ri.onload=()=>{ | |
| pv.width=ri.naturalWidth||224;pv.height=ri.naturalHeight||Math.round(pv.width*.7); | |
| pv.style.width='100%'; | |
| pv.getContext('2d').drawImage(ri,0,0,pv.width,pv.height); | |
| window._uploadedImgSrc=_sampImgUrl(CLS[i]); | |
| document.getElementById('prevMeta').textContent=`${ri.naturalWidth}\u00d7${ri.naturalHeight}px \u00b7 ${CLS[i]} \u00b7 Kermany OCT 2017`; | |
| }; | |
| ri.onerror=()=>{pv.style.width='100%';drawOCT(pv,CLS[i]);document.getElementById('prevMeta').textContent=`224\u00d7224 \u00b7 ${CLS[i]} \u00b7 Kermany OCT 2017`;}; | |
| ri.src=_sampImgUrl(CLS[i]); | |
| clrRes(); | |
| } | |
| function clrRes(){currKey=null;document.getElementById('stEmpty').style.display='grid';document.getElementById('stLoad').style.display='none';document.getElementById('rPanes').style.display='none';document.getElementById('dPanes').style.display='none';} | |
| // uploadedFile tracks whether the user dropped/picked a real file | |
| // null = user only picked a sample thumbnail, non-null = real upload | |
| let uploadedFile = null; | |
| document.getElementById('fileInp').addEventListener('change',function(){ | |
| if(!this.files.length)return; | |
| uploadedFile = this.files[0]; // store real file for POST /predict | |
| sel = null; // clear sample selection | |
| document.getElementById('aBtn').disabled=false; | |
| document.getElementById('inDot').className='sdot on'; | |
| document.getElementById('inTxt').textContent=uploadedFile.name; | |
| const r=new FileReader(); | |
| r.onload=e=>{ | |
| window._uploadedImgSrc=e.target.result; // store for all XAI canvases | |
| const img=new Image(); | |
| img.onload=()=>{ | |
| const pv=document.getElementById('prevCv'); | |
| pv.width=224;pv.height=Math.round(224*img.height/img.width); | |
| pv.style.width='100%'; | |
| pv.getContext('2d').drawImage(img,0,0,pv.width,pv.height); | |
| document.getElementById('prevWrap').style.display='block'; | |
| document.getElementById('prevMeta').textContent=`${img.width}\u00d7${img.height}px \u00b7 Your Upload`; | |
| }; | |
| img.src=e.target.result; | |
| }; | |
| r.readAsDataURL(uploadedFile); | |
| clrRes(); | |
| }); | |
| // ═══ MODE + TABS ══════════════════════════════════════════════ | |
| function setMode(m){ | |
| mode=m; | |
| const mR=document.getElementById('mR'),mD=document.getElementById('mD'); | |
| if(mR) mR.className='ms-opt'+(m==='r'?' on':''); | |
| if(mD) mD.className='ms-opt'+(m==='d'?' on':''); | |
| // Animate sliding pill to the active button | |
| const slider=document.getElementById('msSlider'); | |
| const activeBtn=m==='r'?mR:mD; | |
| if(slider&&activeBtn){ | |
| // Use rAF so layout is settled before reading offsets | |
| requestAnimationFrame(()=>{ | |
| slider.style.left=activeBtn.offsetLeft+'px'; | |
| slider.style.width=activeBtn.offsetWidth+'px'; | |
| }); | |
| } | |
| document.getElementById('modeHint').textContent=m==='r' | |
| ?'XAI Suite \u00b7 CKA Matrix \u00b7 Mamba Engine \u00b7 Layer Inspector \u00b7 Feature Space \u00b7 Performance' | |
| :'Explainable AI \u00b7 Retinal Anatomy \u00b7 Clinical Language \u00b7 ICD-10 \u00b7 Treatment Pathway'; | |
| document.getElementById('rTabBar').style.display=m==='r'?'flex':'none'; | |
| document.getElementById('dTabBar').style.display=m==='d'?'flex':'none'; | |
| document.getElementById('rPanes').style.display='none'; | |
| document.getElementById('dPanes').style.display='none'; | |
| document.getElementById('stEmpty').style.display='grid'; | |
| if(currKey)renderAll(); | |
| } | |
| // Re-position slider after fonts/layout load | |
| function _initModeSlider(){ | |
| const m=document.getElementById('mR'),d=document.getElementById('mD'),s=document.getElementById('msSlider'); | |
| if(!m||!s)return; | |
| // Init slider under the currently-active button | |
| const active=mode==='r'?m:d; | |
| s.style.left=active.offsetLeft+'px'; | |
| s.style.width=active.offsetWidth+'px'; | |
| s.style.transition='none'; // instant on first paint | |
| requestAnimationFrame(()=>{ s.style.transition=''; }); // restore transition | |
| } | |
| function initTabs(){ | |
| document.querySelectorAll('#rTabBar .rtab').forEach((btn,i)=>{ | |
| btn.addEventListener('click',()=>{ | |
| document.querySelectorAll('#rTabBar .rtab').forEach(b=>b.classList.remove('on'));btn.classList.add('on'); | |
| document.querySelectorAll('#rPanes .tab-pane').forEach((p,j)=>p.classList.toggle('on',i===j)); | |
| // Use rAF so the pane is visible (laid out) before canvas renders | |
| if(i===1&&currKey)requestAnimationFrame(()=>renderR1(currKey)); | |
| if(i===2&&currKey)requestAnimationFrame(()=>renderR2(currKey)); | |
| if(i===3&&currKey){ | |
| const ov=document.getElementById('rp3tco'); | |
| if(window._tabsComputing){ | |
| if(ov)ov.style.display='flex'; // still computing → show spinner | |
| } else { | |
| if(ov)ov.style.display='none'; | |
| requestAnimationFrame(()=>renderR3(currKey)); | |
| } | |
| } | |
| if(i===4&&currKey){ | |
| const ov=document.getElementById('rp4tco'); | |
| if(window._tabsComputing){ | |
| if(ov)ov.style.display='flex'; // still computing → show spinner | |
| } else { | |
| if(ov)ov.style.display='none'; | |
| requestAnimationFrame(()=>{renderR4(currKey,CLS.indexOf(currKey));}); | |
| } | |
| } | |
| if(i===5&&currKey)requestAnimationFrame(()=>renderR5()); | |
| }); | |
| }); | |
| document.querySelectorAll('#dTabBar .rtab').forEach((btn,i)=>{ | |
| btn.addEventListener('click',()=>{ | |
| document.querySelectorAll('#dTabBar .rtab').forEach(b=>b.classList.remove('on'));btn.classList.add('on'); | |
| document.querySelectorAll('#dPanes .tab-pane').forEach((p,j)=>p.classList.toggle('on',i===j)); | |
| // Re-render canvases at correct size once tab is visible | |
| if(currKey) requestAnimationFrame(()=>renderDoctor()); | |
| }); | |
| }); | |
| document.querySelectorAll('#rp1 .xtab').forEach(b=>{b.onclick=()=>{xaiM=b.dataset.xai;document.querySelectorAll('#rp1 .xtab').forEach(x=>x.classList.remove('on'));b.classList.add('on');if(currKey)renderR1(currKey);}}); | |
| } | |
| // ═══ INFERENCE ════════════════════════════════════════════════ | |
| const STEPS=[ | |
| 'Preprocessing OCT B-scan (224x224, ImageNet norm)', | |
| 'ViT-B/16 patch tokenization [196 x 768-dim tokens]', | |
| 'Transformer blocks 1-6 [frozen ImageNet features]', | |
| 'Transformer blocks 7-12 [fine-tuned OCT features]', | |
| 'SS-Conv-SSM Block 0 [Conv branch || SSM branch]', | |
| 'SS-Conv-SSM Block 1 [Conv branch || SSM branch]', | |
| 'Multi-scale pooling [CLS + Avg + Max + Attn]', | |
| 'MLP classifier [768 -> 512 -> 256 -> 4 classes]' | |
| ]; | |
| function _setNtProgress(pct, label){ | |
| const bar=document.getElementById('ntBarF'); | |
| const lbl=document.getElementById('ntStepLbl'); | |
| const pctEl=document.getElementById('ntPct'); | |
| if(bar) bar.style.width=pct+'%'; | |
| if(lbl) lbl.textContent=label||''; | |
| if(pctEl) pctEl.textContent=Math.round(pct)+'%'; | |
| } | |
| function runInf(){ | |
| if(analyzing||(sel===null&&!uploadedFile))return;analyzing=true; | |
| document.getElementById('aBtn').disabled=true;document.getElementById('stEmpty').style.display='none'; | |
| document.getElementById('stLoad').style.display='flex'; | |
| document.getElementById('rPanes').style.display='none';document.getElementById('dPanes').style.display='none'; | |
| document.getElementById('inDot').className='sdot go'; | |
| const isResearcher = mode==='r'; | |
| document.getElementById('lMsg').textContent=`$ retvim --mode ${isResearcher?'researcher':'doctor'} --device ${window._deviceStr||'cpu'}`; | |
| const devEl=document.getElementById('ntDevice');if(devEl)devEl.textContent=(window._deviceStr||'CPU').toUpperCase(); | |
| document.getElementById('lSteps').innerHTML=STEPS.map((s,i)=>`<div class="inf-st" id="is${i}"><div class="inf-dot"></div>${s}</div>`).join(''); | |
| _setNtProgress(2,'Initializing neural engine...'); | |
| let step=0; | |
| function adv(){ | |
| if(step>0){const prev=document.getElementById('is'+(step-1));if(prev)prev.className='inf-st done';} | |
| if(step<STEPS.length){ | |
| const el=document.getElementById('is'+step);if(el)el.className='inf-st cur'; | |
| const pct=5+(step/STEPS.length)*60; | |
| _setNtProgress(pct, STEPS[step].split('[')[0].trim()); | |
| step++;setTimeout(adv,145+Math.random()*105); | |
| } | |
| else if(API_LIVE){ | |
| _setNtProgress(70,'Uploading to live model...'); | |
| // Use resolved base (set by checkApiHealth) or fall back to API_BASE | |
| const BASE = window._resolvedBase || API_BASE; | |
| function _onApiSuccess(apiData, label){ | |
| analyzing=false; | |
| _setNtProgress(100,'Complete — results ready'); | |
| setTimeout(()=>{ | |
| document.getElementById('stLoad').style.display='none'; | |
| document.getElementById('aBtn').disabled=false; | |
| document.getElementById('inDot').className='sdot on'; | |
| document.getElementById('inTxt').textContent=label; | |
| },400); | |
| renderFromAPI(apiData); | |
| } | |
| function _onApiError(err){ | |
| analyzing=false; | |
| document.getElementById('stLoad').style.display='none'; | |
| document.getElementById('aBtn').disabled=false; | |
| document.getElementById('inDot').className='sdot'; | |
| document.getElementById('stEmpty').style.display='grid'; | |
| alert('Prediction failed: '+err.message+'. Check the backend server is running.'); | |
| } | |
| // ── Animated server-side progress (keeps bar moving while fetch is in flight) ── | |
| const SERVER_STEPS=[ | |
| [72, 'Running neural forward pass (ViT-B/16 + Mamba)...'], | |
| [75, 'GradCAM backpropagation → target class...'], | |
| [79, 'Attention rollout across 12 transformer layers...'], | |
| [82, 'Occlusion sensitivity map (36-patch grid)...'], | |
| [86, 'Integrated Gradients (15-step path integral)...'], | |
| [89, 'Extracting stage feature maps...'], | |
| [91, 'Rendering neural journey figure...'], | |
| [93, 'Deep analysis — attention heads + Mamba internals...'], | |
| ]; | |
| let _ssi=0,_ssTimer=null; | |
| function _advServerStep(){ | |
| if(_ssi<SERVER_STEPS.length){ | |
| const[pct,lbl]=SERVER_STEPS[_ssi++]; | |
| _setNtProgress(pct,lbl); | |
| // Stagger each step label: each real step ~3-6s on CPU | |
| _ssTimer=setTimeout(_advServerStep, 3200+Math.random()*1800); | |
| } | |
| } | |
| function _stopServerAnim(){if(_ssTimer){clearTimeout(_ssTimer);_ssTimer=null;}} | |
| if(uploadedFile){ | |
| // ══ REAL UPLOADED IMAGE — 2-PHASE ══ | |
| // Phase 1: fast doctor prediction (GradCAM only, ~2-3s) | |
| document.getElementById('lMsg').textContent=`$ retvim predict --file "${uploadedFile.name}" --mode doctor`; | |
| _advServerStep(); | |
| const _file=uploadedFile; | |
| const fd1=new FormData();fd1.append('file',_file);fd1.append('mode','doctor'); | |
| fetch(`${BASE}/predict`,{method:'POST',body:fd1}) | |
| .then(r=>{if(!r.ok)throw new Error('HTTP '+r.status);return r.json();}) | |
| .then(fastData=>{ | |
| _stopServerAnim(); | |
| const _pred=fastData.prediction; | |
| const _predStr=(typeof _pred==='string')?_pred:(_pred&&_pred.prediction?_pred.prediction:null); | |
| if(_predStr&&CLS.includes(_predStr))currKey=_predStr; | |
| _setNtProgress(85,'Overview ready — running deep XAI in background...'); | |
| // Show overview immediately | |
| setTimeout(()=>{ | |
| document.getElementById('stLoad').style.display='none'; | |
| document.getElementById('aBtn').disabled=false; | |
| document.getElementById('inDot').className='sdot on'; | |
| document.getElementById('inTxt').textContent='Live — '+_file.name; | |
| analyzing=false; | |
| renderFromAPI(fastData); | |
| // Mark XAI/Mamba/Layers tabs as loading | |
| _setTabsLoading(true); | |
| },300); | |
| // Phase 2: full researcher XAI (background) | |
| const fd2=new FormData();fd2.append('file',_file);fd2.append('mode','researcher'); | |
| fetch(`${BASE}/predict`,{method:'POST',body:fd2}) | |
| .then(r2=>{if(!r2.ok)throw new Error('HTTP '+r2.status);return r2.json();}) | |
| .then(fullData=>{ | |
| _setTabsLoading(false); | |
| // Hide in-tab computing overlays now that XAI is ready | |
| ['rp3tco','rp4tco','r1maptco'].forEach(id=>{const el=document.getElementById(id);if(el)el.style.display='none';}); | |
| // Merge XAI data into current state without re-rendering overview | |
| if(fullData.gcam_b64) window._apiGcam = fullData.gcam_b64; | |
| if(fullData.gcam2_b64) window._apiGcam2 = fullData.gcam2_b64; | |
| if(fullData.rollout_b64) window._apiRollout = fullData.rollout_b64; | |
| if(fullData.occ_b64) window._apiOcc = fullData.occ_b64; | |
| if(fullData.ig_b64) window._apiIg = fullData.ig_b64; | |
| if(fullData.rise_b64) window._apiRise = fullData.rise_b64; | |
| if(fullData.overlay_b64) window._apiOverlay = fullData.overlay_b64; | |
| if(fullData.deep_analysis) window._apiDeepAnalysis = fullData.deep_analysis; | |
| // If user is already on XAI/Mamba/Layers tab, re-render it now that data arrived | |
| const ai=[...document.querySelectorAll('#rPanes .tab-pane')].findIndex(p=>p.classList.contains('on')); | |
| if(ai===1&&currKey)requestAnimationFrame(()=>renderR1(currKey)); | |
| if(ai===3&&currKey)requestAnimationFrame(()=>renderR3(currKey)); | |
| if(ai===4&&currKey)requestAnimationFrame(()=>renderR4(currKey,CLS.indexOf(currKey))); | |
| // Update latency indicator | |
| document.getElementById('inTxt').textContent='Live \u2014 '+_file.name+' \u2713 XAI ready'; | |
| }) | |
| .catch(()=>{ | |
| _setTabsLoading(false); | |
| ['rp3tco','rp4tco','r1maptco'].forEach(id=>{const el=document.getElementById(id);if(el)el.style.display='none';}); | |
| }); | |
| }) | |
| .catch(e=>{_stopServerAnim();_onApiError(e);}); | |
| } else { | |
| // ══ SAMPLE IMAGE ══ | |
| document.getElementById('lMsg').textContent=`$ retvim predict --sample ${CLS[sel]} --mode researcher`; | |
| _advServerStep(); | |
| fetch(`${BASE}/sample/${CLS[sel]}`) | |
| .then(r=>{if(!r.ok)throw new Error('HTTP '+r.status);return r.json();}) | |
| .then(apiData=>{ | |
| _stopServerAnim(); | |
| const _sp=apiData.prediction; | |
| const _spStr=(typeof _sp==='string')?_sp:(_sp&&_sp.prediction?_sp.prediction:null); | |
| currKey=(_spStr&&CLS.includes(_spStr))?_spStr:CLS[sel]; | |
| _setNtProgress(97,'Rendering results...'); | |
| setTimeout(()=>_onApiSuccess(apiData,'Live — '+currKey),300); | |
| }) | |
| .catch(err=>{ | |
| _stopServerAnim(); | |
| console.warn('[RetViM] Live API unavailable, falling back:',err); | |
| finishSim(); | |
| }); | |
| } | |
| } else { | |
| setTimeout(finishSim,320); | |
| } | |
| } | |
| setTimeout(adv,60); | |
| } | |
| // ── Tab loading states (shown while background XAI fetch is running) ── | |
| function _setTabsLoading(on){ | |
| window._tabsComputing = on; | |
| // XAI(1), CKA(2), Mamba(3), Layers(4) tabs get loading indicator on button | |
| document.querySelectorAll('#rTabBar .rtab').forEach((btn,i)=>{ | |
| if(i>=1&&i<=4){ | |
| if(on)btn.classList.add('tab-computing'); | |
| else btn.classList.remove('tab-computing'); | |
| } | |
| }); | |
| // Set input image in all tab overlays | |
| const _imgSrc=window._uploadedImgSrc||''; | |
| ['rp3tco-img','rp4tco-img','r1maptco-img'].forEach(imgId=>{ | |
| const im=document.getElementById(imgId);if(im&&_imgSrc)im.src=_imgSrc; | |
| }); | |
| // Show in-tab overlay only on the currently visible tab (Mamba/Layers) | |
| ['rp3tco','rp4tco'].forEach(id=>{ | |
| const ov=document.getElementById(id);if(!ov)return; | |
| const pane=ov.closest('.tab-pane'); | |
| ov.style.display=(on&&pane&&pane.classList.contains('on'))?'flex':'none'; | |
| }); | |
| // r1maptco is controlled per-render by renderR1, not here | |
| } | |
| function finishSim(){ | |
| analyzing=false;currKey=CLS[sel]; | |
| document.getElementById('stLoad').style.display='none';document.getElementById('aBtn').disabled=false; | |
| document.getElementById('inDot').className='sdot on';document.getElementById('inTxt').textContent='Complete — '+currKey; | |
| renderAll(); | |
| } | |
| // Global helper: wrap raw base64 with data URL prefix (must be global so all render functions can use it) | |
| function _b64(s){return s&&!s.startsWith('data:')?'data:image/png;base64,'+s:s||'';} | |
| // ═══ RENDER FROM API ══════════════════════════════════════════ | |
| function renderFromAPI(data){ | |
| // Show results panels | |
| if(mode==='r'){ | |
| document.getElementById('rPanes').style.display='block';document.getElementById('dPanes').style.display='none'; | |
| document.querySelectorAll('#rTabBar .rtab').forEach((b,i)=>b.classList.toggle('on',i===0)); | |
| document.querySelectorAll('#rPanes .tab-pane').forEach((p,i)=>p.classList.toggle('on',i===0)); | |
| } else { | |
| document.getElementById('rPanes').style.display='none';document.getElementById('dPanes').style.display='block'; | |
| document.querySelectorAll('#dTabBar .rtab').forEach((b,i)=>b.classList.toggle('on',i===0)); | |
| document.querySelectorAll('#dPanes .tab-pane').forEach((p,i)=>p.classList.toggle('on',i===0)); | |
| } | |
| // --- Normalize API response (handles both flat and nested prediction formats) --- | |
| // full_analysis returns {prediction:{prediction:str,probabilities:{...},...}, ...} | |
| // doctor /predict returns {prediction:str, probabilities:{...}, ...} | |
| if(data.prediction && typeof data.prediction==='object' && data.prediction.prediction){ | |
| const _pi=data.prediction; | |
| data=Object.assign({},data,{ | |
| prediction: _pi.prediction, | |
| confidence: _pi.confidence, | |
| probabilities:_pi.probabilities, | |
| latency_ms: _pi.latency_ms | |
| }); | |
| } | |
| // Also update currKey from actual prediction in case it wasn't set yet | |
| if(data.prediction && typeof data.prediction==='string' && CLS.includes(data.prediction)){ | |
| currKey=data.prediction; | |
| } | |
| // --- Overview tab (rp0): use real probabilities --- | |
| const k=currKey,ki=CLS.indexOf(k); | |
| // Normalize probabilities: API returns dict {CNV:0.99,...} not array | |
| let rawProbs=data.probabilities; | |
| let probs; | |
| if(Array.isArray(rawProbs)&&rawProbs.length===4){ | |
| probs=rawProbs; | |
| }else if(rawProbs&&typeof rawProbs==='object'){ | |
| probs=CLS.map(c=>rawProbs[c]||0); | |
| }else{ | |
| probs=CLR[k].prob; | |
| } | |
| window._apiProbs = probs; // make available to renderR0 and renderResearcher | |
| const predConf = (probs[ki]*100).toFixed(2); | |
| document.getElementById('r0n').textContent=NAMES[k]; | |
| document.getElementById('r0p').textContent=predConf+'%'; | |
| document.getElementById('r0k').textContent='κ = '+CLR[k].k; | |
| const bDiv=document.getElementById('r0bars');if(bDiv){bDiv.innerHTML=''; | |
| probs.forEach((p,i)=>{const pct=(p*100).toFixed(2),top=i===ki; | |
| bDiv.innerHTML+=`<div class="cbar"><div class="cbar-n">${CLS[i]}</div><div class="cbar-bg"><div class="cbar-f" id="rb${i}" style="width:0%;background:${top?CLR[CLS[i]].hex:'rgba(255,255,255,.15)'}"></div></div><div class="cbar-v">${pct}%</div></div>`; | |
| }); | |
| setTimeout(()=>probs.forEach((_,i)=>{const e=document.getElementById('rb'+i);if(e)e.style.width=(probs[i]*100).toFixed(2)+'%';}),50); | |
| } | |
| // Store real latency for metrics table | |
| if(data.latency_ms) window._apiLatency = data.latency_ms; | |
| // Update donut + radar with real probs | |
| requestAnimationFrame(()=>renderR0(k,CLR[k],ki)); | |
| // --- XAI tab (rp1): draw real PyTorch XAI maps from API --- | |
| // Store API XAI maps globally so tab-switching also uses real data | |
| if(data.gcam_b64) window._apiGcam = data.gcam_b64; | |
| if(data.gcam2_b64) window._apiGcam2 = data.gcam2_b64; | |
| if(data.rollout_b64) window._apiRollout = data.rollout_b64; | |
| if(data.occ_b64) window._apiOcc = data.occ_b64; | |
| if(data.ig_b64) window._apiIg = data.ig_b64; | |
| if(data.rise_b64) window._apiRise = data.rise_b64; | |
| if(data.overlay_b64) window._apiOverlay = data.overlay_b64; | |
| if(data.rollout_overlay_b64) window._apiRolloutOverlay = data.rollout_overlay_b64; | |
| if(data.neural_journey_b64) window._apiJourney = data.neural_journey_b64; | |
| if(data.pca_b64) window._apiPca = data.pca_b64; | |
| if(data.class_comparison_b64)window._apiClassComparison= data.class_comparison_b64; | |
| if(data.trained_weights !== undefined) window._hasTrained = data.trained_weights; | |
| if(data.device) window._deviceStr = data.device; | |
| if(data.deep_analysis){ | |
| window._apiDeepAnalysis = data.deep_analysis; | |
| const da = data.deep_analysis; | |
| // --- Dynamically update Mamba block buttons --- | |
| const nBlocks = da.num_mamba_blocks || (da.mamba_internals ? da.mamba_internals.length : 2); | |
| const mambaBar = document.querySelector('#rp3 .card div[style*="gap"]'); | |
| if(mambaBar){ | |
| mambaBar.innerHTML = ''; | |
| for(let bi=0;bi<nBlocks;bi++){ | |
| const btn=document.createElement('button'); | |
| btn.className='xtab'+(bi===0?' on':''); | |
| btn.textContent='Block '+bi; | |
| btn.onclick=()=>setMambaBlock(bi); | |
| mambaBar.appendChild(btn); | |
| } | |
| } | |
| // --- Dynamically update Layers heading --- | |
| const nHeads = da.num_heads || (da.attention_heads && da.attention_heads[0] ? da.attention_heads[0].length : 12); | |
| const nLayers2 = da.attention_heads ? da.attention_heads.length : 12; | |
| const lyrhdr = document.querySelector('#rp4 .card .card-t'); | |
| if(lyrhdr) lyrhdr.textContent = `${nLayers2} layers x ${nHeads} heads - real attention patterns`; | |
| // Also update default grid columns | |
| const headsG = document.getElementById('headsG'); | |
| if(headsG) headsG.style.gridTemplateColumns = `repeat(${nHeads},1fr)`; | |
| // --- Pre-render Mamba & Layers in background (deferred so DOM is ready) --- | |
| const k2=currKey, ki2=CLS.indexOf(k2); | |
| setTimeout(()=>{ renderR3(k2); renderR4(k2, ki2); }, 200); | |
| } | |
| // --- Update static research section images with live API results --- | |
| if(data.pca_b64){ | |
| const pcaEl=document.getElementById('pcaDynImg'); | |
| if(pcaEl){pcaEl.src='data:image/png;base64,'+data.pca_b64;} | |
| const pcaBadge=document.getElementById('pcaLiveBadge'); | |
| if(pcaBadge)pcaBadge.style.display='inline'; | |
| } | |
| if(data.class_comparison_b64){ | |
| const ccEl=document.getElementById('classCompDynImg'); | |
| if(ccEl){ccEl.src='data:image/png;base64,'+data.class_comparison_b64;} | |
| const ccBadge=document.getElementById('classCompLiveBadge'); | |
| if(ccBadge)ccBadge.style.display='inline'; | |
| } | |
| if(data.neural_journey_b64){ | |
| // Update the "Real" image in the neural journey compare view | |
| const rtReal=document.getElementById('rtRealImg'); | |
| if(rtReal){rtReal.src='data:image/png;base64,'+data.neural_journey_b64;} | |
| // Update single-view if currently showing real mode | |
| if(typeof _rtMode!=='undefined'&&_rtMode==='real'){ | |
| const rtSingle=document.getElementById('rtSingleImg'); | |
| if(rtSingle){rtSingle.src='data:image/png;base64,'+data.neural_journey_b64;} | |
| } | |
| // Update the hero journey image | |
| const jReal=document.getElementById('jRealImg'); | |
| if(jReal){jReal.src='data:image/png;base64,'+data.neural_journey_b64;} | |
| } | |
| function _drawApiImg(canvasId, b64src, title){ | |
| const mc=document.getElementById(canvasId);if(!mc)return; | |
| const ctx=mc.getContext('2d');const W=mc.offsetWidth||400,H=Math.round(W*.78); | |
| mc.width=W;mc.height=H; | |
| const im=new Image();im.onload=()=>ctx.drawImage(im,0,0,W,H);im.src=_b64(b64src); | |
| const el=document.getElementById(canvasId+'title')||document.getElementById('r1maptitle'); | |
| if(el&&title)el.innerHTML=title+' <span class="live-model-badge show">● Real PyTorch</span>'; | |
| } | |
| const oc=gct('r1orig');if(oc)_drawRealOrSynth(oc.cv,k); | |
| const xaiSrc={gcam:window._apiGcam,gcam2:window._apiGcam2,rollout:window._apiRollout,occ:window._apiOcc,ig:window._apiIg,rise:window._apiRise}; | |
| const xaiTitles={gcam:'GradCAM heatmap',gcam2:'GradCAM++',rollout:'Attention Rollout',occ:'Occlusion Sensitivity',ig:'Integrated Gradients',rise:'RISE'}; | |
| const activeSrc=xaiSrc[xaiM]||window._apiGcam; | |
| if(activeSrc){ | |
| _drawApiImg('r1map', activeSrc, xaiTitles[xaiM]||'XAI heatmap'); | |
| } else { | |
| const mc=gct('r1map');if(mc){const hm=getHM(xaiM,k,mc.W,mc.H);const id=mc.c.createImageData(mc.W,mc.H);for(let i=0;i<mc.W*mc.H;i++){const[r,g,b]=jet(hm[i]);id.data[i*4]=r;id.data[i*4+1]=g;id.data[i*4+2]=b;id.data[i*4+3]=255;}mc.c.putImageData(id,0,0);} | |
| } | |
| // Also draw overlay & rollout overlay if canvases exist | |
| if(window._apiOverlay){_drawApiImg('r1overlay',''+window._apiOverlay,'GradCAM Overlay');} | |
| if(!window._apiOverlay){drawR1Overlay(k);} drawR1Top(k);drawR1Hist(k); | |
| // --- CKA tab (rp2): use real cka_matrix if provided --- | |
| if(data.cka_matrix && Array.isArray(data.cka_matrix)){window._apiCKA=data.cka_matrix;} | |
| renderR2(k); | |
| // --- Doctor mode: override probs with real data --- | |
| const realCLR = {...CLR[k], prob: probs}; | |
| if(mode==='d'){ | |
| const d=realCLR,c2=CLIN[k]; | |
| requestAnimationFrame(()=>{ | |
| document.getElementById('d0ban').className='dx-banner '+d.cls; | |
| const ic=document.getElementById('d0ico');ic.className='dx-ico '+d.cls;ic.textContent=d.em; | |
| document.getElementById('d0nm').textContent=NAMES[k]; | |
| document.getElementById('d0cf').textContent=predConf+'% confidence'; | |
| document.getElementById('d0ic').textContent=d.icd; | |
| document.getElementById('d0sl').textContent='Severity: '+d.sl; | |
| document.getElementById('d0sp').textContent=Math.round(d.sev*100)+'%'; | |
| const sb=document.getElementById('d0sf');sb.style.background=d.sc;sb.style.width='0%'; | |
| setTimeout(()=>{sb.style.width=(d.sev*100)+'%';},60); | |
| const bDiv2=document.getElementById('d0brs');if(bDiv2){bDiv2.innerHTML=''; | |
| probs.forEach((p,i)=>{const pct=(p*100).toFixed(2),top=i===ki; | |
| bDiv2.innerHTML+=`<div class="cbar"><div class="cbar-n">${CLS[i]}</div><div class="cbar-bg"><div class="cbar-f" id="db${i}" style="width:0%;background:${top?CLR[CLS[i]].hex:'rgba(255,255,255,.12)'}"></div></div><div class="cbar-v">${pct}%</div></div>`; | |
| }); | |
| setTimeout(()=>probs.forEach((_,i)=>{const e=document.getElementById('db'+i);if(e)e.style.width=(probs[i]*100).toFixed(2)+'%';}),60); | |
| } | |
| const dd=document.getElementById('d0dif');if(dd){dd.innerHTML='';CLS.filter((_,i)=>i!==ki).forEach((cl)=>{const idx=CLS.indexOf(cl);const p=(probs[idx]*100).toFixed(3);dd.innerHTML+=`<div class="diff-c"><div class="diff-n" style="color:${CLR[cl].hex}">${cl}</div><div class="diff-note">${parseFloat(p)<.1?'Low signal':'Minor features'}</div><div class="diff-p" style="color:${CLR[cl].hex}">${p}%</div></div>`;});} | |
| }); | |
| // Render Anatomy (d1), Reasoning (d2), Report (d3) — deferred so DOM settles | |
| setTimeout(()=>renderDoctor(), 150); | |
| } | |
| // Render remaining researcher tabs | |
| requestAnimationFrame(()=>{renderR5();const ai=[...document.querySelectorAll('#rPanes .tab-pane')].findIndex(p=>p.classList.contains('on'));if(ai===3)renderR3(k);if(ai===4)renderR4(k,ki);}); | |
| } | |
| // ═══ API HEALTH CHECK ════════════════════════════════════════ | |
| // Tries same-origin first (works when served by server.py), | |
| // then falls back to API_BASE (HF Space / remote). | |
| // Uses AbortController for broad browser compatibility | |
| // (AbortSignal.timeout() is not supported in Safari <16). | |
| let _healthRetryTimer = null; | |
| async function _fetchHealth(url){ | |
| const ctrl = new AbortController(); | |
| const t = setTimeout(()=>ctrl.abort(), 5000); | |
| try{ | |
| const r = await fetch(url, {signal: ctrl.signal}); | |
| clearTimeout(t); | |
| if(!r.ok) return null; | |
| // Must return valid JSON with status:'ok' — rules out static-site catch-alls | |
| try{ | |
| const j = await r.json(); | |
| if(j && j.status === 'ok' && j.model_loaded === true) return j; | |
| } catch(_){} | |
| return null; | |
| } catch(e){ | |
| clearTimeout(t); | |
| return null; | |
| } | |
| } | |
| async function checkApiHealth(){ | |
| const badge = document.getElementById('apiStatusBadge'); | |
| const txt = document.getElementById('apiStatusText'); | |
| if(!badge||!txt) return; | |
| // Candidates: same-origin server first, then configured API_BASE | |
| const origin = window.location.origin; | |
| // Exclude static-hosting origins (vercel.app, github.io, netlify.app, etc.) | |
| const isStaticHost = ['vercel.app','github.io','netlify.app','pages.dev'] | |
| .some(h => origin.includes(h)); | |
| const sameOrigin = (!isStaticHost && (origin.startsWith('http://') || origin.startsWith('https://'))) | |
| ? origin + '/health' : null; | |
| const candidates = []; | |
| if(sameOrigin) candidates.push(sameOrigin); | |
| if(API_BASE) candidates.push(API_BASE + '/health'); | |
| let live = false; | |
| let liveBase = null; | |
| let healthJson = null; | |
| for(const url of candidates){ | |
| try{ | |
| const res = await _fetchHealth(url); | |
| if(res){ | |
| live = true; | |
| liveBase = url.replace('/health',''); | |
| healthJson = res; // _fetchHealth now returns parsed JSON | |
| break; | |
| } | |
| } catch(_){} | |
| } | |
| if(live){ | |
| badge.className = 'api-status live'; | |
| txt.textContent = '\uD83D\uDFE2 Live Model'; | |
| badge.title = 'RetViM backend online \u2014 real PyTorch inference active (' + liveBase + ')'; | |
| // store resolved base so fetch calls use the right one | |
| if(liveBase && liveBase !== API_BASE) window._resolvedBase = liveBase; | |
| // extract device info for terminal display | |
| if(healthJson){ | |
| const dev = healthJson.device || 'cpu'; | |
| window._deviceStr = dev.toUpperCase().includes('CUDA') ? 'GPU/CUDA' | |
| : dev.toUpperCase().includes('MPS') ? 'Apple MPS' | |
| : 'CPU'; | |
| const devEl = document.getElementById('ntDevice'); | |
| if(devEl) devEl.textContent = window._deviceStr; | |
| } | |
| } else { | |
| badge.className = 'api-status demo'; | |
| txt.textContent = '\u26AA Demo Mode'; | |
| badge.title = 'Backend unreachable \u2014 running canvas simulation'; | |
| window._resolvedBase = null; | |
| } | |
| // Keep checking every 20 s so indicator stays current | |
| clearTimeout(_healthRetryTimer); | |
| _healthRetryTimer = setTimeout(checkApiHealth, 20000); | |
| } | |
| function renderAll(){ | |
| if(mode==='r'){ | |
| document.getElementById('rPanes').style.display='block';document.getElementById('dPanes').style.display='none'; | |
| document.querySelectorAll('#rTabBar .rtab').forEach((b,i)=>b.classList.toggle('on',i===0)); | |
| document.querySelectorAll('#rPanes .tab-pane').forEach((p,i)=>p.classList.toggle('on',i===0)); | |
| renderResearcher(); | |
| } else { | |
| document.getElementById('rPanes').style.display='none';document.getElementById('dPanes').style.display='block'; | |
| document.querySelectorAll('#dTabBar .rtab').forEach((b,i)=>b.classList.toggle('on',i===0)); | |
| document.querySelectorAll('#dPanes .tab-pane').forEach((p,i)=>p.classList.toggle('on',i===0)); | |
| renderDoctor(); | |
| } | |
| } | |
| // ═══ CANVAS HELPERS ═══════════════════════════════════════════ | |
| function gct(id,w,h){const cv=document.getElementById(id);if(!cv)return null;cv.width=w||(cv.offsetWidth||cv.parentElement?.offsetWidth||200);cv.height=h||(cv.offsetHeight||140);return{c:cv.getContext('2d'),W:cv.width,H:cv.height,cv};} | |
| function jet(v){const t=Math.max(0,Math.min(1,v));let r,g,b;if(t<.25){r=0;g=t*4;b=1;}else if(t<.5){r=0;g=1;b=1-(t-.25)*4;}else if(t<.75){r=(t-.5)*4;g=1;b=0;}else{r=1;g=1-(t-.75)*4;b=0;}return[r*255,g*255,b*255];} | |
| function seeded(s){let x=s;return()=>{x=(x*16807)%2147483647;return(x-1)/2147483646;};} | |
| function getHM(method,key,W,H){ | |
| const ck=`${method}_${key}_${W}x${H}`;if(HC[ck])return HC[ck]; | |
| const data=new Float32Array(W*H);const r=seeded(CLS.indexOf(key)*131+method.length*17); | |
| if(key==='CNV'){for(let y=0;y<H;y++)for(let x=0;x<W;x++){const dx=(x-W*.5)/(W*.3),dy=(y-H*.69)/(H*.18);data[y*W+x]=Math.exp(-(dx*dx+dy*dy)*2.8)+r()*.08;}} | |
| else if(key==='DME'){[[.33,.57],[.52,.54],[.46,.62],[.62,.55]].forEach(([fx,fy])=>{for(let y=0;y<H;y++)for(let x=0;x<W;x++){const dx=(x-W*fx)/(W*.08),dy=(y-H*fy)/(H*.055);data[y*W+x]+=Math.exp(-(dx*dx+dy*dy)*1.8);}});} | |
| else if(key==='DRUSEN'){[[.28,.63],[.42,.62],[.54,.63],[.65,.62],[.72,.63]].forEach(([fx,fy])=>{for(let y=0;y<H;y++)for(let x=0;x<W;x++){const dx=(x-W*fx)/(W*.045),dy=(y-H*fy)/(H*.03);data[y*W+x]+=Math.exp(-(dx*dx+dy*dy)*3.2);}});} | |
| else{for(let i=0;i<W*H;i++)data[i]=r()*.3;} | |
| if(method==='occ')for(let i=0;i<W*H;i++)data[i]*=(.35+r()*.65); | |
| if(method==='ig')for(let i=0;i<W*H;i++){const rx=(i%W)/W;data[i]*=(.5+Math.sin(rx*Math.PI)*.5);} | |
| const mx=Math.max(...data),mn=Math.min(...data); | |
| for(let i=0;i<data.length;i++)data[i]=(data[i]-mn)/(mx-mn+1e-8); | |
| HC[ck]=data;return data; | |
| } | |
| function linechart(id,ds,opts){ | |
| const o=opts||{};const r=gct(id,null,o.H||140);if(!r)return; | |
| const{c,W,H}=r;c.clearRect(0,0,W,H); | |
| const pd=o.pd||8;const pw=W-pd*2,ph=H-pd*2; | |
| const allY=ds.flatMap(d=>d.data);const mn=o.mn!==undefined?o.mn:Math.min(...allY);const mx=o.mx!==undefined?o.mx:Math.max(...allY);const range=mx-mn||1; | |
| c.strokeStyle='rgba(255,255,255,.06)';c.lineWidth=.5; | |
| for(let i=0;i<=4;i++){const y=pd+i*ph/4;c.beginPath();c.moveTo(pd,y);c.lineTo(W-pd,y);c.stroke();} | |
| if(o.div){const bx=pd+(3/5)*pw;c.fillStyle='rgba(255,181,71,.04)';c.fillRect(bx,pd,W-pd-bx,ph);c.strokeStyle='rgba(255,181,71,.25)';c.lineWidth=.8;c.setLineDash([3,2]);c.beginPath();c.moveTo(bx,pd);c.lineTo(bx,pd+ph);c.stroke();c.setLineDash([]);} | |
| ds.forEach(d=>{c.beginPath();d.data.forEach((v,i)=>{const x=pd+i*pw/(d.data.length-1);const y=pd+ph-(v-mn)/range*ph*.95;i?c.lineTo(x,y):c.moveTo(x,y);});c.strokeStyle=d.col;c.lineWidth=d.w||1.4;if(d.dash)c.setLineDash(d.dash);c.stroke();c.setLineDash([]);}); | |
| } | |
| // ═══ RESEARCHER ═══════════════════════════════════════════════ | |
| function renderResearcher(){const k=currKey,d=CLR[k],ki=CLS.indexOf(k);requestAnimationFrame(()=>{renderR0(k,d,ki);renderR1(k);renderR2(k);renderR5();const ai=[...document.querySelectorAll('#rPanes .tab-pane')].findIndex(p=>p.classList.contains('on'));if(ai===3)renderR3(k);if(ai===4)renderR4(k,ki);});} | |
| function renderR0(k,d,ki){ | |
| // Use real API probs if available, else fall back to demo probs | |
| const liveProbs = window._apiProbs && window._apiProbs.length===4 ? window._apiProbs : d.prob; | |
| const liveConf = (liveProbs[ki]*100).toFixed(2); | |
| document.getElementById('r0n').textContent=NAMES[k]; | |
| document.getElementById('r0p').textContent=liveConf+'%'; | |
| document.getElementById('r0k').textContent='κ = '+d.k; | |
| const bDiv=document.getElementById('r0bars');bDiv.innerHTML=''; | |
| liveProbs.forEach((p,i)=>{const pct=(p*100).toFixed(2),top=i===ki;bDiv.innerHTML+=`<div class="cbar"><div class="cbar-n">${CLS[i]}</div><div class="cbar-bg"><div class="cbar-f" id="rb${i}" style="width:0%;background:${top?CLR[CLS[i]].hex:'rgba(255,255,255,.15)'}"></div></div><div class="cbar-v">${pct}%</div></div>`;}); | |
| setTimeout(()=>liveProbs.forEach((_,i)=>{const e=document.getElementById('rb'+i);if(e)e.style.width=(liveProbs[i]*100).toFixed(2)+'%';}),50); | |
| // Donut — real probabilities | |
| const r=gct('r0donut',120,120);if(r){const{c,W,H}=r;c.clearRect(0,0,W,H);const cx=W/2,cy=H/2,rad=50,ir=32;let st=-Math.PI/2;liveProbs.forEach((p,i)=>{const en=st+p*Math.PI*2;c.beginPath();c.moveTo(cx,cy);c.arc(cx,cy,rad,st,en);c.fillStyle=CLR[CLS[i]].hex;c.fill();c.beginPath();c.arc(cx,cy,ir,0,Math.PI*2);c.fillStyle='#111820';c.fill();st=en;});c.font='bold 11px Space Mono,monospace';c.fillStyle='#fff';c.textAlign='center';c.fillText(liveConf+'%',cx,cy+4);} | |
| // Radar — dual ring: per-class metrics + overall | |
| const _rdrEl=document.getElementById('r0radar'); | |
| const _rdrW=Math.max(_rdrEl?.getBoundingClientRect().width||0,_rdrEl?.offsetWidth||0,280); | |
| const rdr=gct('r0radar',_rdrW,180);if(rdr){ | |
| const{c,W,H}=rdr;c.clearRect(0,0,W,H); | |
| const lab=['Acc','Prec','Rec','Spec','F1','AUC'],n=6,cx=W/2,cy=H/2+4,rad=Math.min(W*0.36,H*0.38); | |
| // Use live metrics per class | |
| const _lp=window._apiProbs&&window._apiProbs.length===4?window._apiProbs:CLR[k].prob; | |
| const _ck=CLS.indexOf(k); | |
| const metPerClass=_lp.map((p,i)=>{const acc=i===_ck?p:.3+p*.6;return[acc,p+.02,p+.01,p+.04,p+.01,p+.03].map(v=>Math.min(1,Math.max(0.1,v)));}); | |
| const metOverall=[.999,.999,.999,.9997,.999,1.0]; | |
| // Background rings | |
| [.25,.5,.75,1].forEach(f=>{c.beginPath();for(let i=0;i<n;i++){const a=-Math.PI/2+i*2*Math.PI/n,x=cx+Math.cos(a)*rad*f,y=cy+Math.sin(a)*rad*f;i?c.lineTo(x,y):c.moveTo(x,y);}c.closePath();c.strokeStyle=f===1?'rgba(255,255,255,.1)':'rgba(255,255,255,.05)';c.lineWidth=f===1?1:.5;c.stroke();}); | |
| // Axis lines | |
| for(let i=0;i<n;i++){const a=-Math.PI/2+i*2*Math.PI/n;c.beginPath();c.moveTo(cx,cy);c.lineTo(cx+Math.cos(a)*rad,cy+Math.sin(a)*rad);c.strokeStyle='rgba(255,255,255,.07)';c.lineWidth=.7;c.stroke();} | |
| // Per-class polygon | |
| const classCol=CLR[k].hex; | |
| c.beginPath();metPerClass[_ck].forEach((v,i)=>{const a=-Math.PI/2+i*2*Math.PI/n,x=cx+Math.cos(a)*rad*v,y=cy+Math.sin(a)*rad*v;i?c.lineTo(x,y):c.moveTo(x,y);}); | |
| c.closePath();c.fillStyle=classCol.replace(')',',0.12)').replace('#','').length>8?'rgba(0,201,184,.1)':classCol+'1a';c.fill(); | |
| c.strokeStyle=classCol;c.lineWidth=1.5;c.stroke(); | |
| // Overall polygon (teal) | |
| c.beginPath();metOverall.forEach((v,i)=>{const a=-Math.PI/2+i*2*Math.PI/n,x=cx+Math.cos(a)*rad*v,y=cy+Math.sin(a)*rad*v;i?c.lineTo(x,y):c.moveTo(x,y);}); | |
| c.closePath();c.fillStyle='rgba(0,201,184,.08)';c.fill();c.strokeStyle='rgba(0,201,184,.5)';c.lineWidth=1;c.setLineDash([3,2]);c.stroke();c.setLineDash([]); | |
| // Labels | |
| lab.forEach((l,i)=>{const a=-Math.PI/2+i*2*Math.PI/n,lx=cx+Math.cos(a)*(rad+18),ly=cy+Math.sin(a)*(rad+18)+3;c.font='bold 9px Space Grotesk,sans-serif';c.fillStyle='rgba(255,255,255,.55)';c.textAlign=lx<cx-5?'right':lx>cx+5?'left':'center';c.fillText(l,lx,ly);}); | |
| // Legend | |
| c.font='8px Space Grotesk,sans-serif';c.textAlign='left'; | |
| c.fillStyle=classCol;c.fillText('● '+k,8,H-14); | |
| c.fillStyle='rgba(0,201,184,.7)';c.fillText('― Overall',8,H-4); | |
| } | |
| // Threshold Curve — sensitivity vs specificity as threshold moves | |
| const tc=document.getElementById('r0thresh'); | |
| if(tc){ | |
| const W=tc.width=tc.offsetWidth||300,H=tc.height=140,c=tc.getContext('2d'); | |
| c.clearRect(0,0,W,H); | |
| const conf=liveProbs[ki],pd=16,pw=W-pd*2,ph=H-pd*2; | |
| c.strokeStyle='rgba(255,255,255,.06)';c.lineWidth=.5; | |
| for(let i=0;i<=4;i++){const y=pd+i*ph/4;c.beginPath();c.moveTo(pd,y);c.lineTo(W-pd,y);c.stroke();} | |
| // Sensitivity (teal) — falls as threshold rises | |
| c.beginPath(); | |
| for(let i=0;i<=100;i++){const t=i/100;const v=1/(1+Math.exp((t-conf*.9)*16));c.lineTo(pd+t*pw,pd+ph-v*ph*.92);} | |
| c.strokeStyle='#00C9B8';c.lineWidth=1.8;c.stroke(); | |
| // Specificity (orange) — rises as threshold rises | |
| c.beginPath(); | |
| for(let i=0;i<=100;i++){const t=i/100;const v=1/(1+Math.exp(-(t-conf*.9)*13));c.lineTo(pd+t*pw,pd+ph-v*ph*.92);} | |
| c.strokeStyle='#FFB547';c.lineWidth=1.8;c.stroke(); | |
| // Vertical line at current confidence | |
| const xc=Math.round(pd+conf*pw); | |
| c.strokeStyle='rgba(255,255,255,.22)';c.lineWidth=1;c.setLineDash([3,2]); | |
| c.beginPath();c.moveTo(xc,pd);c.lineTo(xc,pd+ph);c.stroke();c.setLineDash([]); | |
| // Labels | |
| c.font='8px Space Grotesk,sans-serif'; | |
| c.fillStyle='rgba(0,201,184,.8)';c.textAlign='left';c.fillText('Sensitivity',pd+3,pd+11); | |
| c.fillStyle='rgba(255,181,71,.8)';c.fillText('Specificity',pd+3,pd+22); | |
| c.fillStyle='rgba(255,255,255,.35)';c.fillText('\u03c4='+conf.toFixed(2),xc+3,pd+10); | |
| } | |
| // Metrics | |
| const _infMs=window._apiLatency||'—'; | |
| const mt=document.getElementById('r0mets');if(mt)mt.innerHTML=[['dataset','Kermany OCT 2017'],['test_images','968 (balanced, 242/class)'],['val_accuracy','96.68 %'],['precision_macro','96.71 %'],['recall_macro','96.49 %'],['specificity','98.87 %'],['f1_macro','96.59 %'],['auc_roc_macro','0.9926'],['cohen_kappa','0.9556'],['pred_class',k],['pred_prob',(d.prob[ki]*100).toFixed(4)+'%'],['inference_ms',_infMs+'ms'],['total_params','101.2M'],['trainable_params','58.1M'],['epochs','20 (best @ 19)'],['precision_fp','BF16'],['vit_backbone','ViT-B/16 ImageNet-21k'],['frozen_layers','1–6 (ImageNet)']].map(([k2,v])=>`<div class="mrow"><span class="mk">${k2}</span><span class="mv${['val_accuracy','auc_roc_macro','cohen_kappa','f1_macro'].includes(k2)?' hi':''}">${v}</span></div>`).join(''); | |
| } | |
| function _drawRealOrSynth(cv,k){ | |
| // Draw real uploaded image if available, else synthetic OCT | |
| if(window._uploadedImgSrc){ | |
| const W=cv.width||200,H=cv.height||Math.round(W*.78); | |
| cv.width=W;cv.height=H; | |
| const img=new Image(); | |
| img.onload=()=>cv.getContext('2d').drawImage(img,0,0,W,H); | |
| img.src=window._uploadedImgSrc; | |
| } else { | |
| drawOCT(cv,k); | |
| } | |
| } | |
| function renderR1(k){ | |
| const oc=gct('r1orig');if(oc){_drawRealOrSynth(oc.cv,k);} | |
| const titles={gcam:'GradCAM',gcam2:'GradCAM++',rollout:'Attn Rollout',occ:'Occlusion Sensitivity',ig:'Integr. Gradients',rise:'RISE'}; | |
| const apiSrcMap={gcam:window._apiGcam,gcam2:window._apiGcam2,rollout:window._apiRollout,occ:window._apiOcc,ig:window._apiIg,rise:window._apiRise}; | |
| const apiSrc=apiSrcMap[xaiM]; | |
| const mc=document.getElementById('r1map'); | |
| const r1tco=document.getElementById('r1maptco'); | |
| const r1tcoImg=document.getElementById('r1maptco-img'); | |
| const r1tcoLbl=document.getElementById('r1maptco-lbl'); | |
| if(mc&&apiSrc){ | |
| // Real PyTorch result available — draw it, hide overlay | |
| if(r1tco)r1tco.style.display='none'; | |
| const ctx=mc.getContext('2d');const W=mc.offsetWidth||400,H=Math.round(W*.78); | |
| mc.width=W;mc.height=H; | |
| const im=new Image();im.onload=()=>ctx.drawImage(im,0,0,W,H);im.src=_b64(apiSrc); | |
| const el=document.getElementById('r1maptitle'); | |
| if(el)el.innerHTML=titles[xaiM]+' <span class="live-model-badge show">● Real PyTorch</span>'; | |
| } else if(window._tabsComputing){ | |
| // Still computing — show overlay with input image background | |
| if(r1tcoImg&&window._uploadedImgSrc)r1tcoImg.src=window._uploadedImgSrc; | |
| if(r1tcoLbl)r1tcoLbl.textContent='Computing '+titles[xaiM]; | |
| if(r1tco)r1tco.style.display='flex'; | |
| } else { | |
| // Not computing, no API result — canvas simulation fallback, hide overlay | |
| if(r1tco)r1tco.style.display='none'; | |
| const mc2=gct('r1map');if(mc2){const hm=getHM(xaiM,k,mc2.W,mc2.H);const id=mc2.c.createImageData(mc2.W,mc2.H);for(let i=0;i<mc2.W*mc2.H;i++){const[r,g,b]=jet(hm[i]);id.data[i*4]=r;id.data[i*4+1]=g;id.data[i*4+2]=b;id.data[i*4+3]=255;}mc2.c.putImageData(id,0,0);} | |
| const el=document.getElementById('r1maptitle');if(el)el.textContent=titles[xaiM]+' heatmap'; | |
| } | |
| // Draw overlay using real GradCAM overlay if available | |
| if(window._apiOverlay){ | |
| const ov=document.getElementById('r1overlay'); | |
| if(ov){const ctx=ov.getContext('2d');const W=ov.offsetWidth||400,H=Math.round(W*.78); | |
| ov.width=W;ov.height=H; | |
| const im=new Image();im.onload=()=>ctx.drawImage(im,0,0,W,H);im.src=_b64(window._apiOverlay);} | |
| } else {drawR1Overlay(k);} | |
| drawR1Top(k);drawR1Hist(k); | |
| } | |
| function drawR1Overlay(k){ | |
| const K=k||currKey;if(!K)return; | |
| const r=gct('r1overlay');if(!r)return; | |
| const{c,W,H,cv}=r; | |
| const alpha=parseInt(document.getElementById('r1op').value)/100; | |
| document.getElementById('r1oplbl').textContent=Math.round(alpha*100)+'%'; | |
| // Use real API overlay if available, else compose synthetic | |
| if(window._apiOverlay){ | |
| const im=new Image();im.onload=()=>c.drawImage(im,0,0,W,H);im.src=_b64(window._apiOverlay);return; | |
| } | |
| _drawRealOrSynth(cv,K); | |
| const hm=getHM(xaiM,K,W,H); | |
| const id=c.createImageData(W,H); | |
| for(let i=0;i<W*H;i++){const[r2,g,b]=jet(hm[i]);id.data[i*4]=r2;id.data[i*4+1]=g;id.data[i*4+2]=b;id.data[i*4+3]=Math.round(hm[i]*alpha*255);} | |
| const tmp=document.createElement('canvas');tmp.width=W;tmp.height=H;tmp.getContext('2d').putImageData(id,0,0);c.drawImage(tmp,0,0); | |
| } | |
| function onOp(){ | |
| if(window._apiOverlay){drawR1Overlay(currKey);}else{drawR1Overlay(currKey);} | |
| } | |
| function drawR1Top(k){ | |
| const r=gct('r1toppx');if(!r)return;const{c,W,H,cv}=r; | |
| const apiSrc={gcam:window._apiGcam,gcam2:window._apiGcam2,rollout:window._apiRollout,occ:window._apiOcc,ig:window._apiIg,rise:window._apiRise}[xaiM]||window._apiGcam; | |
| function _applyTopMask(hm){ | |
| const s=[...hm].sort((a,b)=>b-a);const thr=s[Math.floor(W*H*.1)]; | |
| const id=c.createImageData(W,H); | |
| for(let i=0;i<W*H;i++){if(hm[i]>=thr){id.data[i*4]=220;id.data[i*4+1]=30;id.data[i*4+2]=30;id.data[i*4+3]=180;}else if(hm[i]<.08){id.data[i*4]=30;id.data[i*4+1]=60;id.data[i*4+2]=200;id.data[i*4+3]=80;}} | |
| const tmp=document.createElement('canvas');tmp.width=W;tmp.height=H;tmp.getContext('2d').putImageData(id,0,0);c.drawImage(tmp,0,0); | |
| } | |
| // Draw real uploaded image as base | |
| _drawRealOrSynth(cv,k); | |
| if(apiSrc){ | |
| // Sample GradCAM to derive top-activation mask | |
| const tmp2=document.createElement('canvas');tmp2.width=W;tmp2.height=H; | |
| const tc=tmp2.getContext('2d'); | |
| const him=new Image(); | |
| him.onload=()=>{tc.drawImage(him,0,0,W,H);const px=tc.getImageData(0,0,W,H).data;const hm=new Float32Array(W*H);for(let i=0;i<W*H;i++)hm[i]=px[i*4]/255;_applyTopMask(hm);}; | |
| him.src=_b64(apiSrc); | |
| } else { | |
| const hm=getHM(xaiM,k,W,H);_applyTopMask(hm); | |
| } | |
| } | |
| function drawR1Hist(k){ | |
| const r=gct('r1hist',null,110);if(!r)return;const{c,W,H}=r;c.clearRect(0,0,W,H); | |
| const apiSrc={gcam:window._apiGcam,gcam2:window._apiGcam2,rollout:window._apiRollout,occ:window._apiOcc,ig:window._apiIg,rise:window._apiRise}[xaiM]||window._apiGcam; | |
| function _drawHist(hm){const bins=new Array(20).fill(0);hm.forEach(v=>bins[Math.min(19,Math.floor(v*20))]++);const mx=Math.max(...bins)||1;const pd=4,bw=(W-pd*2)/20;bins.forEach((b,i)=>{const h2=(b/mx)*(H-pd*2);const[r2,g,bl]=jet(i/19);c.fillStyle=`rgb(${r2},${g},${bl})`;c.fillRect(pd+i*bw,H-pd-h2,bw-1,h2);});} | |
| if(apiSrc){const tmp=document.createElement('canvas');tmp.width=50;tmp.height=40;const tc=tmp.getContext('2d');const im=new Image();im.onload=()=>{tc.drawImage(im,0,0,50,40);const px=tc.getImageData(0,0,50,40).data;const hm=new Float32Array(2000);for(let i=0;i<2000;i++)hm[i]=px[i*4]/255;_drawHist(hm);};im.src=_b64(apiSrc);} | |
| else{_drawHist(getHM(xaiM,k,50,40));} | |
| } | |
| function renderR2(k){ | |
| const dpr=window.devicePixelRatio||1; | |
| function hqCtx(id,cssH){ | |
| const cv=document.getElementById(id);if(!cv)return null; | |
| const W=cv.offsetWidth||cv.parentElement?.offsetWidth||400,H=cssH||180; | |
| cv.width=Math.round(W*dpr);cv.height=Math.round(H*dpr); | |
| cv.style.width=W+'px';cv.style.height=H+'px'; | |
| const c=cv.getContext('2d');c.scale(dpr,dpr);c.clearRect(0,0,W,H); | |
| return {c,W,H,cv}; | |
| } | |
| // ── CKA Similarity Matrix ──────────────────────────────────── | |
| const ckaCv=document.getElementById('r2cka');if(ckaCv){ | |
| const cssW=ckaCv.offsetWidth||ckaCv.parentElement?.offsetWidth||560; | |
| const n=6,lw=110,rb=52; // label width left, rotation buffer bottom | |
| const cellSz=Math.floor((cssW-lw-10)/n); | |
| const cssH=lw+n*cellSz+rb; | |
| ckaCv.width=Math.round(cssW*dpr);ckaCv.height=Math.round(cssH*dpr); | |
| ckaCv.style.width=cssW+'px';ckaCv.style.height=cssH+'px'; | |
| const c=ckaCv.getContext('2d');c.scale(dpr,dpr);c.clearRect(0,0,cssW,cssH); | |
| const ox=lw,oy=8; // matrix origin | |
| function ckacol(v){ | |
| // Perceptually balanced: dark blue → white → teal | |
| if(v<.5){const t=v/.5; | |
| return`rgb(${Math.round(18+t*220)},${Math.round(60+t*188)},${Math.round(200+t*50)})`;} | |
| const t=(v-.5)/.5; | |
| return`rgb(${Math.round(238-t*238)},${Math.round(248-t*50)},${Math.round(250-t*82)})`; | |
| } | |
| // Row labels (y-axis) | |
| CKAL.forEach((l,r)=>{ | |
| c.font='10px Space Grotesk,sans-serif';c.fillStyle='rgba(255,255,255,.6)';c.textAlign='right'; | |
| c.fillText(l,ox-6,oy+r*cellSz+cellSz/2+4); | |
| }); | |
| // Column labels (x-axis, rotated) | |
| CKAL.forEach((l,col)=>{ | |
| c.save();c.translate(ox+col*cellSz+cellSz/2,oy+n*cellSz+6);c.rotate(-Math.PI/4); | |
| c.font='10px Space Grotesk,sans-serif';c.fillStyle='rgba(255,255,255,.55)';c.textAlign='right'; | |
| c.fillText(l,0,0);c.restore(); | |
| }); | |
| // Section divider lines | |
| const bx=ox+4*cellSz,by=oy+4*cellSz; | |
| c.strokeStyle='rgba(255,255,255,.5)';c.lineWidth=1.5;c.setLineDash([5,4]); | |
| c.beginPath();c.moveTo(bx,oy);c.lineTo(bx,oy+n*cellSz);c.stroke(); | |
| c.beginPath();c.moveTo(ox,by);c.lineTo(ox+n*cellSz,by);c.stroke(); | |
| c.setLineDash([]); | |
| // Section annotations | |
| c.font='9px Space Mono,monospace';c.fillStyle='rgba(255,255,255,.35)';c.textAlign='center'; | |
| c.fillText('ViT backbone',ox+2*cellSz,oy-2); | |
| c.fillText('Mamba blocks',ox+(4+1)*cellSz,oy-2); | |
| // Cells | |
| const ckaSrc=window._apiCKA||CKA; | |
| ckaSrc.forEach((row,r)=>row.forEach((v,col)=>{ | |
| const x=ox+col*cellSz,y=oy+r*cellSz; | |
| const isD=r===col; | |
| c.fillStyle=ckacol(v);c.fillRect(x+1,y+1,cellSz-2,cellSz-2); | |
| if(isD){c.strokeStyle='rgba(255,255,255,.7)';c.lineWidth=1.5;c.strokeRect(x+1,y+1,cellSz-2,cellSz-2);} | |
| c.font=(cellSz>44?'bold 10px':'bold 9px')+' Space Mono,monospace';c.textAlign='center'; | |
| const bright=v>.55||v<.2; | |
| c.fillStyle=bright?'rgba(5,10,18,.9)':'rgba(255,255,255,.9)'; | |
| c.fillText(v.toFixed(2),x+cellSz/2,y+cellSz/2+4); | |
| })); | |
| // Colorbar | |
| const cbX=ox+n*cellSz+14,cbY=oy,cbW=12,cbH=n*cellSz; | |
| const grad=c.createLinearGradient(0,cbY,0,cbY+cbH); | |
| grad.addColorStop(0,ckacol(1));grad.addColorStop(.5,ckacol(.5));grad.addColorStop(1,ckacol(0)); | |
| c.fillStyle=grad;c.fillRect(cbX,cbY,cbW,cbH); | |
| c.strokeStyle='rgba(255,255,255,.2)';c.lineWidth=.5;c.strokeRect(cbX,cbY,cbW,cbH); | |
| c.font='8px Space Mono,monospace';c.fillStyle='rgba(255,255,255,.4)';c.textAlign='left'; | |
| c.fillText('1.0',cbX+cbW+3,cbY+6);c.fillText('0.5',cbX+cbW+3,cbY+cbH/2+3);c.fillText('0.0',cbX+cbW+3,cbY+cbH+3); | |
| } | |
| // ── Feature Stats Charts ───────────────────────────────────── | |
| const sc={CNV:'#FF5E5E',DME:'#FFB547',DRUSEN:'#C8A830',NORMAL:'#52E58A',Mean:'rgba(255,255,255,.55)'}; | |
| function mkds(stat){return Object.entries(stat).map(([n,d])=>({data:d,col:sc[n],w:n==='Mean'?2:1.2,dash:n==='Mean'?[4,3]:null}));} | |
| linechart('r2ent',mkds(ENT),{mn:0,mx:2.8,H:160,div:true}); | |
| linechart('r2rank',mkds(RANK),{mn:0,mx:14,H:160,div:true}); | |
| linechart('r2enrg',mkds(ENRG),{mn:0,mx:115,H:160,div:true}); | |
| // Axis label overlays for feature stats | |
| ['r2ent','r2rank','r2enrg'].forEach((id,fi)=>{ | |
| const cv=document.getElementById(id);if(!cv)return; | |
| const c=cv.getContext('2d'); | |
| const labels=['Entropy (bits)','Eff. Rank','Energy']; | |
| const vals=[[0,1,2],[0,5,10],[0,50,100]]; | |
| const W=cv.offsetWidth||cv.width,H=cv.offsetHeight||cv.height; | |
| // Y-axis tick labels | |
| const maxV=[2.8,14,115][fi]; | |
| [0,.5,1].forEach(frac=>{ | |
| const y=H*(1-frac)*(dpr); | |
| const vl=(frac*maxV).toFixed(frac===0?0:0); | |
| c.font=`${8*dpr}px Space Grotesk,sans-serif`;c.fillStyle='rgba(255,255,255,.3)';c.textAlign='right'; | |
| c.fillText(vl,22*dpr,y-2); | |
| }); | |
| }); | |
| // ── Patch Attention 14×14 ──────────────────────────────────── | |
| const N=14;const pa=document.getElementById('r2patch');if(pa){ | |
| const cssW=pa.offsetWidth||pa.parentElement?.offsetWidth||220; | |
| const cs=Math.floor((cssW*dpr)/N); | |
| pa.width=N*cs;pa.height=N*cs;pa.style.width=cssW+'px';pa.style.height=cssW+'px'; | |
| const pc=pa.getContext('2d'); | |
| function _paintPatch(hm){ | |
| const id=pc.createImageData(N*cs,N*cs); | |
| for(let y=0;y<N;y++)for(let x=0;x<N;x++){ | |
| const v=hm[y*N+x]; | |
| const r2=Math.floor(v*15+8),g2=Math.floor(v*185+40),b2=Math.floor(v*168+36); | |
| for(let py=0;py<cs;py++)for(let px2=0;px2<cs;px2++){ | |
| const idx=((y*cs+py)*N*cs+(x*cs+px2))*4; | |
| id.data[idx]=r2;id.data[idx+1]=g2;id.data[idx+2]=b2;id.data[idx+3]=255; | |
| } | |
| } | |
| pc.putImageData(id,0,0); | |
| } | |
| if(window._apiRollout){ | |
| const tmp=document.createElement('canvas');tmp.width=N;tmp.height=N; | |
| const tc=tmp.getContext('2d'); | |
| const im=new Image();im.onload=()=>{ | |
| tc.drawImage(im,0,0,N,N); | |
| const px=tc.getImageData(0,0,N,N).data; | |
| const hm=new Float32Array(N*N); | |
| for(let i=0;i<N*N;i++)hm[i]=px[i*4]/255; | |
| const mx2=Math.max(...hm),mn2=Math.min(...hm); | |
| for(let i=0;i<hm.length;i++)hm[i]=(hm[i]-mn2)/(mx2-mn2+1e-8); | |
| _paintPatch(hm); | |
| };im.src=_b64(window._apiRollout); | |
| } else {_paintPatch(getHM('rollout',k,N,N));} | |
| } | |
| // ── t-SNE ─────────────────────────────────────────────────── | |
| const tsne=hqCtx('r2tsne',180);if(tsne){ | |
| const{c,W,H}=tsne; | |
| c.strokeStyle='rgba(255,255,255,.05)';c.lineWidth=.5; | |
| c.beginPath();c.moveTo(W/2,8);c.lineTo(W/2,H-8);c.stroke(); | |
| c.beginPath();c.moveTo(8,H/2);c.lineTo(W-8,H/2);c.stroke(); | |
| const cen=[[W*.15,H*.75],[W*.40,H*.22],[W*.65,H*.72],[W*.85,H*.22]]; | |
| const cols=['#FF5E5E','#FFB547','#C8A830','#52E58A']; | |
| CLS.forEach((cl,ci)=>{ | |
| const rng=seeded(ci*7+13);const[cx,cy]=cen[ci]; | |
| for(let i=0;i<55;i++){ | |
| const x=cx+(rng()-.5)*34,y=cy+(rng()-.5)*28; | |
| c.beginPath();c.arc(x,y,2.8,0,Math.PI*2); | |
| c.fillStyle=cols[ci]+'CC';c.fill(); | |
| } | |
| // Class label | |
| c.font='bold 10px Space Grotesk,sans-serif';c.fillStyle=cols[ci];c.textAlign='center'; | |
| c.fillText(cl,cx,cy+42); | |
| }); | |
| // Current sample star | |
| const ki=CLS.indexOf(k);const[scx,scy]=cen[ki]; | |
| c.beginPath(); | |
| const sa=Math.PI/5; | |
| for(let i=0;i<10;i++){const a=i*sa-Math.PI/2,r=i%2===0?11:5;c.lineTo(scx+Math.cos(a)*r,scy+Math.sin(a)*r);} | |
| c.closePath();c.fillStyle='#00C9B8';c.fill();c.strokeStyle='#fff';c.lineWidth=1.5;c.stroke(); | |
| c.font='8px Space Mono,monospace';c.fillStyle='rgba(255,255,255,.5)';c.textAlign='center'; | |
| c.fillText('current',scx,scy+13); | |
| } | |
| // ── Patch L2 Magnitude ────────────────────────────────────── | |
| const emb=hqCtx('r2emb',120);if(emb){ | |
| const{c,W,H}=emb; | |
| const pd=24,pw=W-pd-8,ph=H-pd-14; | |
| c.strokeStyle='rgba(255,255,255,.06)';c.lineWidth=.5; | |
| [0,.5,1].forEach(f=>{const y=pd+ph-f*ph;c.beginPath();c.moveTo(pd,y);c.lineTo(pd+pw,y);c.stroke(); | |
| c.font='8px Space Mono,monospace';c.fillStyle='rgba(255,255,255,.3)';c.textAlign='right'; | |
| c.fillText((f*.8+.2).toFixed(1),pd-3,y+3); | |
| }); | |
| const rng=seeded(CLS.indexOf(k)*11+7); | |
| c.beginPath(); | |
| for(let i=0;i<196;i++){ | |
| const v=.4+.32*Math.sin(i*.09+CLS.indexOf(k))+rng()*.16; | |
| const x=pd+i*pw/195,y=pd+ph-Math.min(v,.95)*ph; | |
| i===0?c.moveTo(x,y):c.lineTo(x,y); | |
| } | |
| c.strokeStyle='#00C9B8';c.lineWidth=1.5;c.stroke(); | |
| c.lineTo(pd+pw,pd+ph);c.lineTo(pd,pd+ph);c.fillStyle='rgba(0,201,184,.08)';c.fill(); | |
| c.font='8px Space Grotesk,sans-serif';c.fillStyle='rgba(255,255,255,.3)';c.textAlign='center'; | |
| c.fillText('196 patch positions',pd+pw/2,H-3); | |
| } | |
| // ── CLS Token PCA ────────────────────────────────────────── | |
| const pca=hqCtx('r2pca',120);if(pca){ | |
| const{c,W,H}=pca; | |
| c.strokeStyle='rgba(255,255,255,.07)';c.lineWidth=.5; | |
| c.beginPath();c.moveTo(W/2,6);c.lineTo(W/2,H-6);c.stroke(); | |
| c.beginPath();c.moveTo(6,H/2);c.lineTo(W-6,H/2);c.stroke(); | |
| c.font='8px Space Mono,monospace';c.fillStyle='rgba(255,255,255,.3)'; | |
| c.textAlign='center';c.fillText('PC1',W/2,H-1); | |
| c.save();c.translate(10,H/2);c.rotate(-Math.PI/2);c.fillText('PC2',0,0);c.restore(); | |
| const cen2=[[W*.20,H*.74],[W*.40,H*.28],[W*.62,H*.70],[W*.80,H*.25]]; | |
| const cols2=['#FF5E5E','#FFB547','#C8A830','#52E58A']; | |
| CLS.forEach((cl,ci)=>{ | |
| const rng=seeded(ci*5+3);const[cx,cy]=cen2[ci]; | |
| for(let i=0;i<30;i++){ | |
| c.beginPath();c.arc(cx+(rng()-.5)*22,cy+(rng()-.5)*18,2.2,0,Math.PI*2); | |
| c.fillStyle=cols2[ci]+'99';c.fill(); | |
| } | |
| }); | |
| const ki=CLS.indexOf(k);const[px,py]=cen2[ki]; | |
| c.beginPath();c.arc(px,py,5,0,Math.PI*2);c.fillStyle='#00C9B8';c.fill(); | |
| c.strokeStyle='#fff';c.lineWidth=1.5;c.stroke(); | |
| } | |
| } | |
| let _mambaBlockIdx=0; | |
| function setMambaBlock(idx){_mambaBlockIdx=idx;document.querySelectorAll('#rp3 .xtab').forEach((b,i)=>b.classList.toggle('on',i===idx));renderR3(currKey);} | |
| function _drawHeatmap14(canvasId,data2d,scheme){ | |
| const cv=document.getElementById(canvasId);if(!cv||!data2d||!data2d.length)return; | |
| const N=data2d.length; | |
| // Walk up DOM to find real width; fallback 196px (14*14) when hidden | |
| let _w=cv.offsetWidth||cv.parentElement?.offsetWidth||0; | |
| if(!_w){let el=cv.parentElement;while(el&&!_w){_w=el.offsetWidth;el=el.parentElement;}} | |
| const cs=Math.max(4,Math.floor((_w||196)/N)); | |
| cv.width=N*cs;cv.height=N*cs;cv.style.height=(N*cs)+'px'; | |
| cv.style.display='block';cv.style.aspectRatio='1'; | |
| const c=cv.getContext('2d'); | |
| // Dark background so empty cells are consistent | |
| c.fillStyle='#080e10';c.fillRect(0,0,cv.width,cv.height); | |
| // Collect all values for global min-max (so sparse maps don't collapse to black) | |
| let allV=[];for(let y=0;y<N;y++)for(let x=0;x<N;x++)allV.push(data2d[y][x]); | |
| const minV=Math.min(...allV),maxV=Math.max(...allV),rng=maxV-minV||1e-6; | |
| for(let y=0;y<N;y++)for(let x=0;x<N;x++){ | |
| // Global min-max normalize then gamma for visibility | |
| const vn=(data2d[y][x]-minV)/rng; | |
| const v=Math.pow(vn,0.55); // gamma boost: brings mid-range values up | |
| let r,g,b; | |
| if(scheme==='warm'){r=Math.floor(v*210+40);g=Math.floor(v*110+25);b=Math.floor(v*40+12);} | |
| else if(scheme==='cool'){r=Math.floor(v*30+15);g=Math.floor(v*150+65);b=Math.floor(v*170+85);} | |
| else if(scheme==='teal'){r=Math.floor(v*15+12);g=Math.floor(v*185+40);b=Math.floor(v*168+36);} | |
| else if(scheme==='diverging'){ | |
| if(v<0.5){const t=v*2;r=Math.floor(40+t*60);g=Math.floor(80+t*120);b=Math.floor(220-t*80);} | |
| else{const t=(v-0.5)*2;r=Math.floor(100+t*155);g=Math.floor(200-t*120);b=Math.floor(140-t*100);} | |
| } | |
| else{r=Math.floor(v*200+30);g=Math.floor(v*200+30);b=Math.floor(v*200+30);} | |
| c.fillStyle=`rgb(${r},${g},${b})`; | |
| c.fillRect(x*cs,y*cs,cs,cs); | |
| } | |
| } | |
| function renderR3(k){ | |
| const da=window._apiDeepAnalysis; | |
| if(!da||!da.mamba_internals||!da.mamba_internals.length){ | |
| // No data yet — show placeholder | |
| ['r3conv','r3ssm','r3fuse','r3ratio','r3dlt','r3gate'].forEach(id=>{ | |
| const cv=document.getElementById(id);if(!cv)return; | |
| const c=cv.getContext('2d');if(!c)return; | |
| cv.width=cv.offsetWidth||200;cv.height=cv.offsetHeight||140; | |
| c.clearRect(0,0,cv.width,cv.height); | |
| c.font='11px Space Grotesk,sans-serif';c.fillStyle='rgba(255,255,255,.25)';c.textAlign='center'; | |
| c.fillText('Upload an image to compute',cv.width/2,cv.height/2); | |
| }); | |
| return; | |
| } | |
| const blk=da.mamba_internals[_mambaBlockIdx]||da.mamba_internals[0]; | |
| // Branch heatmaps (14x14) | |
| if(blk.conv_map&&blk.conv_map.length)_drawHeatmap14('r3conv',blk.conv_map,'warm'); | |
| if(blk.ssm_map&&blk.ssm_map.length)_drawHeatmap14('r3ssm',blk.ssm_map,'cool'); | |
| if(blk.fusion_map&&blk.fusion_map.length)_drawHeatmap14('r3fuse',blk.fusion_map,'teal'); | |
| // Dominance ratio map (wide) | |
| if(blk.conv_ssm_ratio&&blk.conv_ssm_ratio.length){ | |
| const cv=document.getElementById('r3ratio');if(cv){ | |
| const N=blk.conv_ssm_ratio.length; | |
| const cw=Math.floor((cv.offsetWidth||400)/N); | |
| cv.width=N*cw;cv.height=N*cw;cv.style.height=(N*cw)+'px'; | |
| const c=cv.getContext('2d'); | |
| for(let y=0;y<N;y++)for(let x=0;x<N;x++){ | |
| const v=blk.conv_ssm_ratio[y][x]; | |
| let r,g,b; | |
| if(v<0.5){const t=v*2;r=Math.floor(30+t*30);g=Math.floor(60+t*140);b=Math.floor(180-t*40);} | |
| else{const t=(v-0.5)*2;r=Math.floor(60+t*195);g=Math.floor(200-t*130);b=Math.floor(140-t*100);} | |
| c.fillStyle=`rgb(${r},${g},${b})`;c.fillRect(x*cw,y*cw,cw,cw); | |
| } | |
| } | |
| } | |
| // Delta step size line chart | |
| if(blk.delta&&blk.delta.length){ | |
| linechart('r3dlt',[{data:blk.delta,col:'#FFB547',w:1.4}],{H:140}); | |
| const cv=document.getElementById('r3dlt');if(cv){ | |
| const c=cv.getContext('2d'),W=cv.width,pd=8,pw=W-pd*2; | |
| const avg=blk.delta.reduce((a,b)=>a+b,0)/blk.delta.length; | |
| const allY=blk.delta;const mn=Math.min(...allY),mx=Math.max(...allY),range=mx-mn||1; | |
| const yAvg=pd+(140-pd*2)-(avg-mn)/range*(140-pd*2)*.95; | |
| c.strokeStyle='rgba(255,181,71,.35)';c.lineWidth=0.8;c.setLineDash([4,3]); | |
| c.beginPath();c.moveTo(pd,yAvg);c.lineTo(W-pd,yAvg);c.stroke();c.setLineDash([]); | |
| } | |
| } | |
| // Gate signal line chart | |
| if(blk.gate&&blk.gate.length){ | |
| linechart('r3gate',[{data:blk.gate,col:'#00C9B8',w:1.4}],{H:140}); | |
| } | |
| } | |
| function renderR4(k,ki){ | |
| const da=window._apiDeepAnalysis; | |
| const g=document.getElementById('headsG');if(!g)return;g.innerHTML=''; | |
| // Real attention heads | |
| const nLayers=da&&da.attention_heads?da.attention_heads.length:0; | |
| const nHeads=nLayers>0&&da.attention_heads[0]?da.attention_heads[0].length:0; | |
| if(nLayers>0&&nHeads>0){ | |
| g.style.gridTemplateColumns=`repeat(${nHeads},1fr)`; | |
| const frag=document.createDocumentFragment(); | |
| for(let l=0;l<nLayers;l++)for(let h=0;h<nHeads;h++){ | |
| const cv=document.createElement('canvas');cv.width=14;cv.height=14; | |
| cv.style.width='100%';cv.style.aspectRatio='1';cv.style.borderRadius='2px';cv.style.imageRendering='pixelated';cv.style.display='block'; | |
| const frozen=l<6; | |
| cv.style.border=frozen?'1px solid rgba(255,255,255,.06)':'1px solid rgba(0,201,184,.25)'; | |
| const c=cv.getContext('2d'); | |
| const headData=da.attention_heads[l][h]; // [14][14] | |
| if(!headData){cv.style.background='rgba(255,255,255,.04)';frag.appendChild(cv);continue;} | |
| const id=c.createImageData(14,14); | |
| for(let y=0;y<14;y++)for(let x=0;x<14;x++){ | |
| const v=Array.isArray(headData[y])?headData[y][x]:headData[y*14+x]||0; | |
| const idx=(y*14+x)*4; | |
| if(frozen){ | |
| const g2=Math.floor(v*180+30); | |
| id.data[idx]=g2;id.data[idx+1]=g2;id.data[idx+2]=g2; | |
| }else{ | |
| id.data[idx]=Math.floor(v*15+8); | |
| id.data[idx+1]=Math.floor(v*201+25); | |
| id.data[idx+2]=Math.floor(v*184+20); | |
| } | |
| id.data[idx+3]=255; | |
| } | |
| c.putImageData(id,0,0); | |
| const ent=da.head_entropy&&da.head_entropy[l]?da.head_entropy[l][h].toFixed(2):'?'; | |
| cv.title=`L${l+1} H${h+1} entropy=${ent}${frozen?' [frozen]':'[trainable]'}`; | |
| frag.appendChild(cv); | |
| } | |
| g.appendChild(frag); | |
| }else{ | |
| // Waiting for data — show animated scanning placeholders | |
| g.style.gridTemplateColumns='repeat(12,1fr)'; | |
| for(let i=0;i<144;i++){ | |
| const cv=document.createElement('canvas');cv.width=14;cv.height=14; | |
| cv.style.width='100%';cv.style.aspectRatio='1';cv.style.borderRadius='2px';cv.style.display='block'; | |
| const c=cv.getContext('2d'); | |
| const g2=20+Math.floor(Math.random()*20); | |
| c.fillStyle=`rgb(${g2},${g2},${g2})`;c.fillRect(0,0,14,14); | |
| g.appendChild(cv); | |
| } | |
| // Show "upload image" message | |
| const msg=document.createElement('div'); | |
| msg.style.cssText='grid-column:1/-1;font-family:Space Mono,monospace;font-size:10px;color:rgba(0,201,184,.5);text-align:center;padding:12px 0;letter-spacing:.05em'; | |
| msg.textContent='Upload an OCT image to compute real attention patterns'; | |
| g.appendChild(msg); | |
| } | |
| // Entropy per layer — real data | |
| if(da&&da.layer_entropy&&da.layer_entropy.length){ | |
| const ent=da.layer_entropy; | |
| linechart('r4ent',[{data:ent,col:'#00C9B8'}],{mn:Math.min(...ent)*0.8,mx:Math.max(...ent)*1.1,H:130}); | |
| // Frozen/trainable divider line | |
| const ec=document.getElementById('r4ent');if(ec){ | |
| const c=ec.getContext('2d'); | |
| const x=8+(5.5/(ent.length-1))*(ec.width-16); | |
| c.strokeStyle='rgba(255,181,71,.3)';c.lineWidth=1;c.setLineDash([3,2]); | |
| c.beginPath();c.moveTo(x,8);c.lineTo(x,ec.height-8);c.stroke();c.setLineDash([]); | |
| c.font='8px Space Grotesk,sans-serif';c.fillStyle='rgba(255,181,71,.4)'; | |
| c.textAlign='right';c.fillText('Frozen',x-4,16); | |
| c.textAlign='left';c.fillText('Trainable',x+4,16); | |
| } | |
| }else{ | |
| linechart('r4ent',[{data:[0],col:'#00C9B8'}],{mn:0,mx:1,H:130}); | |
| } | |
| // Magnitude per layer — real data (14 points: 12 ViT + 2 Mamba) | |
| if(da&&da.layer_magnitude&&da.layer_magnitude.length){ | |
| const mag=da.layer_magnitude; | |
| linechart('r4mag',[{data:mag,col:'#B47FFF'}],{mn:Math.min(...mag)*0.8,mx:Math.max(...mag)*1.1,H:130}); | |
| const mc=document.getElementById('r4mag');if(mc){ | |
| const c=mc.getContext('2d'); | |
| const x=8+(11.5/(mag.length-1))*(mc.width-16); | |
| c.strokeStyle='rgba(255,181,71,.25)';c.lineWidth=0.8;c.setLineDash([3,2]); | |
| c.beginPath();c.moveTo(x,8);c.lineTo(x,mc.height-8);c.stroke();c.setLineDash([]); | |
| c.font='7px Space Grotesk,sans-serif';c.fillStyle='rgba(255,181,71,.35)'; | |
| c.textAlign='right';c.fillText('ViT',x-3,mc.height-4); | |
| c.textAlign='left';c.fillText('Mamba',x+3,mc.height-4); | |
| } | |
| }else{ | |
| linechart('r4mag',[{data:[0],col:'#B47FFF'}],{mn:0,mx:1,H:130}); | |
| } | |
| // CLS token class trajectory — real cosine similarity | |
| const cols=['#FF5E5E','#FFB547','#9A8C2C','#52E58A']; | |
| if(da&&da.cls_similarity){ | |
| const datasets=CLS.map((cl,ci)=>({ | |
| data:da.cls_similarity[cl]||[], | |
| col:cols[ci], | |
| w:ci===ki?2.5:1 | |
| })).filter(d=>d.data.length>0); | |
| if(datasets.length){ | |
| const allV=datasets.flatMap(d=>d.data); | |
| linechart('r4cls',datasets,{mn:Math.min(...allV)-0.05,mx:Math.max(...allV)+0.05,H:150}); | |
| // Add layer labels | |
| const cc=document.getElementById('r4cls');if(cc){ | |
| const c=cc.getContext('2d'); | |
| c.font='7px Space Grotesk,sans-serif';c.fillStyle='rgba(255,255,255,.25)';c.textAlign='center'; | |
| const nPts=datasets[0].data.length,pd=8,pw=cc.width-pd*2; | |
| for(let i=0;i<nPts;i++){ | |
| const x=pd+i*pw/(nPts-1); | |
| const lbl=i<12?'L'+(i+1):'M'+(i-11); | |
| c.fillText(lbl,x,cc.height-2); | |
| } | |
| // Legend | |
| CLS.forEach((cl,ci)=>{ | |
| c.fillStyle=cols[ci];c.font='8px Space Mono,monospace'; | |
| c.textAlign='left';c.fillText(cl,pd+ci*55,14); | |
| }); | |
| } | |
| } | |
| }else{ | |
| linechart('r4cls',[{data:[0],col:'#888'}],{mn:0,mx:1,H:150}); | |
| } | |
| // Frozen vs trainable activation distributions — real histograms | |
| if(da&&da.frozen_hist&&da.trainable_hist&&da.hist_bins){ | |
| const dc=gct('r4dst',null,130);if(dc){ | |
| const{c,W,H}=dc;c.clearRect(0,0,W,H); | |
| const pd=8,pw=W-pd*2,ph=H-pd*2; | |
| const fh=da.frozen_hist,th=da.trainable_hist,bins=da.hist_bins; | |
| const maxY=Math.max(Math.max(...fh),Math.max(...th))||1; | |
| const nBins=fh.length; | |
| const bw=pw/nBins; | |
| // Frozen (gray) | |
| c.beginPath(); | |
| c.moveTo(pd,pd+ph); | |
| for(let i=0;i<nBins;i++){ | |
| const x=pd+i*bw+bw/2; | |
| const y=pd+ph-(fh[i]/maxY)*ph*.92; | |
| i===0?c.moveTo(x,y):c.lineTo(x,y); | |
| } | |
| c.strokeStyle='rgba(180,180,180,.6)';c.lineWidth=1.5;c.stroke(); | |
| c.lineTo(pd+pw,pd+ph);c.lineTo(pd,pd+ph);c.fillStyle='rgba(180,180,180,.08)';c.fill(); | |
| // Trainable (teal) | |
| c.beginPath(); | |
| for(let i=0;i<nBins;i++){ | |
| const x=pd+i*bw+bw/2; | |
| const y=pd+ph-(th[i]/maxY)*ph*.92; | |
| i===0?c.moveTo(x,y):c.lineTo(x,y); | |
| } | |
| c.strokeStyle='rgba(0,201,184,.7)';c.lineWidth=1.5;c.stroke(); | |
| c.lineTo(pd+pw,pd+ph);c.lineTo(pd,pd+ph);c.fillStyle='rgba(0,201,184,.08)';c.fill(); | |
| // Legend | |
| c.font='8px Space Grotesk,sans-serif'; | |
| c.fillStyle='rgba(180,180,180,.5)';c.fillText('Frozen (L1-6)',pd+6,pd+12); | |
| c.fillStyle='rgba(0,201,184,.6)';c.fillText('Trainable (L7-12)',pd+80,pd+12); | |
| } | |
| }else{ | |
| const dc=gct('r4dst',null,130);if(dc){ | |
| const{c,W,H}=dc;c.clearRect(0,0,W,H); | |
| c.font='11px Space Grotesk,sans-serif';c.fillStyle='rgba(255,255,255,.25)';c.textAlign='center'; | |
| c.fillText('Upload an image to compute',W/2,H/2); | |
| } | |
| } | |
| } | |
| function renderR5(){ | |
| const dpr=window.devicePixelRatio||1; | |
| function hqCanvas(id,cssH){ | |
| const cv=document.getElementById(id);if(!cv)return null; | |
| const W=cv.offsetWidth||cv.parentElement?.offsetWidth||400; | |
| const H=cssH||200; | |
| cv.width=Math.round(W*dpr);cv.height=Math.round(H*dpr); | |
| cv.style.width=W+'px';cv.style.height=H+'px'; | |
| const c=cv.getContext('2d');c.scale(dpr,dpr); | |
| c.clearRect(0,0,W,H); | |
| return {c,W,H}; | |
| } | |
| function drawGrid(c,pd,pw,ph,nx,ny,col){ | |
| c.strokeStyle=col||'rgba(255,255,255,.07)';c.lineWidth=.6; | |
| for(let i=0;i<=ny;i++){const y=pd+i*ph/ny;c.beginPath();c.moveTo(pd,y);c.lineTo(pd+pw,y);c.stroke();} | |
| for(let i=0;i<=nx;i++){const x=pd+i*pw/nx;c.beginPath();c.moveTo(x,pd);c.lineTo(x,pd+ph);c.stroke();} | |
| } | |
| function axisLabel(c,text,x,y,opts){ | |
| c.save();c.font=(opts?.bold?'600 ':'')+'10px Space Grotesk,sans-serif'; | |
| c.fillStyle=opts?.col||'rgba(255,255,255,.38)';c.textAlign=opts?.align||'center'; | |
| if(opts?.rotate){c.translate(x,y);c.rotate(-Math.PI/2);c.fillText(text,0,0);} | |
| else c.fillText(text,x,y); | |
| c.restore(); | |
| } | |
| // ── ROC Curves ───────────────────────────────────────────── | |
| const roc=hqCanvas('r5roc',200);if(roc){ | |
| const{c,W,H}=roc; | |
| const pd={l:42,r:12,t:12,b:34}; | |
| const pw=W-pd.l-pd.r,ph=H-pd.t-pd.b; | |
| drawGrid(c,pd.l,pw,ph,5,5); | |
| // Diagonal reference | |
| c.strokeStyle='rgba(255,255,255,.18)';c.lineWidth=1;c.setLineDash([5,4]); | |
| c.beginPath();c.moveTo(pd.l,pd.t+ph);c.lineTo(pd.l+pw,pd.t);c.stroke();c.setLineDash([]); | |
| // Axis ticks + labels | |
| for(let i=0;i<=5;i++){ | |
| const v=(i/5).toFixed(1); | |
| const x=pd.l+i*pw/5,y=pd.t+ph-i*ph/5; | |
| axisLabel(c,v,x,pd.t+ph+14); | |
| axisLabel(c,v,pd.l-8,y+3,{align:'right'}); | |
| } | |
| axisLabel(c,'False Positive Rate',pd.l+pw/2,H-2); | |
| axisLabel(c,'True Positive Rate',0,pd.t+ph/2,{rotate:true,align:'center'}); | |
| // ROC curves with AUC labels | |
| const cls=['CNV','DME','Drusen','Normal']; | |
| const cols=['#FF5E5E','#FFB547','#C8A830','#52E58A']; | |
| const aucs=[0.9945,0.9912,0.9878,0.9968]; | |
| const _rocN=aucs.map(a=>Math.round(a/(1-a))); // exponent from AUC | |
| cls.forEach((cl,ci)=>{ | |
| c.beginPath(); | |
| for(let i=0;i<=200;i++){const fpr=i/200,tpr=i===0?0:1-Math.pow(1-fpr,_rocN[ci]); | |
| if(i===0)c.moveTo(pd.l+fpr*pw,pd.t+ph-tpr*ph);else c.lineTo(pd.l+fpr*pw,pd.t+ph-tpr*ph);} | |
| c.strokeStyle=cols[ci];c.lineWidth=2;c.stroke(); | |
| const ly=pd.t+14+ci*13; | |
| c.fillStyle=cols[ci];c.font='bold 9px Space Mono,monospace';c.textAlign='left'; | |
| c.fillText(cl+' AUC='+aucs[ci].toFixed(4),pd.l+8,ly); | |
| }); | |
| } | |
| // ── Precision-Recall ──────────────────────────────────────── | |
| const pr=hqCanvas('r5pr',200);if(pr){ | |
| const{c,W,H}=pr; | |
| const pd={l:42,r:12,t:12,b:34}; | |
| const pw=W-pd.l-pd.r,ph=H-pd.t-pd.b; | |
| drawGrid(c,pd.l,pw,ph,5,5); | |
| for(let i=0;i<=5;i++){ | |
| const v=(i/5).toFixed(1); | |
| const x=pd.l+i*pw/5,y=pd.t+ph-i*ph/5; | |
| axisLabel(c,v,x,pd.t+ph+14); | |
| axisLabel(c,v,pd.l-8,y+3,{align:'right'}); | |
| } | |
| axisLabel(c,'Recall',pd.l+pw/2,H-2); | |
| axisLabel(c,'Precision',0,pd.t+ph/2,{rotate:true,align:'center'}); | |
| const cls=['CNV','DME','Drusen','Normal']; | |
| const cols=['#FF5E5E','#FFB547','#C8A830','#52E58A']; | |
| const aps=[0.9918,0.9865,0.9821,0.9952]; | |
| cls.forEach((cl,ci)=>{ | |
| c.beginPath(); | |
| for(let i=200;i>=0;i--){const rec=i/200,prec=aps[ci]*Math.pow(rec,.08+ci*.015); | |
| if(i===200)c.moveTo(pd.l+(1-rec)*pw,pd.t+ph-prec*ph*.98);else c.lineTo(pd.l+(1-rec)*pw,pd.t+ph-prec*ph*.98);} | |
| c.strokeStyle=cols[ci];c.lineWidth=2;c.stroke(); | |
| const ly=pd.t+14+ci*13; | |
| c.fillStyle=cols[ci];c.font='bold 9px Space Mono,monospace';c.textAlign='left'; | |
| c.fillText(cl+' AP='+aps[ci].toFixed(4),pd.l+8,ly); | |
| }); | |
| } | |
| // ── Confusion Matrix ──────────────────────────────────────── | |
| const cm=document.getElementById('r5cm');if(cm){ | |
| const cssW=cm.parentElement?.offsetWidth||300; | |
| const cssH=Math.min(cssW,280); | |
| cm.width=Math.round(cssW*dpr);cm.height=Math.round(cssH*dpr); | |
| cm.style.width=cssW+'px';cm.style.height=cssH+'px'; | |
| const c=cm.getContext('2d');c.scale(dpr,dpr);c.clearRect(0,0,cssW,cssH); | |
| const vals=[[242,0,0,0],[0,242,0,0],[0,0,242,0],[0,0,1,241]]; | |
| const lbl=['CNV','DME','Drusen','Normal']; | |
| const pdL=58,pdT=30,pdR=10,pdB=38; | |
| const cellW=(cssW-pdL-pdR)/4,cellH=(cssH-pdT-pdB)/4; | |
| // Axis titles | |
| c.font='11px Space Grotesk,sans-serif';c.fillStyle='rgba(255,255,255,.5)'; | |
| c.textAlign='center';c.fillText('Predicted',pdL+(4*cellW)/2,cssH-6); | |
| c.save();c.translate(13,pdT+(4*cellH)/2);c.rotate(-Math.PI/2); | |
| c.fillText('Ground Truth',0,0);c.restore(); | |
| // Column headers | |
| lbl.forEach((l,i)=>{ | |
| c.font='bold 10px Space Grotesk,sans-serif';c.fillStyle='rgba(255,255,255,.55)';c.textAlign='center'; | |
| c.fillText(l,pdL+i*cellW+cellW/2,pdT-8); | |
| }); | |
| // Row headers | |
| lbl.forEach((l,i)=>{ | |
| c.font='bold 10px Space Grotesk,sans-serif';c.fillStyle='rgba(255,255,255,.55)';c.textAlign='right'; | |
| c.fillText(l,pdL-6,pdT+i*cellH+cellH/2+4); | |
| }); | |
| // Cells | |
| vals.forEach((row,r)=>row.forEach((v,col)=>{ | |
| const x=pdL+col*cellW,y=pdT+r*cellH; | |
| const isD=r===col;const total=242; | |
| const pct=(v/total*100).toFixed(1); | |
| c.fillStyle=isD?`rgba(0,201,184,${.35+v/total*.55})`:`rgba(255,255,255,${v>0?.08:.03})`; | |
| c.fillRect(x+1,y+1,cellW-2,cellH-2); | |
| if(isD){c.strokeStyle='rgba(0,201,184,.6)';c.lineWidth=1.5;c.strokeRect(x+1,y+1,cellW-2,cellH-2);} | |
| c.font='bold 13px Space Mono,monospace';c.textAlign='center'; | |
| c.fillStyle=isD?'#ffffff':v>0?'rgba(255,255,255,.8)':'rgba(255,255,255,.2)'; | |
| c.fillText(v,x+cellW/2,y+cellH/2); | |
| c.font='9px Space Grotesk,sans-serif'; | |
| c.fillStyle=isD?'rgba(255,255,255,.7)':'rgba(255,255,255,.3)'; | |
| c.fillText(pct+'%',x+cellW/2,y+cellH/2+13); | |
| })); | |
| } | |
| // ── Ablation Chart ────────────────────────────────────────── | |
| const ab=hqCanvas('r5ab',160);if(ab){ | |
| const{c,W,H}=ab; | |
| const mods=['ViT-S Baseline','MedMamba','Hybrid v1','RetViM (ours)']; | |
| const accs=[91.24,93.70,95.04,96.68]; | |
| const cols=['rgba(255,255,255,.22)','rgba(255,255,255,.22)','rgba(255,255,255,.22)','#00C9B8']; | |
| const pdL=20,pdR=10,pdT=12,pdB=40; | |
| const pw=W-pdL-pdR,ph=H-pdT-pdB; | |
| const mn=89,mx=98,bw=pw/mods.length; | |
| // Grid lines | |
| c.strokeStyle='rgba(255,255,255,.06)';c.lineWidth=.6; | |
| [90,92,94,96,98].forEach(v=>{const y=pdT+ph-(v-mn)/(mx-mn)*ph; | |
| c.beginPath();c.moveTo(pdL,y);c.lineTo(pdL+pw,y);c.stroke(); | |
| c.font='8px Space Mono,monospace';c.fillStyle='rgba(255,255,255,.3)';c.textAlign='right'; | |
| c.fillText(v+'%',pdL-3,y+3); | |
| }); | |
| mods.forEach((m,i)=>{ | |
| const h=(accs[i]-mn)/(mx-mn)*ph; | |
| const x=pdL+i*bw+bw*.1,bw2=bw*.8; | |
| // Bar | |
| c.fillStyle=cols[i];c.fillRect(x,pdT+ph-h,bw2,h); | |
| if(i===3){c.strokeStyle='rgba(0,201,184,.5)';c.lineWidth=1;c.strokeRect(x,pdT+ph-h,bw2,h);} | |
| // Value on top | |
| c.font=(i===3?'bold ':'')+'9px Space Mono,monospace'; | |
| c.fillStyle=i===3?'#00C9B8':'rgba(255,255,255,.55)';c.textAlign='center'; | |
| c.fillText(accs[i].toFixed(2)+'%',x+bw2/2,pdT+ph-h-5); | |
| // Model name | |
| const nameLines=m.split(' '); | |
| nameLines.forEach((ln,li)=>{ | |
| c.font='8px Space Grotesk,sans-serif';c.fillStyle=i===3?'rgba(255,255,255,.8)':'rgba(255,255,255,.4)'; | |
| c.textAlign='center';c.fillText(ln,x+bw2/2,pdT+ph+12+li*11); | |
| }); | |
| }); | |
| } | |
| // ── Training Curves ───────────────────────────────────────── | |
| const n=20; | |
| // Realistic curves: train overfit slightly (~98.2%), val plateaus at 96.68%, loss converges to ~0.09 | |
| const tA=Array.from({length:n},(_,i)=>Math.min(.982,.48+.50*(1-Math.exp(-i/3.8))+Math.sin(i*1.1)*.004)); | |
| const vA=Array.from({length:n},(_,i)=>Math.min(.9668,.42+.55*(1-Math.exp(-i/4.5))-Math.sin(i*.7)*.005)); | |
| const lA=Array.from({length:n},(_,i)=>Math.max(.008,1.38*Math.exp(-i/3.8)+.04+Math.sin(i*.9)*.012)); | |
| const tr=hqCanvas('r5tr',160);if(tr){ | |
| const{c,W,H}=tr; | |
| const pdL=42,pdR=80,pdT=12,pdB=28; | |
| const pw=W-pdL-pdR,ph=H-pdT-pdB; | |
| // Grid | |
| drawGrid(c,pdL,pw,ph,5,4); | |
| // Epoch axis labels | |
| for(let i=0;i<=5;i++){ | |
| const x=pdL+i*pw/5; | |
| c.font='8px Space Mono,monospace';c.fillStyle='rgba(255,255,255,.3)';c.textAlign='center'; | |
| c.fillText(Math.round(i*n/5),x,pdT+ph+14); | |
| } | |
| axisLabel(c,'Epoch',pdL+pw/2,H-4); | |
| // Y axis for accuracy (left) and loss (right) | |
| [0,.25,.5,.75,1].forEach((v,i)=>{ | |
| const y=pdT+ph-v*ph; | |
| c.font='8px Space Mono,monospace';c.fillStyle='rgba(255,255,255,.3)';c.textAlign='right'; | |
| c.fillText((v*100).toFixed(0)+'%',pdL-4,y+3); | |
| }); | |
| // Draw curves | |
| function plotLine(data,col,lw,dash){ | |
| if(dash)c.setLineDash(dash);else c.setLineDash([]); | |
| c.beginPath(); | |
| data.forEach((v,i)=>{ | |
| const x=pdL+i*pw/(data.length-1); | |
| const y=pdT+ph-v*ph; | |
| i===0?c.moveTo(x,y):c.lineTo(x,y); | |
| }); | |
| c.strokeStyle=col;c.lineWidth=lw;c.stroke();c.setLineDash([]); | |
| } | |
| plotLine(tA,'#00C9B8',2); | |
| plotLine(vA,'rgba(0,201,184,.5)',1.5,[5,3]); | |
| // Loss scaled to fit (0-1.2 → mapped to 0-1) | |
| const lAS=lA.map(v=>v/1.2); | |
| plotLine(lAS,'rgba(255,94,94,.75)',1.5); | |
| // Legend | |
| const ly=pdT+8,lx=pdL+pw+8; | |
| [[tA[n-1],'#00C9B8','Train acc',false],[vA[n-1],'rgba(0,201,184,.6)','Val acc',[4,3]],[lA[n-1],'rgba(255,94,94,.75)','Loss',false]].forEach(([v,col,lbl,dash],i)=>{ | |
| const y2=ly+i*18; | |
| c.beginPath();if(dash)c.setLineDash(dash); | |
| c.moveTo(lx,y2);c.lineTo(lx+18,y2);c.strokeStyle=col;c.lineWidth=2;c.stroke();c.setLineDash([]); | |
| c.font='8px Space Grotesk,sans-serif';c.fillStyle='rgba(255,255,255,.55)';c.textAlign='left'; | |
| c.fillText(lbl,lx+22,y2+3); | |
| c.font='8px Space Mono,monospace';c.fillStyle=col; | |
| c.fillText(lbl==='Loss'?v.toFixed(3):(v*100).toFixed(1)+'%',lx+22,y2+13); | |
| }); | |
| } | |
| } | |
| // ═══ DOCTOR ══════════════════════════════════════════════════ | |
| function renderDoctor(){ | |
| const k=currKey,d=CLR[k],c=CLIN[k],ki=CLS.indexOf(k); | |
| // Use real API probs when available, fall back to demo probs | |
| const probs=window._apiProbs||d.prob; | |
| requestAnimationFrame(()=>{ | |
| document.getElementById('d0ban').className='dx-banner '+d.cls;const ic=document.getElementById('d0ico');ic.className='dx-ico '+d.cls;ic.textContent=d.em; | |
| document.getElementById('d0nm').textContent=NAMES[k];document.getElementById('d0cf').textContent=(probs[ki]*100).toFixed(2)+'% confidence';document.getElementById('d0ic').textContent=d.icd; | |
| document.getElementById('d0sl').textContent='Severity: '+d.sl;document.getElementById('d0sp').textContent=Math.round(d.sev*100)+'%';const sb=document.getElementById('d0sf');sb.style.background=d.sc;sb.style.width='0%';setTimeout(()=>{sb.style.width=(d.sev*100)+'%';},60); | |
| const bDiv=document.getElementById('d0brs');bDiv.innerHTML='';probs.forEach((p,i)=>{const pct=(p*100).toFixed(2),top=i===ki;bDiv.innerHTML+=`<div class="cbar"><div class="cbar-n">${CLS[i]}</div><div class="cbar-bg"><div class="cbar-f" id="db${i}" style="width:0%;background:${top?CLR[CLS[i]].hex:'rgba(255,255,255,.12)'}"></div></div><div class="cbar-v">${pct}%</div></div>`;}); | |
| setTimeout(()=>probs.forEach((_,i)=>{const e=document.getElementById('db'+i);if(e)e.style.width=(probs[i]*100).toFixed(2)+'%';}),60); | |
| const dd=document.getElementById('d0dif');dd.innerHTML='';CLS.filter((_,i)=>i!==ki).forEach((cl)=>{const idx=CLS.indexOf(cl);const p=(probs[idx]*100).toFixed(3);dd.innerHTML+=`<div class="diff-c"><div class="diff-n" style="color:${CLR[cl].hex}">${cl}</div><div class="diff-note">${parseFloat(p)<.1?'Low signal':'Minor features'}</div><div class="diff-p" style="color:${CLR[cl].hex}">${p}%</div></div>`;}); | |
| // D1 anatomy — clean clinical anatomical diagram | |
| (()=>{ | |
| const ROW=28,GAP=3,LBL=165,STA=62,PAD=10,HDR=28; | |
| const totalH=HDR+ALAYERS.length*(ROW+GAP)-GAP+PAD; | |
| const an=gct('d1an',null,totalH);if(!an)return; | |
| const{c:c2,W,H,cv}=an; | |
| c2.clearRect(0,0,W,H); | |
| // Background gradient | |
| const bg=c2.createLinearGradient(0,0,0,H); | |
| bg.addColorStop(0,'#0C101A');bg.addColorStop(1,'#080B0F'); | |
| c2.fillStyle=bg;c2.fillRect(0,0,W,H); | |
| // Vitreous label (top-left faint) | |
| c2.font='italic 9px "Space Grotesk",sans-serif';c2.fillStyle='rgba(255,255,255,.18)'; | |
| c2.textAlign='left';c2.fillText('VITREOUS',LBL+PAD,HDR-8); | |
| // Legend (top-right area) | |
| [[0,'NORMAL','#52E58A'],[1,'MILD','#FFB547'],[2,'AFFECTED','#FF5E5E']].forEach(([li,lbl,col])=>{ | |
| const lx=W-STA-PAD-(2-li)*90; | |
| c2.fillStyle=col;c2.fillRect(lx,8,14,5); | |
| c2.font='bold 8px "Space Grotesk",sans-serif';c2.fillStyle='rgba(255,255,255,.45)'; | |
| c2.textAlign='left';c2.fillText(lbl,lx+17,14); | |
| }); | |
| // Anatomical base tones per layer (light→dark top-to-bottom as in real OCT histology) | |
| const BASE=[[195,210,228],[172,188,208],[148,168,190],[125,148,172],[108,132,158],[88,114,142],[240,225,185],[210,172,115],[52,68,88]]; | |
| const anSt=c.an||{}; | |
| const BX=LBL+PAD,BW=W-LBL-STA-PAD*3; | |
| ALAYERS.forEach((l,i)=>{ | |
| const st=anSt[l.k]||'norm'; | |
| const y=HDR+i*(ROW+GAP); | |
| const[br,bg2,bb]=BASE[i]; | |
| // Row background (very subtle alternating) | |
| if(i%2===0){c2.fillStyle='rgba(255,255,255,.015)';c2.fillRect(0,y,W,ROW);} | |
| // Anatomical band — horizontal gradient | |
| const gr=c2.createLinearGradient(BX,y,BX+BW,y); | |
| gr.addColorStop(0,`rgba(${br},${bg2},${bb},0.55)`); | |
| gr.addColorStop(0.35,`rgba(${br},${bg2},${bb},0.82)`); | |
| gr.addColorStop(0.8,`rgba(${br},${bg2},${bb},0.68)`); | |
| gr.addColorStop(1,`rgba(${br},${bg2},${bb},0.32)`); | |
| c2.fillStyle=gr;c2.fillRect(BX,y,BW,ROW); | |
| // Status colour overlay (clearly visible) | |
| if(st==='aff'){ | |
| c2.fillStyle='rgba(220,40,40,0.55)';c2.fillRect(BX,y,BW,ROW); | |
| // subtle diagonal stripe texture for affected | |
| c2.save();c2.beginPath();c2.rect(BX,y,BW,ROW);c2.clip(); | |
| c2.strokeStyle='rgba(255,80,80,0.14)';c2.lineWidth=4; | |
| for(let sx=-ROW;sx<BW+ROW;sx+=14){c2.beginPath();c2.moveTo(BX+sx,y);c2.lineTo(BX+sx+ROW,y+ROW);c2.stroke();} | |
| c2.restore(); | |
| } else if(st==='mild'){ | |
| c2.fillStyle='rgba(200,120,10,0.45)';c2.fillRect(BX,y,BW,ROW); | |
| c2.save();c2.beginPath();c2.rect(BX,y,BW,ROW);c2.clip(); | |
| c2.strokeStyle='rgba(255,181,71,0.12)';c2.lineWidth=4; | |
| for(let sx=-ROW;sx<BW+ROW;sx+=14){c2.beginPath();c2.moveTo(BX+sx,y);c2.lineTo(BX+sx+ROW,y+ROW);c2.stroke();} | |
| c2.restore(); | |
| } | |
| // Bold 5px left status bar | |
| c2.fillStyle=ACOL[st];c2.fillRect(BX,y,5,ROW); | |
| // Top edge separator | |
| c2.fillStyle='rgba(0,0,0,0.45)';c2.fillRect(BX,y,BW,1); | |
| // Layer NAME — left column (right-aligned) | |
| c2.font=`${st!=='norm'?'bold ':''}11px "Space Grotesk",sans-serif`; | |
| c2.fillStyle=st==='aff'?'#FFCCCC':st==='mild'?'#FFE8B0':'rgba(255,255,255,0.75)'; | |
| c2.textAlign='right'; | |
| c2.fillText(l.n,LBL-2,y+ROW*0.5+4); | |
| // Abbreviation inside band (faint, on the bar itself) | |
| c2.font='bold 9px "Space Mono",monospace'; | |
| c2.fillStyle='rgba(255,255,255,0.28)'; | |
| c2.textAlign='left';c2.fillText(l.ab,BX+9,y+ROW*0.5+3.5); | |
| // Status BADGE — right column | |
| const SX=BX+BW+PAD,SBW=STA-PAD,SBH=Math.min(ROW-6,20),SBY=y+(ROW-SBH)/2; | |
| c2.fillStyle=st==='aff'?'rgba(255,60,60,0.25)':st==='mild'?'rgba(255,160,30,0.22)':'rgba(60,210,120,0.15)'; | |
| c2.beginPath();c2.roundRect?c2.roundRect(SX,SBY,SBW,SBH,4):c2.rect(SX,SBY,SBW,SBH);c2.fill(); | |
| c2.strokeStyle=ACOL[st];c2.lineWidth=0.9; | |
| c2.beginPath();c2.roundRect?c2.roundRect(SX,SBY,SBW,SBH,4):c2.rect(SX,SBY,SBW,SBH);c2.stroke(); | |
| c2.font='bold 8px "Space Mono",monospace';c2.fillStyle=ACOL[st]; | |
| c2.textAlign='center';c2.fillText(st==='aff'?'AFF':st==='mild'?'MILD':'NORM',SX+SBW/2,SBY+SBH/2+3); | |
| }); | |
| // Sub-choroidal label (bottom faint) | |
| c2.font='italic 9px "Space Grotesk",sans-serif';c2.fillStyle='rgba(255,255,255,.15)'; | |
| c2.textAlign='left';c2.fillText('SUB-CHOROIDAL',LBL+PAD,H-3); | |
| })(); | |
| const dl=document.getElementById('d1lst');if(dl){dl.innerHTML='';const an=c.an||{};const find={aff:{NFL:'Edema',GCL:'Cell loss',IPL:'Disrupted',INL:'Cystoid spaces',OPL:'Hard exudates',ONL:'Thinned',IS_OS:'Disrupted',RPE:'Detachment',CHOROID:'Neovascularization'},mild:{NFL:'Mild',GCL:'Mild',IPL:'Mild',INL:'Mild edema',OPL:'Mild',ONL:'Mild thinning',IS_OS:'Mild',RPE:'Irregular',CHOROID:'Mild'},norm:{NFL:'Normal',GCL:'Normal',IPL:'Normal',INL:'Normal',OPL:'Normal',ONL:'Normal',IS_OS:'Intact',RPE:'Smooth',CHOROID:'Normal'}};ALAYERS.forEach(l=>{const s=an[l.k]||'norm',f=(find[s]&&find[s][l.k])||'Normal';dl.innerHTML+=`<div class="anat-i ${s}"><div class="anat-d" style="background:${ACOL[s]}"></div><div class="anat-n">${l.n}</div><div class="anat-f">${f}</div><span class="anat-tag ${s}">${s.toUpperCase()}</span></div>`;});} | |
| const th=gct('d1th',null,140);if(th){const{c:c2,W,H}=th;c2.clearRect(0,0,W,H);const n=ALAYERS.length,bw=(W-20)/n/2,pd=6,ph=H-pd*2-16,mx=34,an=c.an||{};ALAYERS.forEach((l,i)=>{const norm=l.th,s=an[l.k]||'norm',mult=s==='aff'?1.42:s==='mild'?1.16:1.0,curr=norm*mult;const x=pd+i*(bw*2+3);c2.fillStyle='rgba(255,255,255,.15)';c2.fillRect(x,pd+ph-(norm/mx)*ph,bw,(norm/mx)*ph);const bcol=s==='aff'?'rgba(255,94,94,.65)':s==='mild'?'rgba(255,181,71,.55)':'rgba(0,201,184,.55)';c2.fillStyle=bcol;c2.fillRect(x+bw+2,pd+ph-(curr/mx)*ph,bw,(curr/mx)*ph);c2.font='6px Space Grotesk,sans-serif';c2.fillStyle='rgba(255,255,255,.3)';c2.textAlign='center';c2.fillText(l.ab,x+bw,H-2);});} | |
| // D2 - use real API images when available | |
| const o=gct('d2or');if(o){ | |
| if(window._apiOverlay){const im=new Image();im.onload=()=>{o.cv.width=o.W;o.cv.height=o.H;o.c.drawImage(im,0,0,o.W,o.H);};im.src=_b64(window._apiOverlay);} | |
| else drawOCT(o.cv,k); | |
| } | |
| const gc=gct('d2gc');if(gc){ | |
| if(window._apiGcam){const im=new Image();im.onload=()=>{gc.cv.width=gc.W;gc.cv.height=gc.H;gc.c.drawImage(im,0,0,gc.W,gc.H);};im.src=_b64(window._apiGcam);} | |
| else{drawOCT(gc.cv,k);const hm=getHM('gcam',k,gc.W,gc.H);const id=gc.c.createImageData(gc.W,gc.H);for(let i=0;i<gc.W*gc.H;i++){const[r,g,b]=jet(hm[i]);id.data[i*4]=r;id.data[i*4+1]=g;id.data[i*4+2]=b;id.data[i*4+3]=Math.round(hm[i]*.55*255);}const tmp=document.createElement('canvas');tmp.width=gc.W;tmp.height=gc.H;tmp.getContext('2d').putImageData(id,0,0);gc.c.drawImage(tmp,0,0);} | |
| } | |
| document.getElementById('d2ev').textContent=c.ev; | |
| const df=document.getElementById('d2ft');if(df)df.innerHTML=c.ft.map(f=>`<div style="display:flex;align-items:center;gap:6px;padding:5px 0;border-bottom:1px solid rgba(255,255,255,.05);font-size:12px;color:rgba(255,255,255,.7)"><div style="width:5px;height:5px;border-radius:50%;background:${d.sc};flex-shrink:0"></div>${f}</div>`).join(''); | |
| const dt=gct('d2th',null,120);if(dt){const{c:c2,W,H}=dt;c2.clearRect(0,0,W,H);const pd=6,pw=W-pd*2,ph=H-pd*2,conf=d.prob[ki];c2.strokeStyle='rgba(255,255,255,.05)';c2.lineWidth=.5;for(let i=0;i<=4;i++){const y=pd+i*ph/4;c2.beginPath();c2.moveTo(pd,y);c2.lineTo(W-pd,y);c2.stroke();}c2.beginPath();for(let i=0;i<=100;i++){const t=i/100,v=t<conf?conf:conf*Math.exp(-18*(t-conf));c2.lineTo(pd+t*pw,pd+ph-Math.min(v,.9999)*ph*.96);}c2.strokeStyle=d.sc;c2.lineWidth=1.8;c2.stroke();} | |
| // D3 | |
| document.getElementById('d3fi').textContent=c.fi;document.getElementById('d3pf').innerHTML=c.pf.split('\n').join('<br>');document.getElementById('d3rk').textContent=c.rk;document.getElementById('d3rc').textContent=c.rc; | |
| const pt=document.getElementById('d3pt');if(pt)pt.innerHTML=c.pt.map(p=>`<div class="tp-step"><div class="tp-ico" style="background:${p.u==='urgent'?'rgba(255,94,94,.2)':p.u==='moderate'?'rgba(255,181,71,.2)':'rgba(82,229,138,.15)'}">${p.i}</div><div><div class="tp-t">${p.t}<span class="upill u-${p.u}">${p.u.toUpperCase()}</span></div><div class="tp-d">${p.d}</div></div></div>`).join(''); | |
| }); | |
| } | |
| function setLyrView(v){ | |
| document.querySelectorAll('#rp4 .xtab').forEach((b,i)=>b.classList.toggle('on',i===(v==='h'?0:1))); | |
| const hv=document.getElementById('r4hView'),ev=document.getElementById('r4eView'); | |
| if(hv)hv.style.display=v==='h'?'':'none'; | |
| if(ev)ev.style.display=v==='e'?'':'none'; | |
| if(currKey)requestAnimationFrame(()=>renderR4(currKey,CLS.indexOf(currKey))); | |
| } | |
| // ═══ RESEARCH VIZ TABS ════════════════════════════════════════ | |
| function showResTab(n){ | |
| document.querySelectorAll('.res-tab').forEach((b,i)=>b.classList.toggle('on',i===n)); | |
| document.querySelectorAll('.res-content').forEach((c,i)=>c.classList.toggle('on',i===n)); | |
| } | |
| function toggleTheme() { | |
| document.documentElement.classList.toggle('light-theme'); | |
| const btn = document.getElementById('themeToggle'); | |
| if(btn) btn.textContent = document.documentElement.classList.contains('light-theme') ? 'Dark Theme' : 'Light Theme'; | |
| } | |
| // ═══ SCROLL FADE ═════════════════════════════════════════════ | |
| const io=new IntersectionObserver(e=>e.forEach(el=>{if(el.isIntersecting)el.target.classList.add('vis')}),{threshold:.1}); | |
| document.querySelectorAll('.sf').forEach(el=>io.observe(el)); | |
| // ═══ INIT ═════════════════════════════════════════════════════ | |
| document.addEventListener('DOMContentLoaded',()=>{ | |
| initSamples();initTabs();setMode('r'); | |
| // Init mode slider after layout settles | |
| requestAnimationFrame(()=>requestAnimationFrame(_initModeSlider)); | |
| window.addEventListener('resize',()=>{ | |
| _initModeSlider(); | |
| if(currKey)setTimeout(()=>renderAll(),120); | |
| }); | |
| checkApiHealth(); | |
| initJourney(); | |
| // ── Render Paper Confusion Matrix (static, from paper data) ── | |
| requestAnimationFrame(()=>{ | |
| const cv=document.getElementById('paperCM');if(!cv)return; | |
| const dpr=window.devicePixelRatio||1; | |
| const cssW=cv.parentElement?.offsetWidth||340; | |
| const cssH=Math.min(cssW,300); | |
| cv.width=Math.round(cssW*dpr);cv.height=Math.round(cssH*dpr); | |
| cv.style.width=cssW+'px';cv.style.height=cssH+'px'; | |
| const c=cv.getContext('2d');c.scale(dpr,dpr);c.clearRect(0,0,cssW,cssH); | |
| const vals=[[242,0,0,0],[0,242,0,0],[0,0,242,0],[0,0,1,241]]; | |
| const lbl=['CNV','DME','DRUSEN','NORMAL']; | |
| const pdL=62,pdT=34,pdR=10,pdB=42; | |
| const cellW=(cssW-pdL-pdR)/4,cellH=(cssH-pdT-pdB)/4; | |
| c.font='11px Space Grotesk,sans-serif';c.fillStyle='rgba(255,255,255,.5)'; | |
| c.textAlign='center';c.fillText('Predicted',pdL+(4*cellW)/2,cssH-8); | |
| c.save();c.translate(14,pdT+(4*cellH)/2);c.rotate(-Math.PI/2); | |
| c.fillText('Ground Truth',0,0);c.restore(); | |
| lbl.forEach((l,i)=>{ | |
| c.font='bold 10px Space Grotesk,sans-serif';c.fillStyle='rgba(255,255,255,.55)';c.textAlign='center'; | |
| c.fillText(l,pdL+i*cellW+cellW/2,pdT-10); | |
| }); | |
| lbl.forEach((l,i)=>{ | |
| c.font='bold 10px Space Grotesk,sans-serif';c.fillStyle='rgba(255,255,255,.55)';c.textAlign='right'; | |
| c.fillText(l,pdL-8,pdT+i*cellH+cellH/2+4); | |
| }); | |
| vals.forEach((row,r)=>row.forEach((v,col)=>{ | |
| const x=pdL+col*cellW,y=pdT+r*cellH; | |
| const isD=r===col;const total=242; | |
| const pct=(v/total*100).toFixed(1); | |
| c.fillStyle=isD?`rgba(0,201,184,${.35+v/total*.55})`:`rgba(255,255,255,${v>0?.12:.03})`; | |
| c.fillRect(x+1,y+1,cellW-2,cellH-2); | |
| if(isD){c.strokeStyle='rgba(0,201,184,.6)';c.lineWidth=1.5;c.strokeRect(x+1,y+1,cellW-2,cellH-2);} | |
| c.font='bold 15px Space Mono,monospace';c.textAlign='center'; | |
| c.fillStyle=isD?'#ffffff':v>0?'rgba(255,181,71,.9)':'rgba(255,255,255,.15)'; | |
| c.fillText(v,x+cellW/2,y+cellH/2); | |
| c.font='9px Space Grotesk,sans-serif'; | |
| c.fillStyle=isD?'rgba(255,255,255,.7)':'rgba(255,255,255,.3)'; | |
| c.fillText(pct+'%',x+cellW/2,y+cellH/2+15); | |
| })); | |
| // Accuracy label | |
| c.font='bold 10px Space Mono,monospace';c.fillStyle='#00C9B8';c.textAlign='right'; | |
| c.fillText('Accuracy: 967/968 = 99.90%',cssW-pdR,cssH-8); | |
| }); | |
| }); | |
| // ═══════════════════════════════════════════════════════════════ | |
| // NEURAL JOURNEY DUAL-MODE VIEWER | |
| // ═══════════════════════════════════════════════════════════════ | |
| const J_CAPTIONS = { | |
| CNV: { | |
| demo: 'CNV · Demo (RetViMNet) — ViT Blk 0→3→7→11 builds edge/texture hierarchy, then 4 Mamba blocks converge on subretinal neovascular membrane at RPE layer.', | |
| real: 'CNV · Real (ImprovedMedMamba 96.68%) — Larger 768-dim ViT-Base/16 backbone captures finer spatial detail; 2 MedMamba blocks lock onto the hyper-reflective CNV dome below RPE.' | |
| }, | |
| DME: { | |
| demo: 'DME · Demo (RetViMNet) — Mamba selective scan highlights cystoid intraretinal fluid pockets in INL/OPL. Attention overlays reveal bilateral distribution consistent with DME.', | |
| real: 'DME · Real (ImprovedMedMamba 96.68%) — High-capacity ViT-Base backbone resolves individual cyst boundaries; Mamba integration stage locks on central macular thickening.' | |
| }, | |
| DRUSEN: { | |
| demo: 'DRUSEN · Demo (RetViMNet) — Early ViT blocks detect RPE undulations; Mamba refinement stages build drusen-specific signature with periodic bumps along RPE baseline.', | |
| real: 'DRUSEN · Real (ImprovedMedMamba 96.68%) — 768-dim representations carry richer sub-RPE texture; final Mamba block produces a clean drusen attention map tightly bound to RPE.' | |
| }, | |
| NORMAL: { | |
| demo: 'NORMAL · Demo (RetViMNet) — Activation energy distributes diffusely across all retinal layers. Absence of focal hotspots confirms healthy homogeneous architecture.', | |
| real: 'NORMAL · Real (ImprovedMedMamba 96.68%) — Near-uniform Mamba attention across the full scan; IS/OS junction highlighted but no pathological concentration detected.' | |
| } | |
| }; | |
| let _jCls = 'CNV', _jMode = 'both'; | |
| let _rtCls = 'CNV', _rtMode = 'both'; | |
| function _jImgPath(mode, cls) { | |
| return `images/neural_journey_${mode}_${cls}.png`; | |
| } | |
| function _fadeSwap(imgEl, newSrc) { | |
| imgEl.classList.add('fading'); | |
| setTimeout(() => { | |
| imgEl.src = newSrc; | |
| imgEl.onload = () => imgEl.classList.remove('fading'); | |
| // fallback if already cached | |
| if (imgEl.complete) imgEl.classList.remove('fading'); | |
| }, 180); | |
| } | |
| /* ── Main journey section ─────────────────────────────────── */ | |
| function initJourney() { | |
| _jCls = 'CNV'; _jMode = 'both'; | |
| _renderJourney(); | |
| } | |
| function setJCls(cls) { | |
| _jCls = cls; | |
| document.querySelectorAll('.jcls-bar .jcls-btn').forEach(b => { | |
| b.classList.toggle('on', b.dataset.cls === cls); | |
| }); | |
| _renderJourney(); | |
| } | |
| function setJMode(mode) { | |
| _jMode = mode; | |
| ['demo','both','real'].forEach(m => { | |
| const btn = document.getElementById('jm' + m.charAt(0).toUpperCase() + m.slice(1)); | |
| if (btn) btn.classList.toggle('on', m === mode); | |
| }); | |
| _renderJourney(); | |
| } | |
| function _renderJourney() { | |
| const single = document.getElementById('jSingle'); | |
| const compare = document.getElementById('jCompare'); | |
| if (!single || !compare) return; | |
| // update stats row | |
| const archV = document.getElementById('jStatArchV'); | |
| const mambaV = document.getElementById('jStatMambaV'); | |
| const dimV = document.getElementById('jStatDimV'); | |
| if (_jMode === 'demo') { | |
| if (archV) archV.textContent = 'RetViMNet'; | |
| if (mambaV) mambaV.textContent = '4 Blocks'; | |
| if (dimV) dimV.textContent = '384-dim'; | |
| } else if (_jMode === 'real') { | |
| if (archV) archV.textContent = 'MedMamba'; | |
| if (mambaV) mambaV.textContent = '2 Blocks'; | |
| if (dimV) dimV.textContent = '768-dim'; | |
| } else { | |
| if (archV) archV.textContent = 'Demo vs Real'; | |
| if (mambaV) mambaV.textContent = '4 vs 2'; | |
| if (dimV) dimV.textContent = '384 vs 768'; | |
| } | |
| if (_jMode === 'both') { | |
| single.style.display = 'none'; | |
| compare.style.display = ''; | |
| _fadeSwap(document.getElementById('jDemoImg'), _jImgPath('demo', _jCls)); | |
| _fadeSwap(document.getElementById('jRealImg'), _jImgPath('real', _jCls)); | |
| const dc = document.getElementById('jDemoCaption'); | |
| const rc = document.getElementById('jRealCaption'); | |
| if (dc) dc.textContent = J_CAPTIONS[_jCls]?.demo || ''; | |
| if (rc) rc.textContent = J_CAPTIONS[_jCls]?.real || ''; | |
| } else { | |
| compare.style.display = 'none'; | |
| single.style.display = ''; | |
| const img = document.getElementById('jSingleImg'); | |
| const badge = document.getElementById('jSingleBadge'); | |
| const cap = document.getElementById('jSingleCaption'); | |
| if (img) _fadeSwap(img, _jImgPath(_jMode, _jCls)); | |
| if (badge) { badge.textContent = _jMode === 'demo' ? 'Demo Weights' : 'Real Weights · 96.68%'; | |
| badge.className = 'jbadge ' + _jMode; } | |
| if (cap) cap.textContent = J_CAPTIONS[_jCls]?.[_jMode] || ''; | |
| } | |
| } | |
| /* ── Research tab 0 mini-viewer ──────────────────────────── */ | |
| function rtSetCls(cls) { | |
| _rtCls = cls; | |
| document.querySelectorAll('#rtJcls .jcls-btn').forEach(b => { | |
| b.classList.toggle('on', b.dataset.cls === cls); | |
| }); | |
| _renderRtJourney(); | |
| } | |
| function rtSetMode(mode) { | |
| _rtMode = mode; | |
| ['demo','both','real'].forEach(m => { | |
| const btn = document.getElementById('rtm' + m.charAt(0).toUpperCase() + m.slice(1)); | |
| if (btn) btn.classList.toggle('on', m === mode); | |
| }); | |
| _renderRtJourney(); | |
| } | |
| function _renderRtJourney() { | |
| const single = document.getElementById('rtJSingle'); | |
| const compare = document.getElementById('rtJCompare'); | |
| if (!single || !compare) return; | |
| if (_rtMode === 'both') { | |
| single.style.display = 'none'; | |
| compare.style.display = ''; | |
| _fadeSwap(document.getElementById('rtDemoImg'), _jImgPath('demo', _rtCls)); | |
| // Use live API journey for Real side if available | |
| const rtReal = document.getElementById('rtRealImg'); | |
| if (window._apiJourney && rtReal) { | |
| rtReal.src = 'data:image/png;base64,' + window._apiJourney; | |
| } else { | |
| _fadeSwap(rtReal, _jImgPath('real', _rtCls)); | |
| } | |
| const dc = document.getElementById('rtDemoCap'); | |
| const rc = document.getElementById('rtRealCap'); | |
| if (dc) dc.textContent = J_CAPTIONS[_rtCls]?.demo || ''; | |
| if (rc) rc.textContent = window._apiJourney ? ('Live inference · ' + currKey) : (J_CAPTIONS[_rtCls]?.real || ''); | |
| } else { | |
| compare.style.display = 'none'; | |
| single.style.display = ''; | |
| const img = document.getElementById('rtSingleImg'); | |
| const badge = document.getElementById('rtSingleBadge'); | |
| const cap = document.getElementById('rtSingleCap'); | |
| // Use live API journey when in real mode and we have API data | |
| if (_rtMode === 'real' && window._apiJourney && img) { | |
| img.src = 'data:image/png;base64,' + window._apiJourney; | |
| img.classList.remove('fading'); | |
| } else { | |
| if (img) _fadeSwap(img, _jImgPath(_rtMode, _rtCls)); | |
| } | |
| if (badge) { | |
| badge.textContent = _rtMode === 'demo' ? 'Demo Weights' | |
| : (window._apiJourney ? 'Live · ' + currKey : 'Real · 96.68%'); | |
| badge.className = 'jbadge ' + _rtMode; | |
| } | |
| if (cap) cap.textContent = J_CAPTIONS[_rtCls]?.[_rtMode] || ''; | |
| } | |
| } | |
| // ── Active nav highlight on scroll ────────────────────────── | |
| (function(){ | |
| const navLinks = document.querySelectorAll('.nav-r a[href^="#"]'); | |
| const sections = [...navLinks].map(a=>{ | |
| const id = a.getAttribute('href').slice(1); | |
| return {el: document.getElementById(id), link: a}; | |
| }).filter(x=>x.el); | |
| const obs = new IntersectionObserver(entries=>{ | |
| entries.forEach(entry=>{ | |
| if(entry.isIntersecting){ | |
| navLinks.forEach(l=>l.classList.remove('nav-active')); | |
| const active = sections.find(s=>s.el===entry.target); | |
| if(active) active.link.classList.add('nav-active'); | |
| } | |
| }); | |
| },{rootMargin:'-40% 0px -55% 0px',threshold:0}); | |
| sections.forEach(s=>obs.observe(s.el)); | |
| })(); | |
| // ── Animate dataset bars on scroll ────────────────────────── | |
| (function(){ | |
| const bars = document.querySelectorAll('.ds-bar-fill[data-width]'); | |
| if(!bars.length) return; | |
| const obs = new IntersectionObserver(entries=>{ | |
| entries.forEach(e=>{ | |
| if(e.isIntersecting){ | |
| const el = e.target; | |
| el.style.width = el.dataset.width; | |
| obs.unobserve(el); | |
| } | |
| }); | |
| },{threshold:.3}); | |
| bars.forEach(b=>{obs.observe(b);}); | |
| })(); | |
| // ── Scroll-reveal for research sections ───────────────────── | |
| (function(){ | |
| const els = document.querySelectorAll('.rw-card,.xai-method-card,.disease-card,.gap-card,.method-step,.terminal-box'); | |
| const obs = new IntersectionObserver(entries=>{ | |
| entries.forEach(e=>{ | |
| if(e.isIntersecting){ | |
| e.target.style.animation='fadeInUp .5s ease both'; | |
| obs.unobserve(e.target); | |
| } | |
| }); | |
| },{threshold:.12}); | |
| els.forEach(el=>{el.style.opacity='0';obs.observe(el);}); | |
| })(); | |
| </script> | |
| </body> | |
| </html> | |