File size: 47,321 Bytes
189df32 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 744 745 746 747 748 749 750 751 752 753 754 755 756 757 758 759 760 761 762 763 764 765 766 767 768 769 770 771 772 773 774 775 776 777 778 779 780 781 782 783 784 785 786 787 788 789 790 791 792 793 794 795 796 797 798 799 800 801 802 803 804 805 806 807 808 809 810 811 812 813 814 815 816 817 818 819 820 821 822 823 824 825 826 827 828 829 830 831 832 833 834 835 836 837 838 839 840 841 842 843 844 845 846 847 848 849 850 851 852 853 854 855 856 857 858 859 860 861 862 863 864 865 866 867 868 869 870 871 872 873 874 875 876 877 878 879 880 881 882 883 884 885 886 887 888 889 890 891 892 893 894 895 896 897 | <!DOCTYPE html>
<html class="dark" lang="en">
<head>
<meta charset="utf-8"/>
<meta content="width=device-width, initial-scale=1.0" name="viewport"/>
<title>EpiRAG Research Assistant</title>
<script src="https://cdn.tailwindcss.com?plugins=forms,container-queries"></script>
<link href="https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@300;400;500;600;700&family=Inter:wght@300;400;500;600&family=IBM+Plex+Mono:wght@300;400;500;600&display=swap" rel="stylesheet"/>
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:wght,FILL@100..700,0..1&display=swap" rel="stylesheet"/>
<style>
.material-symbols-outlined {
font-variation-settings: 'FILL' 0, 'wght' 400, 'GRAD' 0, 'opsz' 24;
}
body { font-family: 'Inter', sans-serif; background-color: #0a0e14; }
.font-mono { font-family: 'IBM Plex Mono', monospace; }
.font-headline { font-family: 'Space Grotesk', sans-serif; }
::-webkit-scrollbar { width: 4px; }
::-webkit-scrollbar-track { background: #0a0e14; }
::-webkit-scrollbar-thumb { background: #3c495b; }
/* Typing cursor animation */
@keyframes blink { 0%,100%{opacity:1} 50%{opacity:0} }
.cursor::after { content:'|'; animation: blink 1s infinite; margin-left:2px; color:#619eff; }
/* Trace log pulse */
@keyframes trace-in { from{opacity:0;transform:translateX(8px)} to{opacity:1;transform:translateX(0)} }
.trace-step { animation: trace-in 0.3s ease forwards; opacity:0; }
/* Source card slide */
@keyframes slide-in { from{opacity:0;transform:translateY(6px)} to{opacity:1;transform:translateY(0)} }
.source-card { animation: slide-in 0.25s ease forwards; opacity:0; }
/* Paper list scroll */
.paper-list { max-height: 180px; overflow-y: auto; }
.paper-list::-webkit-scrollbar { width: 2px; }
.paper-list::-webkit-scrollbar-thumb { background: #3c495b; }
/* Live debate panel */
#live-debate-panel {
position: fixed;
bottom: 24px;
left: 24px;
width: 320px;
max-height: 420px;
z-index: 100;
display: none;
}
#live-debate-panel.active { display: block; }
#debate-feed {
max-height: 300px;
overflow-y: auto;
scroll-behavior: smooth;
}
#debate-feed::-webkit-scrollbar { width: 2px; }
#debate-feed::-webkit-scrollbar-thumb { background: #3c495b; }
@keyframes msg-in { from{opacity:0;transform:translateY(4px)} to{opacity:1;transform:translateY(0)} }
.debate-msg { animation: msg-in 0.2s ease forwards; }
@keyframes typing { 0%,100%{opacity:1} 50%{opacity:0.3} }
.typing-dot { animation: typing 1s infinite; display:inline-block; }
/* Shimmer loading */
@keyframes shimmer { 0%{background-position:-200% 0} 100%{background-position:200% 0} }
.shimmer {
background: linear-gradient(90deg, #16202e 25%, #1e2d41 50%, #16202e 75%);
background-size: 200% 100%;
animation: shimmer 1.5s infinite;
}
/* Draggable trace log */
#trace-panel {
position: fixed;
bottom: 24px;
right: 24px;
width: 256px;
z-index: 100;
user-select: none;
}
#trace-handle {
cursor: grab;
}
#trace-handle:active { cursor: grabbing; }
#trace-panel.dragging { opacity: 0.92; box-shadow: 0 8px 32px rgba(0,0,0,0.6); }
/* Markdown rendering inside answer block */
#answer-text h1,#answer-text h2,#answer-text h3 {
font-family: 'Space Grotesk', sans-serif;
font-weight: 600;
color: #d9e6fd;
margin: 1rem 0 0.5rem;
}
#answer-text h1 { font-size: 1.2rem; }
#answer-text h2 { font-size: 1.05rem; }
#answer-text h3 { font-size: 0.95rem; color: #619eff; }
#answer-text p { margin-bottom: 0.75rem; line-height: 1.75; }
#answer-text strong { color: #d9e6fd; font-weight: 600; }
#answer-text em { color: #9facc1; font-style: italic; }
#answer-text a { color: #619eff; text-decoration: underline; text-underline-offset: 3px; }
#answer-text a:hover { color: #93b8ff; }
#answer-text ul,#answer-text ol { padding-left: 1.4rem; margin-bottom: 0.75rem; }
#answer-text li { margin-bottom: 0.35rem; line-height: 1.65; }
#answer-text ul li { list-style-type: disc; }
#answer-text ol li { list-style-type: decimal; }
#answer-text code {
font-family: 'IBM Plex Mono', monospace;
font-size: 0.82em;
background: #16202e;
border: 1px solid #3c495b;
padding: 1px 5px;
color: #3fb950;
}
#answer-text blockquote {
border-left: 3px solid #619eff;
padding-left: 1rem;
color: #9facc1;
margin: 0.75rem 0;
}
#answer-text hr { border-color: #3c495b; margin: 1rem 0; }
</style>
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
<script id="tailwind-config">
tailwind.config = {
darkMode: "class",
theme: {
extend: {
colors: {
"primary-container": "#41484c",
"on-secondary-container": "#bac0c8",
"on-background": "#d9e6fd",
"outline-variant": "#3c495b",
"outline": "#6a768a",
"background": "#0a0e14",
"secondary": "#989ea7",
"tertiary": "#619eff",
"on-surface-variant": "#9facc1",
"on-surface": "#d9e6fd",
"surface-container-low": "#0e141c",
"surface-container": "#121a25",
"surface-container-high": "#16202e",
"surface-container-highest": "#1a2637",
"surface-container-lowest": "#000000",
"surface": "#0a0e14",
"primary": "#c1c7cd",
"on-primary": "#3b4146",
"primary-dim": "#b3b9bf",
},
fontFamily: {
"headline": ["Space Grotesk"],
"body": ["Inter"],
"label": ["Space Grotesk"]
},
borderRadius: {"DEFAULT": "0px", "lg": "0px", "xl": "0px", "full": "9999px"},
},
},
}
</script>
</head>
<body class="bg-background text-on-surface selection:bg-tertiary/30">
<!-- TopAppBar -->
<header class="flex justify-between items-center w-full px-6 h-16 bg-[#0a0e14] border-b border-[#30363d]/40 fixed top-0 z-50">
<div class="flex items-center gap-8">
<div class="flex items-center gap-2 text-xl font-bold text-slate-100 tracking-tighter font-['Space_Grotesk']">
<span class="material-symbols-outlined text-2xl">biotech</span>
EpiRAG
</div>
<nav class="hidden md:flex items-center gap-6">
<a class="text-slate-100 border-b-2 border-slate-100 pb-1 font-mono text-xs uppercase tracking-widest" href="#">Research</a>
<a class="text-slate-400 font-mono text-xs uppercase tracking-widest hover:text-slate-100 transition-colors" href="/performance">Performance</a>
<a class="flex items-center gap-1 text-slate-400 font-mono text-xs uppercase tracking-widest hover:text-slate-100 transition-colors" href="https://github.com/RohanBiswas67/epirag" target="_blank">
GitHub
<span class="material-symbols-outlined text-sm">open_in_new</span>
</a>
</nav>
</div>
<div class="flex items-center gap-4">
<span id="system-status" class="font-mono text-[10px] text-tertiary flex items-center gap-2">
<span class="w-2 h-2 bg-tertiary animate-pulse"></span>
SYSTEM ACTIVE
</span>
</div>
</header>
<div class="flex min-h-screen pt-16">
<!-- Sidebar β Corpus Info (no API keys) -->
<aside class="hidden md:flex flex-col h-[calc(100vh-64px)] w-64 p-4 gap-5 bg-[#0e141c] border-r border-[#30363d]/40 sticky top-16 overflow-y-auto">
<div class="space-y-1">
<h2 class="flex items-center gap-2 text-slate-100 font-bold font-mono text-xs uppercase tracking-widest">
<span class="material-symbols-outlined text-sm">database</span>
CORPUS
</h2>
<p class="text-[10px] text-on-surface-variant font-mono">v2.0 Β· EpiRAG Index</p>
</div>
<!-- Corpus Stats -->
<div class="p-4 border border-outline-variant/40 bg-surface-container space-y-3">
<h3 class="font-mono text-[10px] text-tertiary flex items-center gap-2">
<span class="material-symbols-outlined text-xs">analytics</span>
INDEX STATS
</h3>
<div class="space-y-2 font-mono text-[11px]">
<div class="flex justify-between">
<span class="text-on-surface-variant">Chunks:</span>
<span id="stat-chunks" class="text-on-surface">β</span>
</div>
<div class="flex justify-between">
<span class="text-on-surface-variant">Papers:</span>
<span id="stat-papers" class="text-on-surface">β</span>
</div>
<div class="flex justify-between">
<span class="text-on-surface-variant">Embeddings:</span>
<span class="text-on-surface">MiniLM-L6</span>
</div>
<div class="flex justify-between">
<span class="text-on-surface-variant">LLM:</span>
<span class="text-on-surface">Llama 3.1</span>
</div>
<div class="flex justify-between">
<span class="text-on-surface-variant">Fallback:</span>
<span class="text-tertiary">Tavily Web</span>
</div>
</div>
</div>
<!-- Paper List -->
<div class="space-y-2">
<h3 class="font-mono text-[10px] text-on-surface-variant uppercase tracking-widest flex items-center gap-2">
<span class="material-symbols-outlined text-xs">description</span>
INDEXED PAPERS
</h3>
<div id="paper-list" class="paper-list space-y-1">
<div class="shimmer h-3 w-full rounded-none"></div>
<div class="shimmer h-3 w-4/5 rounded-none mt-1"></div>
<div class="shimmer h-3 w-full rounded-none mt-1"></div>
<div class="shimmer h-3 w-3/5 rounded-none mt-1"></div>
</div>
</div>
<!-- Retrieval Strategy -->
<div class="p-3 border border-outline-variant/40 bg-surface-container space-y-2">
<h3 class="font-mono text-[10px] text-on-surface-variant uppercase tracking-widest">RETRIEVAL STRATEGY</h3>
<div class="space-y-1.5 font-mono text-[10px] text-on-surface-variant">
<div class="flex items-center gap-2">
<span class="w-1.5 h-1.5 bg-tertiary inline-block flex-shrink-0"></span>
Local sim β₯ 0.45 β corpus
</div>
<div class="flex items-center gap-2">
<span class="w-1.5 h-1.5 bg-green-400 inline-block flex-shrink-0"></span>
Sim < 0.45 β web fallback
</div>
<div class="flex items-center gap-2">
<span class="w-1.5 h-1.5 bg-purple-400 inline-block flex-shrink-0"></span>
Recency kw β forced hybrid
</div>
</div>
</div>
<div class="mt-auto flex flex-col gap-2">
<a class="flex items-center gap-3 p-2 text-slate-500 hover:text-slate-300 font-mono text-[10px] uppercase tracking-widest transition-colors" href="https://github.com/RohanBiswas67/epirag" target="_blank">
<span class="material-symbols-outlined text-sm">code</span>
Source Code
</a>
<a class="flex items-center gap-3 p-2 text-slate-500 hover:text-slate-300 font-mono text-[10px] uppercase tracking-widest transition-colors" href="https://linkedin.com/in/rohan-biswas-0rb" target="_blank">
<span class="material-symbols-outlined text-sm">person</span>
Rohan Biswas
</a>
</div>
</aside>
<!-- Main Content -->
<main class="flex-1 bg-surface-container-low min-h-full">
<div class="max-w-4xl mx-auto px-6 py-12">
<!-- Header -->
<div class="mb-12 border-l-4 border-tertiary pl-6">
<h1 class="text-4xl font-headline font-bold text-on-surface tracking-tight mb-2 uppercase">Semantic Research Engine</h1>
<p class="text-on-surface-variant font-mono text-sm">Query epidemic modeling literature with RAG-enhanced reasoning.</p>
</div>
<!-- Query Entry -->
<div class="bg-surface border border-outline-variant p-1 mb-8">
<div class="relative">
<textarea id="query-input"
class="w-full bg-surface-container-lowest text-on-surface font-mono text-sm p-4 focus:ring-0 focus:outline-none resize-none border-0"
placeholder="Enter research query (e.g., 'What does Shalizi say about homophily and contagion?')..."
rows="4"></textarea>
<div class="absolute bottom-4 right-4 flex items-center gap-4">
<span class="font-mono text-[10px] text-on-surface-variant hidden md:block">Press ββ΅ to search</span>
<button id="search-btn"
class="bg-primary text-on-primary px-8 py-2 font-mono text-xs font-bold uppercase tracking-widest hover:bg-primary-dim transition-colors">
Search
</button>
</div>
</div>
</div>
<!-- Results β hidden until first query -->
<div id="results-area" class="space-y-6 hidden">
<!-- Result Metadata Header -->
<div class="flex items-center justify-between border-b border-outline-variant pb-2">
<div class="flex items-center gap-4">
<span id="mode-badge" class="border px-2 py-0.5 font-mono text-[10px] uppercase tracking-widest"></span>
<span id="meta-line" class="text-on-surface-variant font-mono text-[10px]"></span>
</div>
<div class="flex items-center gap-2">
<button onclick="copyAnswer()" title="Copy answer">
<span class="material-symbols-outlined text-on-surface-variant text-sm hover:text-slate-100 transition-colors">content_copy</span>
</button>
</div>
</div>
<!-- Answer Block -->
<div class="bg-surface-container border border-outline-variant p-8 relative overflow-hidden">
<div class="absolute top-0 right-0 p-2 opacity-10">
<span class="material-symbols-outlined text-6xl">psychology</span>
</div>
<h3 class="font-mono text-xs text-tertiary uppercase tracking-widest mb-4">Generated Synthesis</h3>
<div id="answer-text" class="prose prose-invert max-w-none text-on-surface leading-relaxed font-body text-base space-y-4 whitespace-pre-wrap"></div>
</div>
<!-- Debate Transcript (hidden until debate runs) -->
<div id="debate-container" class="hidden border border-outline-variant mb-0">
<button onclick="toggleDebate()" class="w-full flex items-center justify-between p-4 bg-surface-container-high hover:bg-surface-container-highest transition-colors">
<span id="debate-label" class="font-mono text-xs uppercase tracking-widest flex items-center gap-2">
<span class="material-symbols-outlined text-sm">forum</span>
Agent Debate Transcript
</span>
<span id="debate-chevron" class="material-symbols-outlined">expand_more</span>
</button>
<div id="debate-body" class="hidden bg-surface p-4 space-y-4 font-mono text-[11px]"></div>
</div>
<!-- Sources Accordion -->
<div id="sources-container" class="border border-outline-variant">
<button onclick="toggleSources()" class="w-full flex items-center justify-between p-4 bg-surface-container-high hover:bg-surface-container-highest transition-colors">
<span id="sources-label" class="font-mono text-xs uppercase tracking-widest flex items-center gap-2">
<span class="material-symbols-outlined text-sm">link</span>
Sources (0)
</span>
<span id="sources-chevron" class="material-symbols-outlined">expand_more</span>
</button>
<div id="sources-list" class="divide-y divide-outline-variant/40 bg-surface"></div>
</div>
</div>
<!-- Loading skeleton β shown while querying -->
<div id="loading-area" class="space-y-6 hidden">
<div class="shimmer h-8 w-48 rounded-none"></div>
<div class="bg-surface-container border border-outline-variant p-8 space-y-3">
<div class="shimmer h-3 w-32 rounded-none"></div>
<div class="shimmer h-4 w-full rounded-none"></div>
<div class="shimmer h-4 w-5/6 rounded-none"></div>
<div class="shimmer h-4 w-4/6 rounded-none"></div>
<div class="shimmer h-4 w-full rounded-none"></div>
<div class="shimmer h-4 w-3/5 rounded-none"></div>
</div>
<div class="shimmer h-12 w-full rounded-none"></div>
</div>
<!-- Example Queries -->
<div id="examples-area" class="mt-12">
<h5 class="flex items-center gap-2 font-mono text-[10px] text-on-surface-variant uppercase tracking-widest mb-4">
<span class="material-symbols-outlined text-xs">help</span>
Example queries
</h5>
<div class="grid grid-cols-1 md:grid-cols-2 gap-3">
<button onclick="setQuery(this)" class="text-left p-3 border border-outline-variant bg-surface-container-low hover:bg-surface-container transition-colors group">
<span class="font-mono text-[11px] text-on-surface group-hover:text-tertiary transition-colors">Explain Barabasi-Albert Model with real-life application example.</span>
</button>
<button onclick="setQuery(this)" class="text-left p-3 border border-outline-variant bg-surface-container-low hover:bg-surface-container transition-colors group">
<span class="font-mono text-[11px] text-on-surface group-hover:text-tertiary transition-colors">Explain Kemeny-Snell lumpability.</span>
</button>
<button onclick="setQuery(this)" class="text-left p-3 border border-outline-variant bg-surface-container-low hover:bg-surface-container transition-colors group">
<span class="font-mono text-[11px] text-on-surface group-hover:text-tertiary transition-colors">Latest GNN-based epidemic forecasting research 2026.</span>
</button>
<button onclick="setQuery(this)" class="text-left p-3 border border-outline-variant bg-surface-container-low hover:bg-surface-container transition-colors group">
<span class="font-mono text-[11px] text-on-surface group-hover:text-tertiary transition-colors">Recent papers related to Network Science and Epidemiology in 2026</span>
</button>
</div>
</div>
</div>
</main>
</div>
<!-- Live Debate Panel -->
<div id="live-debate-panel">
<div class="bg-[#0a0e14] border border-outline-variant">
<div id="live-debate-handle" class="flex items-center justify-between p-3 cursor-grab select-none border-b border-outline-variant/40">
<span class="font-mono text-[10px] text-on-surface-variant flex items-center gap-2">
<span class="material-symbols-outlined text-xs">forum</span>
LIVE DEBATE
</span>
<div class="flex items-center gap-2">
<span id="debate-status-dot" class="w-1.5 h-1.5 rounded-full bg-outline-variant"></span>
<button onclick="closeLiveDebate()" class="text-outline hover:text-slate-300 transition-colors">
<span class="material-symbols-outlined text-sm">close</span>
</button>
</div>
</div>
<div id="debate-round-header" class="px-3 py-1.5 font-mono text-[9px] text-on-surface-variant border-b border-outline-variant/20 hidden"></div>
<div id="debate-feed" class="p-3 space-y-2"></div>
</div>
</div>
<!-- Trace Log β draggable panel -->
<div id="trace-panel" class="hidden lg:block">
<div class="bg-[#0a0e14] border border-outline-variant p-4">
<!-- Drag handle -->
<div id="trace-handle" class="flex items-center justify-between mb-4 select-none">
<span class="font-mono text-[10px] text-on-surface-variant flex items-center gap-2">
<span class="material-symbols-outlined text-xs text-outline">drag_indicator</span>
TRACE LOG
</span>
<span id="trace-dot" class="w-1.5 h-1.5 rounded-full bg-outline-variant"></span>
</div>
<div id="trace-log" class="relative space-y-4 before:content-[''] before:absolute before:left-1 before:top-2 before:bottom-2 before:w-[1px] before:bg-outline-variant">
<div class="relative pl-6 text-on-surface-variant">
<div class="absolute left-0 top-1.5 w-2 h-2 bg-outline-variant border border-[#0a0e14]"></div>
<div class="font-mono text-[10px] font-bold">IDLE</div>
<div class="font-mono text-[9px]">Awaiting query...</div>
</div>
</div>
</div>
</div>
<script>
// ββ State ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
let sourcesOpen = true;
const API_BASE = window.location.origin; // same server
// ββ Load corpus stats on page load βββββββββββββββββββββββββββββββββββββββββββ
async function loadStats() {
try {
const res = await fetch(`${API_BASE}/api/stats`);
const data = await res.json();
document.getElementById("stat-chunks").textContent = data.chunks.toLocaleString();
document.getElementById("stat-papers").textContent = data.papers;
const listEl = document.getElementById("paper-list");
listEl.innerHTML = "";
(data.paperList || []).forEach(p => {
const div = document.createElement("div");
div.className = "font-mono text-[10px] text-on-surface-variant py-0.5 border-b border-outline-variant/20 truncate hover:text-slate-300 transition-colors";
div.title = p;
div.textContent = p;
listEl.appendChild(div);
});
if (data.status === "offline") {
document.getElementById("system-status").innerHTML =
'<span class="w-2 h-2 bg-red-500"></span><span class="text-red-400">CORPUS OFFLINE</span>';
}
} catch (e) {
console.error("Stats load failed:", e);
}
}
// ββ Trace log helpers ββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
function setTrace(steps) {
const log = document.getElementById("trace-log");
const dot = document.getElementById("trace-dot");
dot.className = "w-1.5 h-1.5 rounded-full bg-tertiary animate-pulse";
log.innerHTML = steps.map((s, i) => `
<div class="relative pl-6 trace-step" style="animation-delay:${i * 120}ms">
<div class="absolute left-0 top-1.5 w-2 h-2 ${s.done ? 'bg-tertiary' : 'bg-outline-variant'} border border-[#0a0e14]"></div>
<div class="font-mono text-[10px] ${s.done ? 'text-on-surface' : 'text-on-surface-variant'} font-bold">${s.label}</div>
<div class="font-mono text-[9px] text-on-surface-variant ${!s.done ? 'italic' : ''}">${s.sub}</div>
</div>
`).join("");
}
function setTraceDone(result) {
const dot = document.getElementById("trace-dot");
dot.className = "w-1.5 h-1.5 rounded-full bg-green-400";
setTrace([
{ label: "QUERY_EMBED_GEN", sub: "Success", done: true },
{ label: "VECTOR_RETRIEVAL", sub: `Top-K: ${result.sources.filter(s=>s.type==="local").length} local`, done: true },
{ label: result.mode === "local" ? "LOCAL_ONLY" : "TAVILY_SEARCH",
sub: result.mode === "local" ? `sim: ${result.avg_sim}` : `${result.sources.filter(s=>s.type==="web").length} web results`, done: true },
{ label: "LLM_SYNTHESIS", sub: `${result.latency_ms}ms Β· ~${result.tokens} tokens`, done: true },
]);
}
// ββ Mode badge ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
const MODE_CONFIG = {
local: { label: "Local Mode", cls: "bg-tertiary/10 border-tertiary text-tertiary" },
web: { label: "Web Mode", cls: "bg-green-900/30 border-green-500 text-green-400" },
hybrid: { label: "Hybrid Mode", cls: "bg-purple-900/30 border-purple-500 text-purple-300" },
none: { label: "No Results", cls: "bg-red-900/30 border-red-500 text-red-400" },
};
// ββ Main query handler ββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
// ββ Agent colors βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
const AGENT_COLORS = {
"Alpha": { text: "text-red-400", border: "border-red-900", bg: "bg-red-950/30" },
"Beta": { text: "text-yellow-400", border: "border-yellow-900", bg: "bg-yellow-950/30" },
"Gamma": { text: "text-green-400", border: "border-green-900", bg: "bg-green-950/30" },
"Delta": { text: "text-purple-400", border: "border-purple-900", bg: "bg-purple-950/30" },
"Epsilon": { text: "text-tertiary", border: "border-blue-900", bg: "bg-blue-950/30" },
};
function openLiveDebate() {
const panel = document.getElementById("live-debate-panel");
panel.classList.add("active");
document.getElementById("debate-feed").innerHTML = "";
document.getElementById("debate-round-header").classList.add("hidden");
document.getElementById("debate-status-dot").className = "w-1.5 h-1.5 rounded-full bg-tertiary animate-pulse";
}
function closeLiveDebate() {
document.getElementById("live-debate-panel").classList.remove("active");
}
function addDebateMessage(name, text, round) {
const feed = document.getElementById("debate-feed");
const color = AGENT_COLORS[name] || { text: "text-on-surface-variant", border: "border-outline-variant", bg: "" };
const div = document.createElement("div");
div.className = `debate-msg border-l-2 ${color.border} pl-2 py-1 ${color.bg} rounded-r`;
div.innerHTML = `
<div class="flex items-center gap-1.5 mb-0.5">
<span class="font-mono text-[9px] font-bold ${color.text}">${name}</span>
<span class="font-mono text-[8px] text-outline">R${round}</span>
</div>
<div class="font-mono text-[9px] text-on-surface-variant leading-relaxed">${text.slice(0, 180)}${text.length > 180 ? "..." : ""}</div>
`;
feed.appendChild(div);
feed.scrollTop = feed.scrollHeight;
}
async function runQuery() {
const question = document.getElementById("query-input").value.trim();
if (!question) return;
document.getElementById("results-area").classList.add("hidden");
document.getElementById("loading-area").classList.remove("hidden");
document.getElementById("examples-area").classList.add("hidden");
document.getElementById("search-btn").disabled = true;
document.getElementById("search-btn").textContent = "Searching...";
setTrace([
{ label: "QUERY_EMBED_GEN", sub: "Running...", done: false },
{ label: "VECTOR_RETRIEVAL", sub: "Pending", done: false },
{ label: "AGENT_DEBATE", sub: "Starting...", done: false },
{ label: "SYNTHESIS", sub: "Pending", done: false },
]);
openLiveDebate();
try {
const response = await fetch(`${API_BASE}/api/query/stream`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ question })
});
const reader = response.body.getReader();
const decoder = new TextDecoder();
let buffer = "";
let finalData = null;
while (true) {
const { done, value } = await reader.read();
if (done) break;
buffer += decoder.decode(value, { stream: true });
const lines = buffer.split("\n\n");
buffer = lines.pop();
for (const line of lines) {
if (!line.startsWith("data: ")) continue;
try {
const event = JSON.parse(line.slice(6));
if (event.type === "round_start") {
const header = document.getElementById("debate-round-header");
header.textContent = `ββ Round ${event.round} ββ`;
header.classList.remove("hidden");
setTrace([
{ label: "QUERY_EMBED_GEN", sub: "Done", done: true },
{ label: "VECTOR_RETRIEVAL", sub: "Done", done: true },
{ label: "AGENT_DEBATE", sub: `Round ${event.round}...`, done: false },
{ label: "SYNTHESIS", sub: "Pending", done: false },
]);
}
else if (event.type === "agent_done") {
addDebateMessage(event.name, event.text, event.round);
}
else if (event.type === "synthesizing") {
const header = document.getElementById("debate-round-header");
header.textContent = "ββ Epsilon synthesizing... ββ";
header.classList.remove("hidden");
setTrace([
{ label: "QUERY_EMBED_GEN", sub: "Done", done: true },
{ label: "VECTOR_RETRIEVAL", sub: "Done", done: true },
{ label: "AGENT_DEBATE", sub: "Done", done: true },
{ label: "SYNTHESIS", sub: "Streaming...", done: false },
]);
}
else if (event.type === "result") {
finalData = event;
document.getElementById("debate-status-dot").className =
"w-1.5 h-1.5 rounded-full bg-green-400";
}
else if (event.type === "error") {
throw new Error(event.text);
}
} catch (parseErr) { /* skip malformed events */ }
}
}
if (finalData) {
renderResults(finalData);
setTraceDone(finalData);
}
} catch (err) {
document.getElementById("loading-area").classList.add("hidden");
document.getElementById("results-area").classList.remove("hidden");
document.getElementById("answer-text").textContent = `Error: ${err.message}`;
document.getElementById("mode-badge").textContent = "ERROR";
closeLiveDebate();
} finally {
document.getElementById("search-btn").disabled = false;
document.getElementById("search-btn").textContent = "Search";
}
}
function renderResults(data) {
document.getElementById("loading-area").classList.add("hidden");
document.getElementById("results-area").classList.remove("hidden");
// Mode badge
const mc = MODE_CONFIG[data.mode] || MODE_CONFIG.none;
const badge = document.getElementById("mode-badge");
badge.textContent = mc.label;
badge.className = `border px-2 py-0.5 font-mono text-[10px] uppercase tracking-widest ${mc.cls}`;
// Meta line
document.getElementById("meta-line").textContent =
`Lat: ${data.latency_ms}ms | Tokens: ~${data.tokens} | Sim: ${data.avg_sim}`;
// Answer β render markdown
if (typeof marked !== 'undefined') {
marked.setOptions({ breaks: true, gfm: true });
document.getElementById("answer-text").innerHTML = marked.parse(data.answer);
} else {
document.getElementById("answer-text").textContent = data.answer;
}
// Sources
const localCount = data.sources.filter(s => s.type === "local").length;
const webCount = data.sources.filter(s => s.type === "web").length;
document.getElementById("sources-label").innerHTML = `
<span class="material-symbols-outlined text-sm">link</span>
Sources (${data.sources.length}) Β·
<span class="text-tertiary">${localCount} local</span>
${webCount > 0 ? ` <span class="text-green-400">${webCount} web</span>` : ""}
`;
const list = document.getElementById("sources-list");
list.innerHTML = "";
data.sources.forEach((src, i) => {
const isWeb = src.type === "web";
const relPct = Math.round(src.similarity * 100);
const card = document.createElement("div");
card.className = "source-card p-4 flex items-start justify-between hover:bg-surface-container-low transition-colors group";
card.style.animationDelay = `${i * 60}ms`;
card.innerHTML = `
<div class="space-y-1 flex-1 min-w-0 pr-4">
<div class="flex items-center gap-2">
<span class="font-mono text-[10px] ${isWeb ? 'text-green-400' : 'text-tertiary'} flex items-center gap-1">
<span class="material-symbols-outlined text-xs">${isWeb ? 'public' : 'description'}</span>
[${String(i+1).padStart(2,'0')}]
</span>
<h4 class="text-sm font-semibold text-on-surface group-hover:text-tertiary transition-colors truncate">${src.source}</h4>
</div>
<p class="text-xs text-on-surface-variant pl-8 font-mono line-clamp-2">${src.text.slice(0, 120)}...</p>
${(() => {
const isWeb = src.type === 'web';
const links = src.links || {};
const btnCls = "inline-flex items-center gap-1 font-mono text-[9px] px-2 py-0.5 border border-outline-variant hover:border-tertiary hover:text-tertiary text-on-surface-variant transition-colors";
if (isWeb && src.url) {
return `<a class="text-[10px] text-tertiary/80 pl-8 font-mono hover:underline flex items-center gap-1 truncate" href="${src.url}" target="_blank">${src.url.slice(0,60)}${src.url.length>60?'β¦':''}<span class="material-symbols-outlined text-[10px] flex-shrink-0">open_in_new</span></a>`;
}
let btns = '<div class="pl-8 flex flex-wrap gap-1.5 mt-1.5">';
// PDF first β highest value
const pdfUrl = links.pdf || links.arxiv_pdf;
if (pdfUrl) btns += `<a class="${btnCls} text-green-400 border-green-800 hover:border-green-400 hover:text-green-300" href="${pdfUrl}" target="_blank">
<span class="material-symbols-outlined text-[11px]">picture_as_pdf</span>
PDF
<span class="material-symbols-outlined text-[9px]">open_in_new</span>
</a>`;
// Exact matches
if (links.semantic_scholar) btns += `<a class="${btnCls}" href="${links.semantic_scholar}" target="_blank">Semantic Scholar <span class="material-symbols-outlined text-[9px]">open_in_new</span></a>`;
if (links.arxiv) btns += `<a class="${btnCls}" href="${links.arxiv}" target="_blank">arXiv <span class="material-symbols-outlined text-[9px]">open_in_new</span></a>`;
if (links.doi) btns += `<a class="${btnCls}" href="${links.doi}" target="_blank">DOI <span class="material-symbols-outlined text-[9px]">open_in_new</span></a>`;
if (links.pubmed) btns += `<a class="${btnCls}" href="${links.pubmed}" target="_blank">PubMed <span class="material-symbols-outlined text-[9px]">open_in_new</span></a>`;
if (links.openalex) btns += `<a class="${btnCls}" href="${links.openalex}" target="_blank">OpenAlex <span class="material-symbols-outlined text-[9px]">open_in_new</span></a>`;
// Search fallbacks β always present
if (!links.semantic_scholar && links.semantic_scholar_search) btns += `<a class="${btnCls}" href="${links.semantic_scholar_search}" target="_blank">Semantic Scholar <span class="material-symbols-outlined text-[9px]">open_in_new</span></a>`;
if (!links.arxiv && links.arxiv_search) btns += `<a class="${btnCls}" href="${links.arxiv_search}" target="_blank">arXiv <span class="material-symbols-outlined text-[9px]">open_in_new</span></a>`;
if (!links.pubmed && links.pubmed_search) btns += `<a class="${btnCls}" href="${links.pubmed_search}" target="_blank">PubMed <span class="material-symbols-outlined text-[9px]">open_in_new</span></a>`;
btns += '<span class="w-full h-px bg-outline-variant/30 my-0.5"></span>';
// Always-present search links
if (links.google_scholar) btns += `<a class="${btnCls}" href="${links.google_scholar}" target="_blank">Google Scholar <span class="material-symbols-outlined text-[9px]">open_in_new</span></a>`;
if (links.ncbi_search) btns += `<a class="${btnCls}" href="${links.ncbi_search}" target="_blank">NCBI <span class="material-symbols-outlined text-[9px]">open_in_new</span></a>`;
if (links.google) btns += `<a class="${btnCls}" href="${links.google}" target="_blank">Google <span class="material-symbols-outlined text-[9px]">open_in_new</span></a>`;
btns += '</div>';
return btns;
})()}
</div>
<div class="text-right flex-shrink-0">
<div class="text-[10px] font-mono text-on-surface-variant uppercase mb-1">Relevance</div>
<div class="text-sm font-mono font-bold ${relPct > 70 ? 'text-tertiary' : relPct > 40 ? 'text-yellow-400' : 'text-on-surface-variant'}">${relPct}%</div>
</div>
`;
list.appendChild(card);
});
// Open sources accordion
sourcesOpen = true;
list.classList.remove("hidden");
document.getElementById("sources-chevron").textContent = "expand_less";
// Render debate transcript if present
const debateContainer = document.getElementById("debate-container");
const debateBody = document.getElementById("debate-body");
const debateLabel = document.getElementById("debate-label");
if (data.is_debate && data.debate_rounds && data.debate_rounds.length > 0) {
debateContainer.classList.remove("hidden");
const consensus = data.consensus ? "Consensus reached" : "Forced synthesis";
const agentCls = {
"Alpha": "text-red-400",
"Beta": "text-yellow-400",
"Gamma": "text-green-400",
"Delta": "text-purple-400",
"Epsilon": "text-tertiary"
};
debateLabel.innerHTML = `
<span class="material-symbols-outlined text-sm">forum</span>
Agent Debate Β· ${data.rounds_run} round${data.rounds_run > 1 ? "s" : ""} Β· ${consensus}
`;
let html = "";
data.debate_rounds.forEach((round, ri) => {
html += `<div class="border-b border-outline-variant/30 pb-3 mb-3">
<div class="text-tertiary mb-2 uppercase tracking-widest text-[10px]">ββ Round ${ri + 1} ββ</div>`;
Object.entries(round).forEach(([agent, answer]) => {
const cls = agentCls[agent] || "text-on-surface-variant";
html += `<div class="mb-3">
<div class="${cls} font-bold mb-1">${agent}</div>
<div class="text-on-surface-variant leading-relaxed whitespace-pre-wrap">${answer.slice(0, 600)}${answer.length > 600 ? "..." : ""}</div>
</div>`;
});
html += "</div>";
});
debateBody.innerHTML = html;
} else {
debateContainer.classList.add("hidden");
}
}
// ββ Helpers βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
function toggleDebate() {
const body = document.getElementById("debate-body");
const chevron = document.getElementById("debate-chevron");
const open = body.classList.toggle("hidden");
chevron.textContent = open ? "expand_more" : "expand_less";
}
function toggleSources() {
sourcesOpen = !sourcesOpen;
document.getElementById("sources-list").classList.toggle("hidden", !sourcesOpen);
document.getElementById("sources-chevron").textContent = sourcesOpen ? "expand_less" : "expand_more";
}
function setQuery(btn) {
document.getElementById("query-input").value = btn.querySelector("span").textContent;
document.getElementById("query-input").focus();
}
function copyAnswer() {
const text = document.getElementById("answer-text").textContent;
navigator.clipboard.writeText(text).then(() => {
const btn = document.querySelector('[onclick="copyAnswer()"] .material-symbols-outlined');
btn.textContent = "check";
setTimeout(() => btn.textContent = "content_copy", 1500);
});
}
// ββ Keyboard shortcut: Cmd/Ctrl + Enter ββββββββββββββββββββββββββββββββββββββ
document.addEventListener("keydown", e => {
if ((e.metaKey || e.ctrlKey) && e.key === "Enter") runQuery();
});
document.getElementById("search-btn").addEventListener("click", runQuery);
// ββ Init ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
// Draggable live debate panel
(function() {
const panel = document.getElementById("live-debate-panel");
const handle = document.getElementById("live-debate-handle");
if (!panel || !handle) return;
let drag = false, sx, sy, sl, sb;
handle.addEventListener("mousedown", e => {
drag = true;
panel.classList.add("dragging");
const r = panel.getBoundingClientRect();
sx = e.clientX; sy = e.clientY;
sl = r.left; sb = window.innerHeight - r.bottom;
e.preventDefault();
});
document.addEventListener("mousemove", e => {
if (!drag) return;
const newLeft = Math.max(0, Math.min(window.innerWidth - panel.offsetWidth, sl + (e.clientX - sx)));
const newBottom = Math.max(0, Math.min(window.innerHeight - panel.offsetHeight, sb - (e.clientY - sy)));
panel.style.left = newLeft + "px";
panel.style.bottom = newBottom + "px";
panel.style.right = "unset";
});
document.addEventListener("mouseup", () => { drag = false; panel.classList.remove("dragging"); });
})();
// Draggable trace panel
(function() {
const panel = document.getElementById("trace-panel");
const handle = document.getElementById("trace-handle");
let isDragging = false, startX, startY, startRight, startBottom;
handle.addEventListener("mousedown", e => {
isDragging = true;
panel.classList.add("dragging");
const rect = panel.getBoundingClientRect();
startX = e.clientX;
startY = e.clientY;
startRight = window.innerWidth - rect.right;
startBottom = window.innerHeight - rect.bottom;
e.preventDefault();
});
document.addEventListener("mousemove", e => {
if (!isDragging) return;
const dx = startX - e.clientX;
const dy = startY - e.clientY;
const newRight = Math.max(0, Math.min(window.innerWidth - panel.offsetWidth, startRight + dx));
const newBottom = Math.max(0, Math.min(window.innerHeight - panel.offsetHeight, startBottom + dy));
panel.style.right = newRight + "px";
panel.style.bottom = newBottom + "px";
});
document.addEventListener("mouseup", () => {
isDragging = false;
panel.classList.remove("dragging");
});
// Touch support
handle.addEventListener("touchstart", e => {
const t = e.touches[0];
const rect = panel.getBoundingClientRect();
startX = t.clientX;
startY = t.clientY;
startRight = window.innerWidth - rect.right;
startBottom = window.innerHeight - rect.bottom;
}, { passive: true });
handle.addEventListener("touchmove", e => {
const t = e.touches[0];
const dx = startX - t.clientX;
const dy = startY - t.clientY;
const newRight = Math.max(0, Math.min(window.innerWidth - panel.offsetWidth, startRight + dx));
const newBottom = Math.max(0, Math.min(window.innerHeight - panel.offsetHeight, startBottom + dy));
panel.style.right = newRight + "px";
panel.style.bottom = newBottom + "px";
}, { passive: true });
})();
loadStats();
</script>
</body>
</html> |