/* THEME */ const html = document.documentElement; const themeBtn = document.getElementById('themeBtn'); function applyTheme(t){ html.dataset.theme=t; themeBtn.textContent=t==='dark'?'☀':'☾'; localStorage.setItem('cortex-theme',t); } const stored=localStorage.getItem('cortex-theme'); const prefersDark=window.matchMedia('(prefers-color-scheme: dark)').matches; applyTheme(stored||(prefersDark?'dark':'light')); themeBtn.addEventListener('click',()=>applyTheme(html.dataset.theme==='dark'?'light':'dark')); window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change',e=>{ if(!localStorage.getItem('cortex-theme')) applyTheme(e.matches?'dark':'light'); }); /* RESPONSIVE NAV BINDINGS */ const sidebar = document.getElementById('sidebar'); const mobileNavToggles = document.querySelectorAll('.mobile-nav-toggle'); const mobileOverlay = document.getElementById('mobileOverlay'); function openMobileSidebar() { sidebar.classList.add('mobile-open'); mobileOverlay.classList.add('show'); } function closeMobilePanels() { sidebar.classList.remove('mobile-open'); sourcesPanel.classList.remove('mobile-open'); mobileOverlay.classList.remove('show'); } mobileNavToggles.forEach(btn => btn.addEventListener('click', openMobileSidebar)); mobileOverlay.addEventListener('click', closeMobilePanels); /* COLLAPSIBLE SIDEBARS (Desktop) & Sources Panel */ const sidebarToggle=document.getElementById('sidebarToggle'); const sourcesPanel=document.getElementById('sourcesPanel'); const sourcesToggle=document.getElementById('sourcesToggle'); const mobileSourcesBtn=document.getElementById('mobileSourcesBtn'); sidebarToggle.addEventListener('click',()=>{ const c=sidebar.classList.toggle('collapsed'); sidebarToggle.textContent=c?'▶':'◀'; sidebarToggle.title=c?'Expand':'Collapse'; }); // For desktop sourcesToggle.addEventListener('click',()=>{ const c=sourcesPanel.classList.toggle('collapsed'); sourcesToggle.textContent=c?'◀':'▶'; sourcesToggle.title=c?'Expand':'Collapse'; }); // For mobile if(mobileSourcesBtn) { mobileSourcesBtn.addEventListener('click',()=>{ sourcesPanel.classList.add('mobile-open'); mobileOverlay.classList.add('show'); }); } /* NAV ROUTING */ document.querySelectorAll('.nav-item').forEach(item=>{ item.addEventListener('click',()=>{ document.querySelectorAll('.nav-item').forEach(n=>n.classList.remove('active')); document.querySelectorAll('.tab-panel').forEach(p=>p.classList.remove('active')); item.classList.add('active'); document.getElementById('tab-'+item.dataset.tab).classList.add('active'); if(item.dataset.tab==='eval') loadMetrics(); if(item.dataset.tab==='system') loadHealth(); // Auto-close sidebar on mobile after navigating if(window.innerWidth <= 768) { closeMobilePanels(); } }); }); /* HEALTH */ async function checkHealth(){ try{ const r=await fetch('/health'); const d=await r.json(); const dot=document.getElementById('statusDot'); const lbl=document.getElementById('statusLabel'); if(d.status==='ok'){dot.className='status-dot ok';lbl.textContent=(d.collection_stats?.entity_count??0)+' chunks';} else{dot.className='status-dot err';lbl.textContent='degraded';} }catch{document.getElementById('statusDot').className='status-dot err';document.getElementById('statusLabel').textContent='offline';} } checkHealth();setInterval(checkHealth,30000); /* TOAST */ function toast(msg,dur=3000){ const t=document.getElementById('toast');t.textContent=msg;t.classList.add('show'); clearTimeout(t._tid);t._tid=setTimeout(()=>t.classList.remove('show'),dur); } /* MARKED */ marked.setOptions({breaks:true,gfm:true}); function renderMarkdown(text){return marked.parse(text);} function linkifyCitations(html,n){ return html.replace(/\[(\d+)\]/g,(match,num)=>{ const i=parseInt(num); if(i<1||i>n) return match; return '['+i+']'; }); } function highlightSource(n){ const card=document.getElementById('src-card-'+n); if(!card) return; // Handle desktop collapse logic if(window.innerWidth > 768 && sourcesPanel.classList.contains('collapsed')){ sourcesPanel.classList.remove('collapsed'); sourcesToggle.textContent='▶'; } // Handle mobile slide-in logic if(window.innerWidth <= 768 && !sourcesPanel.classList.contains('mobile-open')){ sourcesPanel.classList.add('mobile-open'); mobileOverlay.classList.add('show'); } card.scrollIntoView({behavior:'smooth',block:'nearest'}); card.classList.remove('highlighted'); void card.offsetWidth; card.classList.add('highlighted'); setTimeout(()=>card.classList.remove('highlighted'),1500); } /* CHAT */ const chatMessages=document.getElementById('chatMessages'); const chatInput=document.getElementById('chatInput'); const sendBtn=document.getElementById('sendBtn'); const streamStatus=document.getElementById('streamStatus'); const sourcesList=document.getElementById('sourcesList'); let isStreaming=false; let currentChunks=[]; chatInput.addEventListener('input',()=>{ chatInput.style.height='auto'; chatInput.style.height=Math.min(chatInput.scrollHeight,150)+'px'; }); chatInput.addEventListener('keydown',e=>{if(e.key==='Enter'&&!e.shiftKey){e.preventDefault();sendMessage();}}); sendBtn.addEventListener('click',sendMessage); document.getElementById('clearChatBtn').addEventListener('click',()=>{ chatMessages.innerHTML='
cx
CORTEX
Cleared. Ask anything.
'; sourcesList.innerHTML='
Retrieved passages will appear here
'; streamStatus.textContent='';currentChunks=[]; }); function renderSourceCards(chunks){ currentChunks=chunks; if(!chunks.length) return; sourcesList.innerHTML=''; chunks.forEach((c,i)=>{ const n=i+1; const pct=Math.round(Math.min(Math.max(c.score,0),1)*100); const card=document.createElement('div'); card.className='source-card';card.id='src-card-'+n; card.innerHTML='
['+n+']
'+escHtml(c.title)+'
'+escHtml(c.text_snippet||'')+'
'+pct+'%
'; sourcesList.appendChild(card); }); } function buildSourcePills(chunks){ if(!chunks.length) return ''; const pills=chunks.map((c,i)=>{ const n=i+1; const snippet=(c.text_snippet||'').slice(0,175); const pct=Math.round(Math.min(Math.max(c.score,0),1)*100); const fname=(c.source||'').split('/').pop()||c.source; const title=c.title.slice(0,24)+(c.title.length>24?'…':''); return '['+n+'] '+escHtml(title)+'
'+escHtml(c.title)+'
'+escHtml(snippet)+(snippet.length>=175?'…':'')+'
'+escHtml(fname)+' · '+pct+'% relevance
'; }).join(''); return '
'+pills+'
'; } async function sendMessage(){ const query=chatInput.value.trim(); if(!query||isStreaming) return; chatInput.value='';chatInput.style.height='auto'; isStreaming=true;sendBtn.disabled=true;sendBtn.textContent='…';currentChunks=[]; // User bubble const ud=document.createElement('div');ud.className='message'; ud.innerHTML='
you
YOU
'+escHtml(query)+'
'; chatMessages.appendChild(ud); // AI bubble const ad=document.createElement('div');ad.className='message'; ad.innerHTML='
cx
CORTEX
'; chatMessages.appendChild(ad); chatMessages.scrollTop=chatMessages.scrollHeight; const liveText=document.getElementById('live-text'); const liveBadges=document.getElementById('live-badges'); const cursor=document.createElement('span');cursor.className='cursor-blink'; liveText.appendChild(cursor); let rawText=''; streamStatus.textContent='…'; try{ const resp=await fetch('/query/stream',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({query,top_k:10,stream:true,llm:{ provider: llmConfig.provider||null, model: llmConfig.model||null, api_key: llmConfig.api_key||null, // Only send base_url for custom — server ignores it for known providers base_url: (llmConfig.provider==='custom' && llmConfig.base_url) ? llmConfig.base_url : null, }})}); if(!resp.ok) throw new Error('HTTP '+resp.status); const reader=resp.body.getReader(); const decoder=new TextDecoder(); let buf=''; while(true){ const{done,value}=await reader.read(); if(done) break; buf+=decoder.decode(value,{stream:true}); const lines=buf.split('\n');buf=lines.pop(); for(const line of lines){ if(!line.startsWith('data: ')) continue; let evt;try{evt=JSON.parse(line.slice(6));}catch{continue;} if(evt.type==='chunk_meta'){ const chunks=evt.chunks||[]; const routing=evt.routing||{}; renderSourceCards(chunks); streamStatus.textContent='generating…'; if(routing.intent) addBadge(liveBadges,routing.intent,'amber'); (routing.strategies||[]).forEach(s=>addBadge(liveBadges,s.toUpperCase(),'blue')); } else if(evt.type==='crag_update'){ const gc={GOOD:'green',POOR:'amber',ABSENT:'red'}; addBadge(liveBadges,'CRAG: '+(evt.grade||''),gc[evt.grade]||'muted'); if(evt.web_search_used) addBadge(liveBadges,'🌐 web','red'); if(evt.rewritten_query) streamStatus.textContent='rewritten: "'+evt.rewritten_query.slice(0,50)+'…"'; } else if(evt.type==='token'){ const tok=evt.text||''; rawText+=tok; cursor.before(document.createTextNode(tok)); chatMessages.scrollTop=chatMessages.scrollHeight; } else if(evt.type==='sources'){ cursor.remove(); liveText.classList.remove('streaming'); liveText.innerHTML=linkifyCitations(renderMarkdown(rawText),currentChunks.length); liveText.insertAdjacentHTML('afterend',buildSourcePills(currentChunks)); streamStatus.textContent=''; chatMessages.scrollTop=chatMessages.scrollHeight; } else if(evt.type==='done'){ if(cursor.isConnected){ cursor.remove(); liveText.classList.remove('streaming'); liveText.innerHTML=linkifyCitations(renderMarkdown(rawText),currentChunks.length); if(currentChunks.length) liveText.insertAdjacentHTML('afterend',buildSourcePills(currentChunks)); } streamStatus.textContent=''; } else if(evt.type==='error'){ cursor.remove(); liveText.textContent='Error: '+evt.message; liveText.style.color='var(--red)'; streamStatus.textContent=''; } } } }catch(err){ cursor.remove(); liveText.textContent='Connection error: '+err.message; liveText.style.color='var(--red)'; streamStatus.textContent=''; } liveText.removeAttribute('id');liveBadges.removeAttribute('id'); isStreaming=false;sendBtn.disabled=false;sendBtn.textContent='send'; chatMessages.scrollTop=chatMessages.scrollHeight; } function addBadge(container,text,color){ const b=document.createElement('span');b.className='badge badge-'+color;b.textContent=text;container.appendChild(b); } function escHtml(s){return String(s).replace(/&/g,'&').replace(//g,'>').replace(/"/g,'"');} /* MODEL SELECTOR */ let providers = []; let llmConfig = JSON.parse(localStorage.getItem('cortex-llm') || 'null') || {provider:'nvidia_nim',model:'openai/gpt-oss-120b',api_key:'',base_url:''}; // Migration: clear stale base_url from known providers stored by older UI versions if(llmConfig.base_url && llmConfig.provider !== 'custom') { llmConfig.base_url = ''; localStorage.setItem('cortex-llm', JSON.stringify(llmConfig)); } let pendingConfig = {...llmConfig}; async function loadProviders(){ try{ const r=await fetch('/providers'); const d=await r.json(); providers=d.providers; if(!providers.find(p=>p.id===llmConfig.provider)){llmConfig.provider=d.default_provider;llmConfig.model=d.default_model;} renderProviderGrid();updateModelPill(); }catch(e){ document.getElementById('modelPillLabel').textContent='no API'; document.getElementById('modelDot').className='model-pill-dot unconfigured'; } } function renderProviderGrid(){ const grid=document.getElementById('providerGrid');grid.innerHTML=''; providers.forEach(p=>{ const c=document.createElement('div'); c.className='provider-card'+(p.id===pendingConfig.provider?' selected':'')+(p.configured?'':' unconfigured'); c.dataset.pid=p.id; c.innerHTML='
'+escHtml(p.label)+'
'+( p.configured?'● configured':'○ no key')+'
'; c.addEventListener('click',()=>selectProvider(p.id)); grid.appendChild(c); }); } function selectProvider(pid){ pendingConfig.provider=pid;pendingConfig.base_url=''; const p=providers.find(x=>x.id===pid); const sel=document.getElementById('modelSelect'); const cust=document.getElementById('modelCustomInput'); const baseRow=document.getElementById('customBaseRow'); if(p&&p.models.length>0){ sel.style.display='';cust.style.display='none'; sel.innerHTML=p.models.map(m=>'').join(''); const prev=pendingConfig.model; if(p.models.find(m=>m.id===prev)) sel.value=prev; pendingConfig.model=sel.value; }else{ sel.style.display='none';cust.style.display='';cust.value=pendingConfig.model||''; } baseRow.style.display=pid==='custom'?'':'none'; document.querySelectorAll('.provider-card').forEach(c=>c.classList.toggle('selected',c.dataset.pid===pid)); updateFooterInfo(pid); } function updateFooterInfo(pid){ const p=providers.find(x=>x.id===pid); const info=document.getElementById('popFooterInfo');if(!p) return; if(p.id==='custom') info.textContent='point to any OpenAI-compatible server'; else if(!p.configured) info.textContent='add '+pid.toUpperCase()+'_API_KEY to .env'; else info.textContent=p.base_url.replace('https://',''); } function updateModelPill(){ const p=providers.find(x=>x.id===llmConfig.provider); const lbl=document.getElementById('modelPillLabel'); const dot=document.getElementById('modelDot'); const shortModel=llmConfig.model?llmConfig.model.split('/').pop():'—'; lbl.textContent=(p?p.label:llmConfig.provider)+' · '+shortModel; dot.className='model-pill-dot'+(p&&p.configured?'':' unconfigured'); } document.getElementById('modelPill').addEventListener('click',e=>{ e.stopPropagation(); const pop=document.getElementById('modelPopover'); const pill=document.getElementById('modelPill'); const isOpen=pop.classList.toggle('open'); pill.classList.toggle('open',isOpen); if(isOpen){ pendingConfig={...llmConfig}; renderProviderGrid();selectProvider(pendingConfig.provider); document.getElementById('apiKeyInput').value=llmConfig.api_key||''; document.getElementById('customBaseInput').value=llmConfig.base_url||''; if(window.innerWidth<=600){ const bd=document.createElement('div'); bd.id='modelBackdrop'; bd.style.cssText='position:fixed;inset:0;background:rgba(0,0,0,0.45);z-index:499'; bd.addEventListener('click',closeModelPopover); document.body.appendChild(bd); } } else { closeModelPopover(); } }); function closeModelPopover(){ document.getElementById('modelPopover').classList.remove('open'); document.getElementById('modelPill').classList.remove('open'); const bd=document.getElementById('modelBackdrop'); if(bd) bd.remove(); } document.getElementById('popCloseBtn').addEventListener('click',closeModelPopover); document.addEventListener('click',e=>{ if(!document.getElementById('modelSelector').contains(e.target)){ closeModelPopover(); } }); document.getElementById('modelSelect').addEventListener('change',e=>{pendingConfig.model=e.target.value;}); document.getElementById('modelCustomInput').addEventListener('input',e=>{pendingConfig.model=e.target.value.trim();}); document.getElementById('applyModelBtn').addEventListener('click',()=>{ const sel=document.getElementById('modelSelect'); const cust=document.getElementById('modelCustomInput'); const p=providers.find(x=>x.id===pendingConfig.provider); pendingConfig.model=(p&&p.models.length>0)?sel.value:cust.value.trim(); if(!pendingConfig.model){toast('Enter a model id');return;} llmConfig={ provider: pendingConfig.provider, model: pendingConfig.model, api_key: document.getElementById('apiKeyInput').value.trim(), // Only store base_url for custom provider — for known providers the // server always uses its own registry URL, so storing it causes stale // URLs to be sent across provider switches. base_url: pendingConfig.provider === 'custom' ? document.getElementById('customBaseInput').value.trim() : '', }; localStorage.setItem('cortex-llm',JSON.stringify(llmConfig)); updateModelPill(); closeModelPopover(); toast('✓ Using '+(p?p.label:llmConfig.provider)+' · '+llmConfig.model.split('/').pop()); }); loadProviders(); /* INGEST TABS */ document.querySelectorAll('.ingest-tab').forEach(tab=>{ tab.addEventListener('click',()=>{ const sec=tab.dataset.section; document.querySelectorAll('.ingest-tab').forEach(t=>t.classList.remove('active')); document.querySelectorAll('.ingest-section').forEach(s=>s.classList.remove('active')); tab.classList.add('active'); document.getElementById('ingest-section-'+sec).classList.add('active'); }); }); /* FILE UPLOAD */ let selectedFiles=[]; function fmtSize(b){ if(b<1024) return b+'B'; if(b<1048576) return (b/1024).toFixed(1)+'KB'; return (b/1048576).toFixed(1)+'MB'; } function renderFileList(){ const list=document.getElementById('fileList'); const count=document.getElementById('uploadCount'); const uploadBtn=document.getElementById('uploadBtn'); const clearBtn=document.getElementById('clearFilesBtn'); list.innerHTML=selectedFiles.map((f,i)=> '
'+escHtml(f.name)+''+fmtSize(f.size)+'
' ).join(''); list.querySelectorAll('.file-item-remove').forEach(btn=>{ btn.addEventListener('click',()=>{ selectedFiles.splice(parseInt(btn.dataset.i),1); renderFileList(); }); }); uploadBtn.disabled=selectedFiles.length===0; clearBtn.style.display=selectedFiles.length?'':'none'; count.textContent=selectedFiles.length?selectedFiles.length+' file'+(selectedFiles.length>1?'s':'')+' selected':''; } function addFiles(newFiles){ const allowed=new Set(['.pdf','.html','.htm','.txt','.md']); Array.from(newFiles).forEach(f=>{ const ext=f.name.slice(f.name.lastIndexOf('.')).toLowerCase(); if(!allowed.has(ext)){toast('Skipped '+f.name+' — unsupported type');return;} if(!selectedFiles.find(x=>x.name===f.name&&x.size===f.size)) selectedFiles.push(f); }); renderFileList(); } const dropZone=document.getElementById('dropZone'); const fileInput=document.getElementById('fileInput'); dropZone.addEventListener('click',()=>fileInput.click()); fileInput.addEventListener('change',()=>{addFiles(fileInput.files);fileInput.value='';}); dropZone.addEventListener('dragover',e=>{e.preventDefault();dropZone.classList.add('drag-over');}); dropZone.addEventListener('dragleave',()=>dropZone.classList.remove('drag-over')); dropZone.addEventListener('drop',e=>{ e.preventDefault();dropZone.classList.remove('drag-over'); addFiles(e.dataTransfer.files); }); document.getElementById('clearFilesBtn').addEventListener('click',()=>{selectedFiles=[];renderFileList();}); document.getElementById('uploadBtn').addEventListener('click',async()=>{ if(!selectedFiles.length) return; const btn=document.getElementById('uploadBtn'); const prog=document.getElementById('uploadProgress'); const res=document.getElementById('ingestResult'); btn.disabled=true;btn.textContent='uploading…';prog.style.display='block';res.style.display='none'; const form=new FormData(); selectedFiles.forEach(f=>form.append('files',f,f.name)); try{ const r=await fetch('/ingest/upload',{method:'POST',body:form}); if(!r.ok){const e=await r.json();throw new Error(e.detail||'Upload failed');} const d=await r.json(); prog.style.display='none';res.style.display='block'; showIngestResult(d,'upload: '+selectedFiles.map(f=>f.name).join(', ')); selectedFiles=[];renderFileList();checkHealth(); }catch(err){ prog.style.display='none'; toast('Error: '+err.message); btn.disabled=false;btn.textContent='upload & ingest'; } btn.disabled=false;btn.textContent='upload & ingest'; }); /* SERVER PATH INGEST */ document.getElementById('ingestBtn').addEventListener('click',async()=>{ const path=document.getElementById('ingestPath').value.trim(); if(!path){toast('Enter a server path first');return;} const recursive=document.getElementById('ingestRecursive').checked; const btn=document.getElementById('ingestBtn'); const prog=document.getElementById('ingestProgress'); const res=document.getElementById('ingestResult'); btn.disabled=true;btn.textContent='running…';prog.style.display='block';res.style.display='none'; try{ const r=await fetch('/ingest',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({path,recursive})}); const d=await r.json(); prog.style.display='none';res.style.display='block'; showIngestResult(d,path);checkHealth(); }catch(err){prog.style.display='none';toast('Error: '+err.message);} btn.disabled=false;btn.textContent='run ingestion'; }); function showIngestResult(d,label){ const res=document.getElementById('ingestResult'); res.style.display='block'; const errHtml=(d.errors||[]).map(e=>'
⚠ '+escHtml(e.source)+': '+escHtml(e.error)+'
').join(''); res.innerHTML='

ingestion complete

'+d.documents_processed+'
DOCS
'+d.chunks_stored+'
CHUNKS
'+(d.bm25_indexed||0)+'
BM25
'+d.documents_skipped+'
SKIPPED
'+(d.graph_entities||0)+'
ENTITIES
'+(d.graph_triples||0)+'
TRIPLES
'+(errHtml?'
'+errHtml+'
':''); const ll=document.getElementById('ingestLogList'); const le=document.createElement('div');le.className='log-entry'; le.innerHTML=''+new Date().toLocaleTimeString()+''+escHtml(label.slice(0,60))+' → '+d.chunks_stored+' chunks'; ll.prepend(le); toast('✓ '+d.documents_processed+' docs, '+d.chunks_stored+' chunks'); } /* EVAL */ document.getElementById('refreshMetrics').addEventListener('click',loadMetrics); document.getElementById('flushCache').addEventListener('click',async()=>{ try{const r=await fetch('/cache/flush',{method:'POST'});const d=await r.json();toast('Cache flushed — '+d.deleted+' entries');loadMetrics();}catch{toast('Flush failed');} }); async function loadMetrics(){ const body=document.getElementById('evalBody'); body.innerHTML='
loading…
'; try{const r=await fetch('/metrics?limit=50&days=14');const d=await r.json();renderEvalDashboard(d);} catch(err){body.innerHTML='
Error: '+escHtml(err.message)+'
';} } function renderEvalDashboard(d){ const body=document.getElementById('evalBody'); const s=d.summary||{};const cache=d.cache||{};const recent=d.recent||[]; const grades=s.crag_grade_dist||{};const totalG=Object.values(grades).reduce((a,b)=>a+b,0)||1; const fmt=v=>(v!=null&&!isNaN(v))?Number(v).toFixed(2):'—'; const kpi='
'+(s.total_queries??0)+'
QUERIES
'+fmt(s.avg_faithfulness)+'
FAITHFULNESS
'+fmt(s.avg_answer_relevancy)+'
RELEVANCY
'+fmt(s.avg_context_precision)+'
CTX PRECISION
'+(s.avg_latency_ms?Math.round(s.avg_latency_ms)+'ms':'—')+'
AVG LATENCY
'+( cache.enabled?Math.round((cache.hit_rate||0)*100)+'%':'off')+'
CACHE HIT
'; const mbars=[['faithfulness',s.avg_faithfulness,'#34d399'],['answer_relevancy',s.avg_answer_relevancy,'#60a5fa'],['ctx_precision',s.avg_context_precision,'#a78bfa'],['chunk_score',s.avg_chunk_score,'#f59e0b']].map(([name,val,color])=>{ const pct=val!=null?Math.round(val*100):0; return '
'+name+'
'+fmt(val)+'
'; }).join(''); const gbars=['GOOD','POOR','ABSENT'].map(g=>{ const cnt=grades[g]||0;const pct=Math.round((cnt/totalG)*100); return '
'+g+'
'+(cnt||'')+'
'; }).join(''); const cacheInfo=cache.enabled?'
'+(cache.hits??0)+'HITS
'+(cache.misses??0)+'MISSES
'+(cache.ttl_s?Math.round(cache.ttl_s/60)+'m':'—')+'TTL
':'Redis not connected'; const stratDist=s.strategy_dist||{};const stTotal=Object.values(stratDist).reduce((a,b)=>a+b,0)||1; const stratBars=Object.entries(stratDist).map(([k,v])=>{ let label=k;try{label=JSON.parse(k).join('+').toUpperCase();}catch{} const pct=Math.round((v/stTotal)*100); return '
'+escHtml(label)+'
'+v+'
'; }).join('')||'No data'; const gc={GOOD:'green',POOR:'amber',ABSENT:'red'}; const tableRows=recent.slice(0,30).map(r=>''+escHtml(r.query)+''+(r.intent||'—')+''+(r.crag_grade?''+escHtml(r.crag_grade)+'':'—')+''+fmt(r.faithfulness)+''+fmt(r.answer_relevancy)+''+(r.latency_ms?Math.round(r.latency_ms)+'ms':'—')+'').join(''); body.innerHTML=kpi+'

ragas metrics

'+(mbars||'No data yet')+'

crag grade distribution

'+gbars+'

cache

'+cacheInfo+'

retrieval strategy mix

'+stratBars+'

recent queries

'+(tableRows?''+tableRows+'
QueryIntentCRAGFaithfulRelevancyLatency
':'
No queries yet.
')+'
'; } /* SYSTEM */ document.getElementById('refreshSystem').addEventListener('click',loadHealth); async function loadHealth(){ const body=document.getElementById('systemBody'); try{ const r=await fetch('/health');const d=await r.json(); const cs=d.collection_stats||{};const gs=d.graph_stats||{}; body.innerHTML='
STATUS
'+d.status+'
MILVUS
'+( d.milvus==='ok'?'●':'✕')+'
'+(cs.entity_count??0)+' chunks
EMBEDDER
'+escHtml(d.embedder)+'
GRAPH NODES
'+(gs.nodes??'—')+'
'+(gs.edges??0)+' edges · '+escHtml(gs.extractor??'—')+'
COLLECTION
'+escHtml(cs.collection??'—')+'
CHUNKS
'+(cs.entity_count??0)+'
'+escHtml(JSON.stringify(d,null,2))+'
'; }catch(err){body.innerHTML='
Error: '+escHtml(err.message)+'
';} }