/* 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||'')+'
';
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
';
chatMessages.appendChild(ud);
// AI bubble
const ad=document.createElement('div');ad.className='message';
ad.innerHTML='cx
';
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 '';
}).join('');
const gbars=['GOOD','POOR','ABSENT'].map(g=>{
const cnt=grades[g]||0;const pct=Math.round((cnt/totalG)*100);
return '';
}).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 '';
}).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?'
| Query | Intent | CRAG | Faithful | Relevancy | Latency |
'+tableRows+'
':'
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='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)+'
';}
}