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} | |
| .tab-pane.on{display:block} | |
| @keyframes fu{from{opacity:0;transform:translateY(4px)}to{opacity:1;transform:none}} | |
| .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} | |
| </style> | |
| </head> | |
| <body> | |
| <nav> | |
| <a href="#" class="logo">Ret<b>ViM</b></a> | |
| <div class="nav-r"> | |
| <a href="#demo">Demo</a> | |
| <a href="#journey">Neural Journey</a> | |
| <a href="#visualizations">Research</a> | |
| <a href="#paper">Results</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="#demo" class="btn btn-primary">Deep Analysis Demo</a> | |
| <a href="#visualizations" class="btn btn-ghost">View Feature Maps</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 style="background:rgba(0,201,184,.04);border:1px solid rgba(0,201,184,.15);border-radius:14px;padding:24px 28px;line-height:1.8;font-size:14px;color:rgba(255,255,255,.6)" class="sf d3"> | |
| <strong style="color:#00C9B8;font-size:11px;letter-spacing:.08em;text-transform:uppercase;display:block;margin-bottom:8px">Abstract</strong> | |
| 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">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">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">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> | |
| <!-- ── 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 style="background:rgba(255,255,255,.02);border:1px solid rgba(255,255,255,.08);border-radius:18px;padding:32px 24px;overflow-x:auto"> | |
| <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 ── --> | |
| <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">Experimental Results · Kermany OCT 2017</div> | |
| <h2 class="sec-head sf d1">Benchmark<br><em>Performance</em></h2> | |
| <p class="sec-sub sf d2">All results on the test set (968 images, 242 per class), completely untouched during training and validation.</p> | |
| <!-- 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> | |
| <!-- ── 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">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"><div class="card-t" id="r1maptitle">GradCAM heatmap</div><canvas id="r1map" style="width:100%;border-radius:7px"></canvas></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> | |
| <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> | |
| <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:9px"><div class="card-t">Retinal layers — affected highlighted</div><canvas id="d1an" style="width:100%;border-radius:7px"></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> | |
| <footer style="background:#0D1117;border-top:1px solid rgba(255,255,255,.08)"> | |
| <a href="#" class="logo" style="text-decoration:none">Ret<b>ViM</b></a> | |
| <div class="fi">Sarvajanik College of Engineering and Technology · Surat, India<br>Kermany OCT 2017 · NVIDIA A100 · PyTorch BF16 · 2026</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)requestAnimationFrame(()=>renderR3(currKey)); | |
| if(i===4&&currKey)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); | |
| // 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 | |
| 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 — '+_file.name+' ✓ XAI ready'; | |
| }) | |
| .catch(()=>{_setTabsLoading(false);}); | |
| }) | |
| .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){ | |
| // XAI(1), CKA(2), Mamba(3), Layers(4) tabs get loading indicator | |
| 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'); | |
| } | |
| }); | |
| } | |
| 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 && !sameOrigin.includes('hf.space')) candidates.push(sameOrigin); | |
| 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'); | |
| if(mc&&apiSrc){ | |
| // Real PyTorch result available — draw it | |
| 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 { | |
| // No API result yet — canvas simulation fallback | |
| 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 | |
| const an=gct('d1an');if(an){const{c:c2,W,H,cv}=an;drawOCT(cv,k);const ps=[.20,.27,.33,.39,.44,.50,.60,.64,.76],hs=[.038,.028,.048,.038,.028,.065,.018,.028,.095];ALAYERS.forEach((l,i)=>{const s=d.an||c.an;const st=(s&&s[l.k])||'norm';const y0=H*ps[i],yh=H*hs[i];if(st==='aff'){c2.fillStyle='rgba(255,94,94,.25)';c2.fillRect(0,y0,W,yh);}else if(st==='mild'){c2.fillStyle='rgba(255,181,71,.18)';c2.fillRect(0,y0,W,yh);}c2.font='7px Space Grotesk,sans-serif';c2.textAlign='right';c2.fillStyle=ACOL[st];c2.fillText(l.ab,W-3,y0+yh*.72);});} | |
| 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] || ''; | |
| } | |
| } | |
| </script> | |
| </body> | |
| </html> | |