Spaces:
Sleeping
Sleeping
| import pathlib | |
| HTML = '''\ | |
| <!DOCTYPE html> | |
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"/> | |
| <meta name="viewport" content="width=device-width,initial-scale=1"/> | |
| <title>DocMind β AI Document Research</title> | |
| <style> | |
| /* ββββββββββββββββββββββββββββββββββββββββββ | |
| THEME VARIABLES | |
| ββββββββββββββββββββββββββββββββββββββββββ */ | |
| :root{ | |
| /* LIGHT theme β default */ | |
| color-scheme:light; | |
| --bg:#eef1f8;--surface:#ffffff;--card:#ffffff;--card2:#eaf0fb; | |
| --border:#dde2f0;--border2:#c6cedf; | |
| --text:#151829;--sub:#434970;--muted:#7882a0; | |
| --accent:#4a7cf7;--accent2:#2d5fe0; | |
| --green:#15924f;--red:#c93c3c;--teal:#0891b2;--gold:#c97a06;--purple:#6d28d9; | |
| /* surface overlays */ | |
| --inp-bg:rgba(0,0,0,.025);--hover-bg:rgba(0,0,0,.04); | |
| --tab-bg:rgba(0,0,0,.03);--trace-bg:rgba(0,0,0,.03); | |
| --shadow-sm:rgba(0,0,0,.08);--shadow-lg:rgba(0,0,0,.13); | |
| --step-border:rgba(0,0,0,.05);--menu-shadow:rgba(0,0,0,.14); | |
| } | |
| :root.dark{ | |
| color-scheme:dark; | |
| --bg:#0d0f1a;--surface:#13161f;--card:#181c27;--card2:#1e2230; | |
| --border:#252836;--border2:#2e3244; | |
| --text:#e8eaf2;--sub:#b0b8cc;--muted:#7880a0; | |
| --accent:#5b8ff9;--accent2:#3a6ee8; | |
| --green:#22d47a;--red:#f05c5c;--teal:#29c6d4;--gold:#f5a623;--purple:#a78bfa; | |
| --inp-bg:rgba(255,255,255,.04);--hover-bg:rgba(255,255,255,.05); | |
| --tab-bg:rgba(255,255,255,.03);--trace-bg:rgba(0,0,0,.22); | |
| --shadow-sm:rgba(0,0,0,.3);--shadow-lg:rgba(0,0,0,.5); | |
| --step-border:rgba(255,255,255,.04);--menu-shadow:rgba(0,0,0,.5); | |
| } | |
| *,*::before,*::after{box-sizing:border-box;margin:0;padding:0} | |
| html,body{height:100%} | |
| body{font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif; | |
| background:var(--bg);color:var(--text);font-size:14px;line-height:1.5; | |
| transition:background .25s,color .25s} | |
| /* ββ ANIMATIONS ββ */ | |
| @keyframes spin{to{transform:rotate(360deg)}} | |
| @keyframes fadeUp{from{opacity:0;transform:translateY(8px)}to{opacity:1;transform:none}} | |
| @keyframes fadeIn{from{opacity:0}to{opacity:1}} | |
| @keyframes pulse{0%,100%{opacity:1}50%{opacity:.35}} | |
| @keyframes shimmer{0%{background-position:-200% 0}100%{background-position:200% 0}} | |
| @keyframes nodeGlow{ | |
| 0%,100%{box-shadow:0 0 0 2px rgba(74,124,247,0)} | |
| 50%{box-shadow:0 0 0 2px rgba(74,124,247,.5),0 0 20px rgba(74,124,247,.25)} | |
| } | |
| @keyframes nodeGlowGreen{ | |
| 0%,100%{box-shadow:0 0 0 2px rgba(21,146,79,0)} | |
| 50%{box-shadow:0 0 0 2px rgba(21,146,79,.5),0 0 20px rgba(21,146,79,.25)} | |
| } | |
| @keyframes nodeGlowGold{ | |
| 0%,100%{box-shadow:0 0 0 2px rgba(201,122,6,0)} | |
| 50%{box-shadow:0 0 0 2px rgba(201,122,6,.5),0 0 20px rgba(201,122,6,.25)} | |
| } | |
| /* flowing dot along the arrow line */ | |
| @keyframes flowDot{ | |
| 0%{left:2px;opacity:0} | |
| 15%{opacity:1} | |
| 85%{opacity:1} | |
| 100%{left:calc(100% - 8px);opacity:0} | |
| } | |
| /* ββ HEADER ββ */ | |
| header{background:var(--surface);border-bottom:1px solid var(--border); | |
| padding:0 20px;height:52px;display:flex;align-items:center;gap:10px; | |
| position:sticky;top:0;z-index:100;transition:background .25s,border-color .25s} | |
| .logo{display:flex;align-items:center;gap:8px;text-decoration:none;flex-shrink:0} | |
| .logo-icon{width:28px;height:28px;background:linear-gradient(135deg,var(--accent),var(--purple)); | |
| border-radius:7px;display:flex;align-items:center;justify-content:center;font-size:14px} | |
| .logo-text{font-size:.95rem;font-weight:800;color:var(--text);letter-spacing:-.3px} | |
| .logo-text span{color:var(--accent)} | |
| .hdr-stack{display:flex;gap:5px;align-items:center;margin-left:8px} | |
| .hs{font-size:.62rem;font-weight:700;padding:2px 8px;border-radius:12px;border:1px solid; | |
| white-space:nowrap;letter-spacing:.02em} | |
| .hs-lg{background:rgba(74,124,247,.1);border-color:rgba(74,124,247,.3);color:var(--accent)} | |
| .hs-lc{background:rgba(201,122,6,.1);border-color:rgba(201,122,6,.3);color:var(--gold)} | |
| .hs-fb{background:rgba(8,145,178,.1);border-color:rgba(8,145,178,.3);color:var(--teal)} | |
| #hdr-src{margin-left:auto;font-size:.68rem;padding:3px 10px;border-radius:14px;flex-shrink:0; | |
| background:rgba(120,128,160,.08);border:1px solid var(--border);color:var(--muted)} | |
| #hdr-src.loaded{background:rgba(21,146,79,.08);border-color:rgba(21,146,79,.3);color:var(--green)} | |
| /* theme toggle button */ | |
| .theme-btn{margin-left:8px;background:var(--card2);border:1px solid var(--border2); | |
| border-radius:20px;padding:4px 10px;cursor:pointer;font-size:.75rem; | |
| color:var(--sub);display:flex;align-items:center;gap:5px; | |
| transition:all .2s;font-family:inherit;white-space:nowrap;flex-shrink:0} | |
| .theme-btn:hover{border-color:var(--accent);color:var(--text)} | |
| /* ββ LAYOUT ββ */ | |
| main{display:grid;grid-template-columns:290px 1fr;height:calc(100vh - 52px);overflow:hidden} | |
| .panel{padding:18px 16px;overflow-y:auto;height:100%} | |
| .panel-left{border-right:1px solid var(--border);background:var(--surface); | |
| display:flex;flex-direction:column;gap:20px;transition:background .25s,border-color .25s} | |
| .panel-right{background:var(--bg);transition:background .25s} | |
| /* ββ SECTION HEADERS ββ */ | |
| .sec-head{display:flex;align-items:center;gap:7px;margin-bottom:12px} | |
| .sec-icon{width:22px;height:22px;border-radius:6px;display:flex;align-items:center; | |
| justify-content:center;font-size:11px;flex-shrink:0} | |
| .si-blue{background:rgba(74,124,247,.15)} | |
| .si-purple{background:rgba(109,40,217,.15)} | |
| .si-gold{background:rgba(201,122,6,.15)} | |
| .sec-title{font-size:.67rem;font-weight:800;text-transform:uppercase;letter-spacing:.09em;color:var(--sub)} | |
| /* ββ TABS ββ */ | |
| .tabs{display:flex;gap:2px;background:var(--tab-bg);border-radius:8px; | |
| padding:3px;margin-bottom:14px;border:1px solid var(--border)} | |
| .tab-btn{flex:1;background:none;border:none;color:var(--muted);font-size:.75rem; | |
| font-weight:600;padding:6px 8px;border-radius:6px;cursor:pointer; | |
| transition:all .15s;font-family:inherit;display:flex;align-items:center; | |
| justify-content:center;gap:5px} | |
| .tab-btn.active{background:var(--card2);color:var(--text);box-shadow:0 1px 5px var(--shadow-sm)} | |
| /* ββ DROPZONE ββ */ | |
| .dropzone{border:2px dashed var(--border2);border-radius:10px;padding:22px 14px; | |
| text-align:center;cursor:pointer;transition:all .22s;position:relative;overflow:hidden} | |
| .dropzone::before{content:"";position:absolute;inset:0;opacity:0; | |
| background:linear-gradient(90deg,transparent,rgba(74,124,247,.07),transparent); | |
| background-size:200% 100%;transition:opacity .3s} | |
| .dropzone:hover::before,.dropzone.drag-over::before{opacity:1;animation:shimmer 1.6s linear infinite} | |
| .dropzone:hover,.dropzone.drag-over{border-color:var(--accent);background:rgba(74,124,247,.04)} | |
| .dz-icon{font-size:1.8rem;margin-bottom:6px;display:block;line-height:1} | |
| .dz-label{font-size:.8rem;color:var(--sub);margin-bottom:3px} | |
| .dz-label strong{color:var(--accent)} | |
| .dz-hint{font-size:.72rem;color:var(--muted)} | |
| /* ββ URL ββ */ | |
| .url-row{display:flex;gap:8px;margin-bottom:6px} | |
| input[type=url]{flex:1;background:var(--inp-bg);border:1px solid var(--border); | |
| border-radius:7px;padding:8px 11px;color:var(--text);font-size:.82rem; | |
| font-family:inherit;outline:none;transition:border-color .2s} | |
| input[type=url]:focus{border-color:var(--accent)} | |
| /* ββ SOURCE CARD ββ */ | |
| #source-card{display:none;background:rgba(21,146,79,.06);border:1px solid rgba(21,146,79,.22); | |
| border-radius:9px;padding:10px 12px;animation:fadeUp .3s ease;margin-top:10px} | |
| .sc-row{display:flex;align-items:center;gap:10px;margin-bottom:4px} | |
| .sc-icon{font-size:1.3rem;flex-shrink:0} | |
| .sc-name{font-size:.82rem;font-weight:700;color:var(--text);overflow:hidden; | |
| text-overflow:ellipsis;white-space:nowrap;max-width:190px} | |
| .sc-meta{font-size:.72rem;color:var(--teal)} | |
| .sc-ready{font-size:.72rem;color:var(--green);font-weight:600} | |
| /* ββ TECH STACK GRID ββ */ | |
| .tech-grid{display:grid;grid-template-columns:1fr 1fr;gap:5px} | |
| .tg{border-radius:7px;padding:7px 9px;border:1px solid;display:flex; | |
| align-items:center;gap:6px;transition:.15s} | |
| .tg:hover{transform:translateY(-1px)} | |
| .tg-icon{font-size:.85rem;flex-shrink:0} | |
| .tg-name{font-size:.69rem;font-weight:700;line-height:1.2} | |
| .tg-sub{font-size:.59rem;color:var(--muted);line-height:1.2} | |
| .tg-lg{background:rgba(74,124,247,.07);border-color:rgba(74,124,247,.2);color:var(--accent)} | |
| .tg-lc{background:rgba(201,122,6,.07);border-color:rgba(201,122,6,.2);color:var(--gold)} | |
| .tg-fb{background:rgba(8,145,178,.07);border-color:rgba(8,145,178,.2);color:var(--teal)} | |
| .tg-emb{background:rgba(21,146,79,.07);border-color:rgba(21,146,79,.2);color:var(--green)} | |
| .tg-fl{background:rgba(201,60,60,.07);border-color:rgba(201,60,60,.2);color:var(--red)} | |
| .tg-dk{background:rgba(6,182,212,.07);border-color:rgba(6,182,212,.2);color:#0891b2} | |
| /* βββββββββββββββββββββββββββββββββββββββββββββββ | |
| LIVE PIPELINE ARCHITECTURE DIAGRAM | |
| βββββββββββββββββββββββββββββββββββββββββββββββ */ | |
| .arch-card{background:var(--card);border:1px solid var(--border);border-radius:14px; | |
| padding:18px;margin-bottom:16px; | |
| box-shadow:0 2px 12px var(--shadow-sm); | |
| transition:background .25s,border-color .25s} | |
| .arch-card-hdr{display:flex;align-items:center;gap:8px;margin-bottom:16px} | |
| .arch-legend{display:flex;gap:10px;margin-left:auto;flex-wrap:wrap} | |
| .al-item{font-size:.61rem;color:var(--muted);display:flex;align-items:center;gap:4px;font-weight:600} | |
| .al-dot{width:8px;height:8px;border-radius:50%;flex-shrink:0} | |
| /* rows */ | |
| .arch-ingest-row{display:flex;align-items:center;gap:0;margin-bottom:6px;flex-wrap:nowrap; | |
| overflow-x:auto;padding-bottom:4px} | |
| .arch-pipe-row{display:flex;align-items:center;gap:0;flex-wrap:nowrap; | |
| overflow-x:auto;padding-bottom:4px} | |
| .arch-row-label{font-size:.61rem;font-weight:800;text-transform:uppercase; | |
| letter-spacing:.09em;color:var(--muted);margin-bottom:6px; | |
| display:flex;align-items:center;gap:5px} | |
| /* connector between rows */ | |
| .arch-connector{display:flex;align-items:center;gap:10px;margin:6px 0 8px 0;padding-left:2px} | |
| .arch-conn-line{width:2px;height:22px;background:linear-gradient(to bottom,var(--teal),var(--accent)); | |
| margin-left:44px;border-radius:2px} | |
| .arch-conn-label{font-size:.61rem;color:var(--muted);font-style:italic} | |
| /* ββ ARROW: proper CSS line + triangle arrowhead ββ */ | |
| .arch-arr{ | |
| position:relative; | |
| flex-shrink:0; | |
| width:34px;height:2px; | |
| background:var(--border2); | |
| align-self:center; | |
| margin:0 2px; | |
| border-radius:1px; | |
| transition:background .3s; | |
| } | |
| /* arrowhead triangle */ | |
| .arch-arr::before{ | |
| content:"";position:absolute; | |
| right:-7px;top:-4px; | |
| width:0;height:0; | |
| border-top:5px solid transparent; | |
| border-bottom:5px solid transparent; | |
| border-left:8px solid var(--border2); | |
| transition:border-left-color .3s; | |
| } | |
| /* flowing dot */ | |
| .arch-arr::after{ | |
| content:""; | |
| position:absolute; | |
| top:-4px;left:2px; | |
| width:8px;height:8px; | |
| border-radius:50%; | |
| background:var(--teal); | |
| box-shadow:0 0 7px var(--teal); | |
| opacity:0; | |
| } | |
| .arch-arr.arr-flowing{ | |
| background:var(--teal); | |
| box-shadow:0 0 4px rgba(8,145,178,.4); | |
| } | |
| .arch-arr.arr-flowing::before{border-left-color:var(--teal);} | |
| .arch-arr.arr-flowing::after{animation:flowDot .85s ease-in-out infinite;} | |
| /* node base */ | |
| .arch-node{ | |
| border-radius:10px;padding:9px 11px;border:1.5px solid;text-align:center; | |
| min-width:88px;transition:all .3s;position:relative;overflow:hidden;cursor:default; | |
| background:var(--card); | |
| flex-shrink:0; | |
| } | |
| /* subtle inner shine on hover */ | |
| .arch-node:hover{transform:translateY(-1px)} | |
| .arch-node-icon{font-size:1.1rem;display:block;margin-bottom:3px;line-height:1} | |
| .arch-node-name{font-size:.72rem;font-weight:800;color:var(--text);line-height:1.2} | |
| .arch-node-sub{font-size:.6rem;color:var(--muted);line-height:1.3;margin-top:2px} | |
| .arch-node-sub2{font-size:.56rem;color:var(--muted);line-height:1.3;margin-top:1px; | |
| opacity:.75;font-style:italic} | |
| /* node type colours */ | |
| .an-io {background:rgba(8,145,178,.06);border-color:rgba(8,145,178,.25)} | |
| .an-chunk{background:rgba(201,122,6,.06);border-color:rgba(201,122,6,.25)} | |
| .an-idx {background:rgba(8,145,178,.06);border-color:rgba(8,145,178,.25)} | |
| .an-llm {background:rgba(74,124,247,.06);border-color:rgba(74,124,247,.25)} | |
| .an-local{background:rgba(21,146,79,.06);border-color:rgba(21,146,79,.25)} | |
| .an-score{background:rgba(201,122,6,.06);border-color:rgba(201,122,6,.25)} | |
| .an-out {background:rgba(109,40,217,.06);border-color:rgba(109,40,217,.25)} | |
| /* ββ node state: running ββ */ | |
| .arch-node.running-llm{ | |
| border-width:2px;border-color:var(--accent); | |
| animation:nodeGlow .9s ease-in-out infinite; | |
| background:rgba(74,124,247,.1);} | |
| .arch-node.running-local{ | |
| border-width:2px;border-color:var(--green); | |
| animation:nodeGlowGreen .9s ease-in-out infinite; | |
| background:rgba(21,146,79,.1);} | |
| .arch-node.running-score,.arch-node.running-io, | |
| .arch-node.running-chunk,.arch-node.running-idx{ | |
| border-width:2px;border-color:var(--gold); | |
| animation:nodeGlowGold .9s ease-in-out infinite; | |
| background:rgba(201,122,6,.1);} | |
| /* ββ node state: done ββ */ | |
| .arch-node.node-done{opacity:.45;filter:saturate(.4)} | |
| .arch-node.node-done-ok{ | |
| border-color:rgba(21,146,79,.6)!important;border-width:2px!important; | |
| background:rgba(21,146,79,.06)!important;opacity:.9} | |
| .arch-node.node-done-ok::after{ | |
| content:"β";position:absolute;top:3px;right:5px; | |
| font-size:.62rem;color:var(--green);font-weight:900} | |
| .arch-node.node-err{ | |
| border-color:rgba(201,60,60,.55)!important;border-width:2px!important; | |
| background:rgba(201,60,60,.06)!important} | |
| /* ββ QUESTION CARD ββ */ | |
| .q-card{background:var(--card);border:1px solid var(--border);border-radius:10px; | |
| padding:14px;margin-bottom:14px; | |
| box-shadow:0 2px 8px var(--shadow-sm); | |
| transition:background .25s,border-color .25s} | |
| .q-label{font-size:.67rem;font-weight:700;color:var(--muted);text-transform:uppercase; | |
| letter-spacing:.07em;margin-bottom:8px} | |
| textarea{width:100%;background:var(--inp-bg);border:1px solid var(--border); | |
| border-radius:7px;padding:9px 11px;color:var(--text);font-size:.84rem; | |
| font-family:inherit;outline:none;resize:vertical;min-height:72px; | |
| transition:border-color .2s;line-height:1.5} | |
| textarea:focus{border-color:var(--accent)} | |
| .q-footer{display:flex;align-items:center;gap:8px;margin-top:10px} | |
| /* ββ MODEL DROPDOWN ββ */ | |
| .model-sel-wrap{position:relative} | |
| .model-sel-btn{display:inline-flex;align-items:center;gap:6px;padding:7px 11px; | |
| background:var(--card2);border:1.5px solid var(--border2);border-radius:7px; | |
| cursor:pointer;font-size:.76rem;font-weight:600;color:var(--sub); | |
| font-family:inherit;transition:all .15s;white-space:nowrap} | |
| .model-sel-btn:hover{border-color:rgba(74,124,247,.5);color:var(--text)} | |
| .msb-dot{width:8px;height:8px;border-radius:50%;flex-shrink:0} | |
| .msb-caret{font-size:.6rem;color:var(--muted);transition:transform .2s;line-height:1} | |
| .model-sel-wrap.open .msb-caret{transform:rotate(180deg)} | |
| .model-menu{display:none;position:absolute;bottom:calc(100% + 6px);left:0; | |
| min-width:260px;background:var(--card);border:1px solid var(--border2); | |
| border-radius:11px;padding:5px;z-index:300; | |
| box-shadow:0 8px 32px var(--menu-shadow);animation:fadeUp .15s ease} | |
| .model-sel-wrap.open .model-menu{display:block} | |
| .mm-header{font-size:.62rem;font-weight:800;text-transform:uppercase;letter-spacing:.08em; | |
| color:var(--muted);padding:6px 10px 4px} | |
| .mm-item{display:flex;align-items:center;gap:9px;padding:9px 10px;border-radius:7px; | |
| cursor:pointer;transition:.15s;position:relative} | |
| .mm-item:hover{background:var(--hover-bg)} | |
| .mm-item.selected{background:rgba(74,124,247,.08)} | |
| .mm-dot{width:9px;height:9px;border-radius:50%;flex-shrink:0} | |
| .mm-body{flex:1;min-width:0} | |
| .mm-name{font-size:.8rem;font-weight:700;color:var(--text);margin-bottom:1px} | |
| .mm-desc{font-size:.69rem;color:var(--muted)} | |
| .mm-right{display:flex;flex-direction:column;align-items:flex-end;gap:2px;flex-shrink:0} | |
| .mm-params{font-size:.65rem;font-weight:700;padding:1px 6px;border-radius:5px; | |
| background:var(--hover-bg);color:var(--sub)} | |
| .mm-speed{font-size:.65rem;color:var(--gold);letter-spacing:1px} | |
| .mm-check{position:absolute;right:10px;top:50%;transform:translateY(-50%); | |
| font-size:.75rem;color:var(--green);opacity:0;transition:opacity .15s} | |
| .mm-item.selected .mm-check{opacity:1} | |
| /* ββ BUTTONS ββ */ | |
| .btn{display:inline-flex;align-items:center;gap:6px;padding:8px 16px;border-radius:7px; | |
| border:none;font-size:.82rem;font-weight:600;cursor:pointer; | |
| transition:all .15s;font-family:inherit;white-space:nowrap} | |
| .btn-primary{background:linear-gradient(135deg,var(--accent),var(--accent2));color:#fff; | |
| box-shadow:0 2px 10px rgba(74,124,247,.3)} | |
| .btn-primary:hover{transform:translateY(-1px);box-shadow:0 4px 16px rgba(74,124,247,.4)} | |
| .btn-primary:disabled{opacity:.45;cursor:default;transform:none;box-shadow:none} | |
| .btn-sm{padding:6px 12px;font-size:.76rem} | |
| .btn-ghost{background:var(--hover-bg);color:var(--sub);border:1px solid var(--border)} | |
| .btn-ghost:hover{background:rgba(74,124,247,.07);color:var(--text)} | |
| .spinner{width:13px;height:13px;border:2px solid rgba(255,255,255,.25); | |
| border-top-color:#fff;border-radius:50%;animation:spin .7s linear infinite} | |
| /* ββ MESSAGES ββ */ | |
| .msg{border-radius:7px;padding:8px 12px;font-size:.78rem;margin-top:8px; | |
| line-height:1.5;animation:fadeUp .25s ease} | |
| .msg-ok{background:rgba(21,146,79,.08);border:1px solid rgba(21,146,79,.22);color:var(--green)} | |
| .msg-err{background:rgba(201,60,60,.08);border:1px solid rgba(201,60,60,.22);color:var(--red)} | |
| .msg-info{color:var(--muted);font-size:.74rem;margin-top:6px} | |
| #q-err{font-size:.76rem;color:var(--red)} | |
| /* ββ TRACE ββ */ | |
| #trace-wrap{display:none;margin-bottom:14px;animation:fadeUp .2s ease} | |
| .trace-hdr{display:flex;align-items:center;gap:8px;padding:8px 12px; | |
| background:var(--card);border:1px solid var(--border);border-radius:9px 9px 0 0} | |
| .trace-title{font-size:.7rem;font-weight:700;color:var(--muted); | |
| text-transform:uppercase;letter-spacing:.06em} | |
| #trace-log{background:var(--trace-bg);border:1px solid var(--border);border-top:none; | |
| border-radius:0 0 9px 9px;padding:8px 10px;max-height:200px;overflow-y:auto} | |
| .t-step{display:flex;align-items:flex-start;gap:8px;font-size:.75rem; | |
| padding:5px 0;border-bottom:1px solid var(--step-border); | |
| animation:fadeUp .2s ease} | |
| .t-step:last-child{border-bottom:none} | |
| .t-badge{font-size:.58rem;font-weight:800;text-transform:uppercase; | |
| padding:2px 6px;border-radius:4px;flex-shrink:0;margin-top:1px;letter-spacing:.03em} | |
| .b-planner {background:rgba(74,124,247,.15); color:var(--accent)} | |
| .b-retriever{background:rgba(8,145,178,.15); color:var(--teal)} | |
| .b-grader {background:rgba(201,122,6,.15); color:var(--gold)} | |
| .b-generator{background:rgba(21,146,79,.15); color:var(--green)} | |
| .b-critic {background:rgba(109,40,217,.15); color:var(--purple)} | |
| .b-error {background:rgba(201,60,60,.15); color:var(--red)} | |
| .t-msg{flex:1;color:var(--sub);line-height:1.45} | |
| .t-lat{color:var(--muted);font-size:.62rem;white-space:nowrap;margin-left:4px;opacity:.7} | |
| /* ββ ANSWER ββ */ | |
| #answer-wrap{display:none;background:var(--card);border:1px solid var(--border); | |
| border-radius:10px;overflow:hidden;animation:fadeUp .35s ease; | |
| box-shadow:0 2px 12px var(--shadow-sm)} | |
| .ans-header{display:flex;align-items:center;gap:10px;padding:12px 16px; | |
| border-bottom:1px solid var(--border)} | |
| .ans-label{font-size:.68rem;font-weight:800;text-transform:uppercase; | |
| letter-spacing:.07em;color:var(--muted)} | |
| .ans-actions{margin-left:auto} | |
| #answer-text{padding:16px;font-size:.88rem;line-height:1.75; | |
| white-space:pre-wrap;word-break:break-word;color:var(--text)} | |
| #verdict{margin:0 16px 12px;font-size:.72rem;font-weight:700; | |
| padding:4px 12px;border-radius:5px;display:inline-block} | |
| .v-ok {background:rgba(21,146,79,.1); color:var(--green)} | |
| .v-warn{background:rgba(201,122,6,.1); color:var(--gold)} | |
| @media(max-width:800px){ | |
| main{grid-template-columns:1fr;height:auto} | |
| .panel-left{border-right:none;border-bottom:1px solid var(--border)} | |
| .hdr-stack{display:none} | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <!-- ββββββββββββββββββββββββββββββββββββββββββ | |
| HEADER | |
| ββββββββββββββββββββββββββββββββββββββββββ --> | |
| <header> | |
| <a class="logo" href="#"> | |
| <div class="logo-icon">🧠</div> | |
| <span class="logo-text">Doc<span>Mind</span></span> | |
| </a> | |
| <div class="hdr-stack"> | |
| <span class="hs hs-lg">🔗 LangGraph</span> | |
| <span class="hs hs-lc">⛩ LangChain LCEL</span> | |
| <span class="hs hs-fb">🗃 FAISS+BM25+RRF</span> | |
| </div> | |
| <span id="hdr-src">No source loaded</span> | |
| <button class="theme-btn" id="theme-btn" onclick="toggleTheme()"> | |
| <span id="theme-icon">🌙</span> | |
| <span id="theme-label">Dark</span> | |
| </button> | |
| </header> | |
| <main> | |
| <!-- ββββββββββββββββββββββββββββββββββββββββββ | |
| LEFT PANEL | |
| ββββββββββββββββββββββββββββββββββββββββββ --> | |
| <div class="panel panel-left"> | |
| <!-- KNOWLEDGE BASE --> | |
| <div> | |
| <div class="sec-head"> | |
| <div class="sec-icon si-blue">📚</div> | |
| <span class="sec-title">Knowledge Base</span> | |
| </div> | |
| <div class="tabs"> | |
| <button class="tab-btn active" onclick="switchTab(this,\'pdf\')">📄 Upload PDF</button> | |
| <button class="tab-btn" onclick="switchTab(this,\'url\')">🌐 Paste URL</button> | |
| </div> | |
| <div id="tab-pdf"> | |
| <div class="dropzone" id="dz" | |
| onclick="document.getElementById(\'fi\').click()" | |
| ondragover="dg(event,true)" ondragleave="dg(event,false)" ondrop="dp(event)"> | |
| <span class="dz-icon">📄</span> | |
| <div class="dz-label"><strong>Click to browse</strong> or drag & drop</div> | |
| <div class="dz-hint">PDF only · max 10 MB</div> | |
| </div> | |
| <input type="file" id="fi" accept=".pdf" style="display:none" onchange="fc(event)"/> | |
| <div id="pdf-msg"></div> | |
| </div> | |
| <div id="tab-url" style="display:none"> | |
| <div class="url-row"> | |
| <input type="url" id="url-inp" placeholder="https://en.wikipedia.org/wiki/..." | |
| onkeydown="if(event.key===\'Enter\')fetchURL()"/> | |
| <button class="btn btn-primary btn-sm" id="url-btn" onclick="fetchURL()">Fetch</button> | |
| </div> | |
| <div class="msg-info" style="margin-bottom:4px">Wikipedia, gov sites & docs work best.</div> | |
| <div id="url-msg"></div> | |
| </div> | |
| <div id="source-card"> | |
| <div class="sc-row"> | |
| <div class="sc-icon" id="sc-icon">📄</div> | |
| <div> | |
| <div class="sc-name" id="source-name"></div> | |
| <div class="sc-meta" id="source-chunks"></div> | |
| </div> | |
| </div> | |
| <div class="sc-ready">✓ Ready for questions</div> | |
| </div> | |
| </div> | |
| <!-- TECH STACK --> | |
| <div> | |
| <div class="sec-head"> | |
| <div class="sec-icon si-purple">🛠</div> | |
| <span class="sec-title">Powered By</span> | |
| </div> | |
| <div class="tech-grid"> | |
| <div class="tg tg-lg"> | |
| <span class="tg-icon">🔗</span> | |
| <div><div class="tg-name">LangGraph 0.2</div><div class="tg-sub">StateGraph · 5 nodes</div></div> | |
| </div> | |
| <div class="tg tg-lc"> | |
| <span class="tg-icon">⛩</span> | |
| <div><div class="tg-name">LangChain LCEL</div><div class="tg-sub">prompt | llm | parser</div></div> | |
| </div> | |
| <div class="tg tg-fb"> | |
| <span class="tg-icon">🗃</span> | |
| <div><div class="tg-name">FAISS + BM25</div><div class="tg-sub">RRF hybrid retrieval</div></div> | |
| </div> | |
| <div class="tg tg-emb"> | |
| <span class="tg-icon">🪘</span> | |
| <div><div class="tg-name">HF Embeddings</div><div class="tg-sub">bge-small-en-v1.5</div></div> | |
| </div> | |
| <div class="tg tg-fl"> | |
| <span class="tg-icon">🆕</span> | |
| <div><div class="tg-name">Flask 3.1</div><div class="tg-sub">+ Gunicorn WSGI</div></div> | |
| </div> | |
| <div class="tg tg-dk"> | |
| <span class="tg-icon">🐺</span> | |
| <div><div class="tg-name">Docker</div><div class="tg-sub">HuggingFace Spaces</div></div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- ββββββββββββββββββββββββββββββββββββββββββ | |
| RIGHT PANEL | |
| ββββββββββββββββββββββββββββββββββββββββββ --> | |
| <div class="panel panel-right"> | |
| <!-- ββ LIVE PIPELINE ARCHITECTURE DIAGRAM ββ --> | |
| <div class="arch-card"> | |
| <div class="arch-card-hdr"> | |
| <div class="sec-icon si-blue" style="width:20px;height:20px;font-size:10px">📊</div> | |
| <span class="sec-title">Live Pipeline</span> | |
| <div class="arch-legend"> | |
| <span class="al-item"><span class="al-dot" style="background:var(--accent)"></span>LLM Agent</span> | |
| <span class="al-item"><span class="al-dot" style="background:var(--green)"></span>Local</span> | |
| <span class="al-item"><span class="al-dot" style="background:var(--gold)"></span>Score & I/O</span> | |
| </div> | |
| </div> | |
| <!-- Ingestion row --> | |
| <div class="arch-row-label">💾 Ingestion</div> | |
| <div class="arch-ingest-row"> | |
| <div class="arch-node an-io" id="anode-source"> | |
| <span class="arch-node-icon">📄</span> | |
| <div class="arch-node-name">Source</div> | |
| <div class="arch-node-sub">PDF · URL</div> | |
| <div class="arch-node-sub2">PyPDF / BeautifulSoup</div> | |
| </div> | |
| <div class="arch-arr" id="iarr-0"></div> | |
| <div class="arch-node an-chunk" id="anode-chunker"> | |
| <span class="arch-node-icon">✂</span> | |
| <div class="arch-node-name">Chunker</div> | |
| <div class="arch-node-sub">RecursiveTextSplitter</div> | |
| <div class="arch-node-sub2">1500 ch · 200 overlap</div> | |
| </div> | |
| <div class="arch-arr" id="iarr-1"></div> | |
| <div class="arch-node an-idx" id="anode-index"> | |
| <span class="arch-node-icon">🗃</span> | |
| <div class="arch-node-name">Hybrid Index</div> | |
| <div class="arch-node-sub">FAISS + BM25</div> | |
| <div class="arch-node-sub2">768-dim · RRF k=60</div> | |
| </div> | |
| <span style="font-size:.62rem;color:var(--teal);margin-left:10px;align-self:center;white-space:nowrap"> | |
| ↑ feeds Retriever | |
| </span> | |
| </div> | |
| <!-- Connector --> | |
| <div class="arch-connector"> | |
| <div class="arch-conn-line"></div> | |
| <span class="arch-conn-label">LangGraph StateGraph β research pipeline</span> | |
| </div> | |
| <!-- Agent pipeline row --> | |
| <div class="arch-row-label">🤖 Research Agents</div> | |
| <div class="arch-pipe-row"> | |
| <div class="arch-node an-llm" data-arch="planner" id="anode-planner"> | |
| <span class="arch-node-icon">🎯</span> | |
| <div class="arch-node-name">Planner</div> | |
| <div class="arch-node-sub">LLM · temp 0.3</div> | |
| <div class="arch-node-sub2">Research plan · 200 tok</div> | |
| </div> | |
| <div class="arch-arr" id="parr-0"></div> | |
| <div class="arch-node an-local" data-arch="retriever" id="anode-retriever"> | |
| <span class="arch-node-icon">🔍</span> | |
| <div class="arch-node-name">Retriever</div> | |
| <div class="arch-node-sub">FAISS + BM25</div> | |
| <div class="arch-node-sub2">top-k 5 · RRF fusion</div> | |
| </div> | |
| <div class="arch-arr" id="parr-1"></div> | |
| <div class="arch-node an-score" data-arch="grader" id="anode-grader"> | |
| <span class="arch-node-icon">⚖</span> | |
| <div class="arch-node-name">Grader</div> | |
| <div class="arch-node-sub">Score · ~1 ms</div> | |
| <div class="arch-node-sub2">0.7Γvec + 0.3Γkw</div> | |
| </div> | |
| <div class="arch-arr" id="parr-2"></div> | |
| <div class="arch-node an-llm" data-arch="generator" id="anode-generator"> | |
| <span class="arch-node-icon">✍</span> | |
| <div class="arch-node-name">Generator</div> | |
| <div class="arch-node-sub">LLM · temp 0.4</div> | |
| <div class="arch-node-sub2">Cited answer · 512 tok</div> | |
| </div> | |
| <div class="arch-arr" id="parr-3"></div> | |
| <div class="arch-node an-llm" data-arch="critic" id="anode-critic"> | |
| <span class="arch-node-icon">🔬</span> | |
| <div class="arch-node-name">Critic</div> | |
| <div class="arch-node-sub">LLM · temp 0.1</div> | |
| <div class="arch-node-sub2">APPROVED / NEEDS‑REVIEW</div> | |
| </div> | |
| <div class="arch-arr" id="parr-4"></div> | |
| <div class="arch-node an-out" id="anode-answer"> | |
| <span class="arch-node-icon">📋</span> | |
| <div class="arch-node-name">Answer</div> | |
| <div class="arch-node-sub">Cited · Verified</div> | |
| <div class="arch-node-sub2">inline [Source, p.N]</div> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- ββ RESEARCH QUESTION ββ --> | |
| <div class="sec-head"> | |
| <div class="sec-icon si-purple">🔍</div> | |
| <span class="sec-title">Research Query</span> | |
| </div> | |
| <div class="q-card"> | |
| <div class="q-label">Your Question</div> | |
| <textarea id="q-inp" rows="3" | |
| placeholder="Ask anything about the loaded document or URL Press Enter to submit · Shift+Enter for new line" | |
| onkeydown="qk(event)"></textarea> | |
| <div class="q-footer"> | |
| <button class="btn btn-primary" id="ask-btn" onclick="ask()">⚡ Ask</button> | |
| <!-- ββ MODEL DROPDOWN ββ --> | |
| <div class="model-sel-wrap" id="model-sel-wrap"> | |
| <button class="model-sel-btn" id="model-sel-btn" onclick="toggleMenu(event)"> | |
| <span class="msb-dot" id="msb-dot" style="background:#4a7cf7"></span> | |
| <span id="msb-label">Qwen 2.5·7B</span> | |
| <span class="msb-caret">▼</span> | |
| </button> | |
| <div class="model-menu" id="model-menu"> | |
| <div class="mm-header">Select LLM Model</div> | |
| <div class="mm-item selected" data-key="qwen-7b" data-color="#4a7cf7" | |
| data-label="Qwen 2.5·7B" onclick="pickModel(this)"> | |
| <span class="mm-dot" style="background:#4a7cf7"></span> | |
| <div class="mm-body"> | |
| <div class="mm-name">Qwen 2.5 · 7B</div> | |
| <div class="mm-desc">Default · fast & free</div> | |
| </div> | |
| <div class="mm-right"> | |
| <span class="mm-params">7B</span> | |
| <span class="mm-speed">⚡⚡⚡</span> | |
| </div> | |
| <span class="mm-check">✓</span> | |
| </div> | |
| <div class="mm-item" data-key="mistral-nemo" data-color="#6d28d9" | |
| data-label="Mistral Nemo·12B" onclick="pickModel(this)"> | |
| <span class="mm-dot" style="background:#6d28d9"></span> | |
| <div class="mm-body"> | |
| <div class="mm-name">Mistral Nemo · 12B</div> | |
| <div class="mm-desc">Stronger reasoning</div> | |
| </div> | |
| <div class="mm-right"> | |
| <span class="mm-params">12B</span> | |
| <span class="mm-speed">⚡⚡</span> | |
| </div> | |
| <span class="mm-check">✓</span> | |
| </div> | |
| <div class="mm-item" data-key="phi-3-mini" data-color="#15924f" | |
| data-label="Phi-3.5 Mini·3.8B" onclick="pickModel(this)"> | |
| <span class="mm-dot" style="background:#15924f"></span> | |
| <div class="mm-body"> | |
| <div class="mm-name">Phi-3.5 Mini · 3.8B</div> | |
| <div class="mm-desc">Ultra-fast & focused</div> | |
| </div> | |
| <div class="mm-right"> | |
| <span class="mm-params">3.8B</span> | |
| <span class="mm-speed">⚡⚡⚡</span> | |
| </div> | |
| <span class="mm-check">✓</span> | |
| </div> | |
| </div> | |
| </div> | |
| <span id="q-err"></span> | |
| </div> | |
| </div> | |
| <!-- ββ TRACE LOG ββ --> | |
| <div id="trace-wrap"> | |
| <div class="trace-hdr"> | |
| <span class="trace-title">📰 Agent Trace</span> | |
| </div> | |
| <div id="trace-log"></div> | |
| </div> | |
| <!-- ββ ANSWER ββ --> | |
| <div id="answer-wrap"> | |
| <div class="ans-header"> | |
| <span class="ans-label">🧠 Answer</span> | |
| <div class="ans-actions"> | |
| <button class="btn btn-ghost btn-sm" onclick="copyAns(this)">📋 Copy</button> | |
| </div> | |
| </div> | |
| <div id="answer-text"></div> | |
| <div id="verdict"></div> | |
| </div> | |
| </div> | |
| </main> | |
| <script> | |
| const esc=s=>String(s).replace(/&/g,"&").replace(/</g,"<").replace(/>/g,">"); | |
| let pollTimer=null,seen=0,selectedModel="qwen-7b"; | |
| /* ββββββββββββββββββββββββββββββββββββββββββββ | |
| THEME TOGGLE | |
| ββββββββββββββββββββββββββββββββββββββββββββ */ | |
| (function initTheme(){ | |
| const saved=localStorage.getItem("dm-theme"); | |
| if(saved==="dark"){ | |
| document.documentElement.classList.add("dark"); | |
| document.getElementById("theme-icon").textContent="βοΈ"; | |
| document.getElementById("theme-label").textContent="Light"; | |
| } | |
| })(); | |
| function toggleTheme(){ | |
| const html=document.documentElement; | |
| const isDark=html.classList.toggle("dark"); | |
| document.getElementById("theme-icon").textContent=isDark?"βοΈ":"π"; | |
| document.getElementById("theme-label").textContent=isDark?"Light":"Dark"; | |
| localStorage.setItem("dm-theme",isDark?"dark":"light"); | |
| } | |
| /* ββ Tab switching ββββββββββββββββββββββββββββββββββββββββββββββββββββββββ */ | |
| function switchTab(btn,name){ | |
| document.querySelectorAll(".tab-btn").forEach(b=>b.classList.remove("active")); | |
| btn.classList.add("active"); | |
| document.getElementById("tab-pdf").style.display=name==="pdf"?"":"none"; | |
| document.getElementById("tab-url").style.display=name==="url"?"":"none"; | |
| } | |
| /* ββ Drag & drop ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ */ | |
| function dg(e,over){e.preventDefault();document.getElementById("dz").classList[over?"add":"remove"]("drag-over");} | |
| function dp(e){e.preventDefault();document.getElementById("dz").classList.remove("drag-over");const f=e.dataTransfer.files[0];if(f)up(f);} | |
| function fc(e){if(e.target.files[0])up(e.target.files[0]);} | |
| /* ββββββββββββββββββββββββββββββββββββββββββββ | |
| INGESTION PIPELINE ANIMATION | |
| ββββββββββββββββββββββββββββββββββββββββββββ */ | |
| const INGEST_NODES=[ | |
| {id:"anode-source", cls:"running-io"}, | |
| {id:"anode-chunker", cls:"running-chunk"}, | |
| {id:"anode-index", cls:"running-idx"}, | |
| ]; | |
| const INGEST_ARRS=["iarr-0","iarr-1"]; | |
| let _ingestTimers=[]; | |
| function ingestStart(){ | |
| ingestReset(); | |
| INGEST_NODES.forEach((n,i)=>{ | |
| _ingestTimers.push(setTimeout(()=>{ | |
| if(i>0){ | |
| const prev=document.getElementById(INGEST_NODES[i-1].id); | |
| prev.classList.remove(INGEST_NODES[i-1].cls); | |
| prev.classList.add("node-done"); | |
| if(i-1<INGEST_ARRS.length) | |
| document.getElementById(INGEST_ARRS[i-1]).classList.remove("arr-flowing"); | |
| } | |
| const el=document.getElementById(n.id); | |
| if(el)el.classList.add(n.cls); | |
| if(i<INGEST_ARRS.length) | |
| document.getElementById(INGEST_ARRS[i]).classList.add("arr-flowing"); | |
| },i*700)); | |
| }); | |
| } | |
| function ingestDone(){ | |
| _ingestTimers.forEach(clearTimeout);_ingestTimers=[]; | |
| INGEST_NODES.forEach(n=>{ | |
| const el=document.getElementById(n.id); | |
| if(el){el.classList.remove(n.cls,"node-done","node-err");el.classList.add("node-done-ok");} | |
| }); | |
| INGEST_ARRS.forEach(id=>document.getElementById(id).classList.remove("arr-flowing")); | |
| } | |
| function ingestErr(){ | |
| _ingestTimers.forEach(clearTimeout);_ingestTimers=[]; | |
| INGEST_NODES.forEach(n=>{ | |
| const el=document.getElementById(n.id); | |
| if(el){el.classList.remove(n.cls,"node-done","node-done-ok");el.classList.add("node-err");} | |
| }); | |
| INGEST_ARRS.forEach(id=>document.getElementById(id).classList.remove("arr-flowing")); | |
| } | |
| function ingestReset(){ | |
| _ingestTimers.forEach(clearTimeout);_ingestTimers=[]; | |
| INGEST_NODES.forEach(n=>{ | |
| const el=document.getElementById(n.id); | |
| if(el)el.className=el.className.replace(/running-\\S+|node-done-ok|node-done|node-err/g,"").trim(); | |
| }); | |
| INGEST_ARRS.forEach(id=>document.getElementById(id).classList.remove("arr-flowing")); | |
| } | |
| /* ββ PDF upload βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ */ | |
| async function up(file){ | |
| if(!file.name.toLowerCase().endsWith(".pdf")){sm("pdf-msg","error","Only PDF files are supported.");return;} | |
| sm("pdf-msg","info","Uploading "+file.name+"β¦"); | |
| ingestStart(); | |
| const fd=new FormData();fd.append("file",file); | |
| try{ | |
| const r=await fetch("/api/upload",{method:"POST",body:fd}); | |
| const d=await r.json(); | |
| if(d.error){ingestErr();sm("pdf-msg","error",d.error);return;} | |
| ingestDone(); | |
| setSource(d.filename,d.chunks,"pdf"); | |
| sm("pdf-msg","ok","✓ Indexed "+d.chunks+" chunks from \\""+d.filename+"\\""); | |
| }catch(e){ingestErr();sm("pdf-msg","error","Upload failed: "+e.message);} | |
| } | |
| /* ββ URL fetch ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ */ | |
| async function fetchURL(){ | |
| const url=document.getElementById("url-inp").value.trim(); | |
| if(!url){sm("url-msg","error","Please enter a URL.");return;} | |
| document.getElementById("url-btn").disabled=true; | |
| sm("url-msg","info","Fetching pageβ¦"); | |
| ingestStart(); | |
| try{ | |
| const r=await fetch("/api/ingest_url",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({url})}); | |
| const d=await r.json(); | |
| if(d.error){ingestErr();sm("url-msg","error",d.error);return;} | |
| ingestDone(); | |
| setSource(d.url,d.chunks,"url"); | |
| sm("url-msg","ok","✓ Indexed "+d.chunks+" chunks"); | |
| }catch(e){ingestErr();sm("url-msg","error","Failed: "+e.message);} | |
| finally{document.getElementById("url-btn").disabled=false;} | |
| } | |
| /* ββ Source card ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ */ | |
| function setSource(name,chunks,type){ | |
| document.getElementById("source-name").textContent=name; | |
| document.getElementById("source-chunks").textContent=chunks+" chunks indexed"; | |
| document.getElementById("sc-icon").textContent=type==="pdf"?"π":"π"; | |
| document.getElementById("source-card").style.display="block"; | |
| const p=document.getElementById("hdr-src"); | |
| p.textContent=name.length>26?name.slice(0,26)+"β¦":name; | |
| p.classList.add("loaded"); | |
| } | |
| /* ββββββββββββββββββββββββββββββββββββββββββββ | |
| MODEL DROPDOWN | |
| ββββββββββββββββββββββββββββββββββββββββββββ */ | |
| function toggleMenu(e){ | |
| e.stopPropagation(); | |
| document.getElementById("model-sel-wrap").classList.toggle("open"); | |
| } | |
| function pickModel(item){ | |
| const key=item.dataset.key,color=item.dataset.color,label=item.dataset.label; | |
| document.querySelectorAll(".mm-item").forEach(i=>i.classList.remove("selected")); | |
| item.classList.add("selected"); | |
| selectedModel=key; | |
| document.getElementById("msb-dot").style.background=color; | |
| document.getElementById("msb-label").textContent=label; | |
| document.getElementById("model-sel-wrap").classList.remove("open"); | |
| fetch("/api/set_model",{method:"POST",headers:{"Content-Type":"application/json"}, | |
| body:JSON.stringify({model:key})}).catch(()=>{}); | |
| } | |
| document.addEventListener("click",e=>{ | |
| const w=document.getElementById("model-sel-wrap"); | |
| if(w&&!w.contains(e.target))w.classList.remove("open"); | |
| }); | |
| /* ββββββββββββββββββββββββββββββββββββββββββββ | |
| RESEARCH PIPELINE ANIMATION | |
| ββββββββββββββββββββββββββββββββββββββββββββ */ | |
| const AGENT_TYPES={ | |
| planner:"running-llm",retriever:"running-local", | |
| grader:"running-score",generator:"running-llm",critic:"running-llm" | |
| }; | |
| const PIPE_ARRS=["parr-0","parr-1","parr-2","parr-3","parr-4"]; | |
| function pipeReset(){ | |
| Object.keys(AGENT_TYPES).forEach(a=>{ | |
| const n=document.getElementById("anode-"+a); | |
| if(n)n.className=n.className.replace(/running-\\S+|node-done-ok|node-done|node-err/g,"").trim(); | |
| }); | |
| const out=document.getElementById("anode-answer"); | |
| if(out)out.className=out.className.replace(/running-\\S+|node-done-ok|node-done/g,"").trim(); | |
| PIPE_ARRS.forEach(id=>document.getElementById(id).classList.remove("arr-flowing")); | |
| } | |
| function pipeSetActive(agent){ | |
| const agents=Object.keys(AGENT_TYPES); | |
| agents.forEach((a,idx)=>{ | |
| const n=document.getElementById("anode-"+a);if(!n)return; | |
| if(a===agent){ | |
| n.className=n.className.replace(/running-\\S+|node-done-ok|node-done/g,"").trim(); | |
| n.classList.add(AGENT_TYPES[a]); | |
| if(idx<PIPE_ARRS.length)document.getElementById(PIPE_ARRS[idx]).classList.add("arr-flowing"); | |
| }else if(n.className.includes("running-")){ | |
| n.className=n.className.replace(/running-\\S+/g,"").trim(); | |
| n.classList.add("node-done-ok"); | |
| if(idx<PIPE_ARRS.length)document.getElementById(PIPE_ARRS[idx]).classList.remove("arr-flowing"); | |
| } | |
| }); | |
| } | |
| function pipeAllDone(){ | |
| Object.keys(AGENT_TYPES).forEach(a=>{ | |
| const n=document.getElementById("anode-"+a); | |
| if(n){n.className=n.className.replace(/running-\\S+/g,"").trim();n.classList.add("node-done-ok");} | |
| }); | |
| PIPE_ARRS.forEach(id=>document.getElementById(id).classList.remove("arr-flowing")); | |
| const out=document.getElementById("anode-answer"); | |
| if(out){ | |
| out.classList.add("running-llm"); | |
| setTimeout(()=>{out.classList.remove("running-llm");out.classList.add("node-done-ok");},1200); | |
| } | |
| } | |
| /* ββ Ask ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ */ | |
| function qk(e){if(e.key==="Enter"&&!e.shiftKey){e.preventDefault();ask();}} | |
| async function ask(){ | |
| const q=document.getElementById("q-inp").value.trim(); | |
| document.getElementById("q-err").textContent=""; | |
| if(!q){document.getElementById("q-err").textContent="Please enter a question.";return;} | |
| const btn=document.getElementById("ask-btn"); | |
| btn.disabled=true; | |
| btn.innerHTML=\'<span class="spinner"></span> Thinkingβ¦\'; | |
| document.getElementById("trace-wrap").style.display="block"; | |
| document.getElementById("trace-log").innerHTML= | |
| \'<div class="t-step"><span class="t-msg" style="color:var(--muted)">Starting agentsβ¦</span></div>\'; | |
| document.getElementById("answer-wrap").style.display="none"; | |
| pipeReset();seen=0;clearInterval(pollTimer); | |
| try{ | |
| const r=await fetch("/api/research",{ | |
| method:"POST",headers:{"Content-Type":"application/json"}, | |
| body:JSON.stringify({question:q,model:selectedModel})}); | |
| const d=await r.json(); | |
| if(d.error){traceErr(d.error);resetBtn();return;} | |
| pollTimer=setInterval(()=>poll(d.query_id),1500); | |
| }catch(e){traceErr("Network error: "+e.message);resetBtn();} | |
| } | |
| function resetBtn(){ | |
| const btn=document.getElementById("ask-btn"); | |
| btn.disabled=false;btn.innerHTML=\'⚡ Ask\'; | |
| } | |
| /* ββ Polling ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ */ | |
| async function poll(qid){ | |
| try{ | |
| const r=await fetch("/api/trace/"+qid); | |
| if(!r.ok){traceErr("Server error "+r.status);clearInterval(pollTimer);resetBtn();return;} | |
| const d=await r.json(); | |
| renderTrace(d.trace||[]); | |
| if(["complete","error"].includes(d.status)){ | |
| clearInterval(pollTimer);resetBtn(); | |
| if(d.status==="complete"&&d.result){renderAnswer(d.result);pipeAllDone();} | |
| else if(d.status==="error"&&d.result)traceErr(d.result.error||"An error occurred."); | |
| } | |
| }catch(e){traceErr("Poll error: "+e.message);clearInterval(pollTimer);resetBtn();} | |
| } | |
| function traceErr(msg){ | |
| const log=document.getElementById("trace-log"); | |
| log.innerHTML+=\'<div class="t-step"><span class="t-badge b-error">error</span><span class="t-msg" style="color:var(--red)">\'+esc(msg)+\'</span></div>\'; | |
| log.scrollTop=log.scrollHeight; | |
| } | |
| function renderTrace(steps){ | |
| if(!steps.length)return; | |
| const log=document.getElementById("trace-log"); | |
| if(seen===0)log.innerHTML=""; | |
| for(let i=seen;i<steps.length;i++){ | |
| const s=steps[i]; | |
| pipeSetActive(s.agent); | |
| const lat=s.latency_ms>0?\'<span class="t-lat">\'+s.latency_ms+\'ms</span>\':""; | |
| log.innerHTML+=\'<div class="t-step"><span class="t-badge b-\'+s.agent+\'">\'+s.agent+ | |
| \'</span><span class="t-msg">\'+esc(s.message)+\'</span>\'+lat+\'</div>\'; | |
| } | |
| seen=steps.length;log.scrollTop=log.scrollHeight; | |
| } | |
| function renderAnswer(result){ | |
| document.getElementById("answer-wrap").style.display="block"; | |
| document.getElementById("answer-text").textContent=result.generation||"No answer generated."; | |
| const v=document.getElementById("verdict"); | |
| if(result.verdict==="APPROVED"){v.className="v-ok";v.textContent="β High confidence";} | |
| else if(result.verdict){v.className="v-warn";v.textContent="β Low confidence β verify with source";} | |
| else v.textContent=""; | |
| document.getElementById("answer-wrap").scrollIntoView({behavior:"smooth",block:"nearest"}); | |
| } | |
| function copyAns(btn){ | |
| navigator.clipboard.writeText(document.getElementById("answer-text").textContent) | |
| .then(()=>{btn.textContent="β Copied!";setTimeout(()=>{btn.innerHTML="📋 Copy";},1800);}); | |
| } | |
| function sm(id,type,msg){ | |
| const el=document.getElementById(id); | |
| if(type==="ok")el.innerHTML=\'<div class="msg msg-ok">\'+msg+\'</div>\'; | |
| else if(type==="error")el.innerHTML=\'<div class="msg msg-err">\'+esc(msg)+\'</div>\'; | |
| else el.innerHTML=\'<div class="msg-info">\'+esc(msg)+\'</div>\'; | |
| } | |
| </script> | |
| </body> | |
| </html> | |
| ''' | |
| out = pathlib.Path(__file__).parent / "templates" / "index.html" | |
| out.write_text(HTML, encoding="utf-8") | |
| print(f"Done, size: {len(HTML)}") | |