| <!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; } |
| |
| |
| @keyframes blink { 0%,100%{opacity:1} 50%{opacity:0} } |
| .cursor::after { content:'|'; animation: blink 1s infinite; margin-left:2px; color:#619eff; } |
| |
| |
| @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; } |
| |
| |
| @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 { max-height: 180px; overflow-y: auto; } |
| .paper-list::-webkit-scrollbar { width: 2px; } |
| .paper-list::-webkit-scrollbar-thumb { background: #3c495b; } |
| |
| |
| #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; } |
| |
| |
| @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; |
| } |
| |
| |
| #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); } |
| |
| |
| #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"> |
|
|
| |
| <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"> |
|
|
| |
| <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> |
|
|
| |
| <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> |
|
|
| |
| <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> |
|
|
| |
| <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 class="flex-1 bg-surface-container-low min-h-full"> |
| <div class="max-w-4xl mx-auto px-6 py-12"> |
|
|
| |
| <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> |
|
|
| |
| <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> |
|
|
| |
| <div id="results-area" class="space-y-6 hidden"> |
|
|
| |
| <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> |
|
|
| |
| <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> |
|
|
| |
| <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> |
|
|
| |
| <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> |
|
|
| |
| <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> |
|
|
| |
| <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> |
|
|
| |
| <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> |
|
|
| |
| <div id="trace-panel" class="hidden lg:block"> |
| <div class="bg-[#0a0e14] border border-outline-variant p-4"> |
| |
| <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> |
| |
| let sourcesOpen = true; |
| const API_BASE = window.location.origin; |
| |
| |
| 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); |
| } |
| } |
| |
| |
| 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 }, |
| ]); |
| } |
| |
| |
| 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" }, |
| }; |
| |
| |
| |
| 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) { } |
| } |
| } |
| |
| 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"); |
| |
| |
| 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}`; |
| |
| |
| document.getElementById("meta-line").textContent = |
| `Lat: ${data.latency_ms}ms | Tokens: ~${data.tokens} | Sim: ${data.avg_sim}`; |
| |
| |
| 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; |
| } |
| |
| |
| 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); |
| }); |
| |
| |
| sourcesOpen = true; |
| list.classList.remove("hidden"); |
| document.getElementById("sources-chevron").textContent = "expand_less"; |
| |
| |
| 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"); |
| } |
| } |
| |
| |
| 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); |
| }); |
| } |
| |
| |
| document.addEventListener("keydown", e => { |
| if ((e.metaKey || e.ctrlKey) && e.key === "Enter") runQuery(); |
| }); |
| document.getElementById("search-btn").addEventListener("click", runQuery); |
| |
| |
| |
| |
| (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"); }); |
| })(); |
| |
| |
| (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"); |
| }); |
| |
| |
| 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> |