Observer KB UI: now shows LLM Team extracted facts + entities

- Knowledge tab: stats (categories, verified facts, entities, decisions, insights, questions),
  auto-generated categories, key decisions, key insights
- Facts & Entities tab (NEW): 279 extracted facts from LLM Team extract mode,
  264 entities as clickable chips (click → search), verified entries from secondary model
- Search tab: semantic HNSW search across all knowledge
- Ask AI tab: RAG with cited sources
- Ingest tab: upload with progress
- Entity chips link to semantic search
- Category drill-down shows related content via HNSW

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
root
2026-03-28 00:25:39 -05:00
co-authored by Claude Opus 4.6
parent 3f19ad5d93
commit 4d64e4a618
+166 -228
View File
@@ -3,85 +3,65 @@
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Observer KB — Knowledge Base</title> <title>Observer KB</title>
<style> <style>
:root{--bg:#0a0a0f;--s1:#12121a;--s2:#1a1a28;--b:#2a2a3a;--t:#e0e0e8;--d:#888898;--a:#6c5ce7;--a2:#4a3db8;--ok:#00d2a0;--err:#ff6b6b;--warn:#ffa94d;--f:'JetBrains Mono','Fira Code',monospace} :root{--bg:#0a0a0f;--s1:#12121a;--s2:#1a1a28;--b:#2a2a3a;--t:#e0e0e8;--d:#888898;--a:#6c5ce7;--ok:#00d2a0;--err:#ff6b6b;--warn:#ffa94d;--f:'JetBrains Mono','Fira Code',monospace}
*{margin:0;padding:0;box-sizing:border-box}body{font-family:var(--f);background:var(--bg);color:var(--t);font-size:13px;min-height:100vh} *{margin:0;padding:0;box-sizing:border-box}body{font-family:var(--f);background:var(--bg);color:var(--t);font-size:13px;min-height:100vh}
.app{max-width:1200px;margin:0 auto;padding:20px} .app{max-width:1200px;margin:0 auto;padding:20px}
header{display:flex;align-items:center;justify-content:space-between;padding:16px 0;border-bottom:1px solid var(--b);margin-bottom:20px} header{display:flex;align-items:center;gap:20px;padding:16px 0;border-bottom:1px solid var(--b);margin-bottom:20px}
header h1{font-size:20px;color:var(--a)} header h1{font-size:20px;color:var(--a)}
.tabs{display:flex;gap:2px;margin-bottom:20px} .tabs{display:flex;gap:2px}.tab{background:none;border:none;color:var(--d);font-family:var(--f);font-size:12px;padding:10px 16px;cursor:pointer;border-bottom:2px solid transparent}
.tab{background:none;border:none;color:var(--d);font-family:var(--f);font-size:12px;padding:10px 18px;cursor:pointer;border-bottom:2px solid transparent}
.tab:hover{color:var(--t)}.tab.active{color:var(--a);border-bottom-color:var(--a)} .tab:hover{color:var(--t)}.tab.active{color:var(--a);border-bottom-color:var(--a)}
.hdr-stats{margin-left:auto;font-size:11px;color:var(--d)}
.btn{background:var(--a);color:white;border:none;padding:8px 16px;border-radius:6px;font-family:var(--f);font-size:12px;cursor:pointer;font-weight:600} .btn{background:var(--a);color:white;border:none;padding:8px 16px;border-radius:6px;font-family:var(--f);font-size:12px;cursor:pointer;font-weight:600}
.btn:hover{opacity:.9}.btn:disabled{opacity:.5} .btn:hover{opacity:.9}.btn:disabled{opacity:.5}.btn-sm{padding:5px 10px;font-size:11px}
.btn-sm{padding:5px 10px;font-size:11px}.btn-ok{background:var(--ok)}.btn-warn{background:var(--warn);color:#000}
.hidden{display:none} .hidden{display:none}
.empty{color:var(--d);text-align:center;padding:40px;font-size:12px} .stats-bar{display:grid;grid-template-columns:repeat(auto-fill,minmax(130px,1fr));gap:8px;margin-bottom:20px}
.stat{background:var(--s1);border:1px solid var(--b);border-radius:8px;padding:12px;text-align:center}
/* Stats bar */ .stat-val{font-size:22px;font-weight:700;color:var(--a)}.stat-label{font-size:10px;color:var(--d);text-transform:uppercase;margin-top:2px}
.stats-bar{display:grid;grid-template-columns:repeat(auto-fill,minmax(140px,1fr));gap:8px;margin-bottom:24px} .section{margin-bottom:24px}.section h2{font-size:13px;color:var(--d);text-transform:uppercase;letter-spacing:.08em;margin-bottom:10px;border-bottom:1px solid var(--b);padding-bottom:6px}
.stat{background:var(--s1);border:1px solid var(--b);border-radius:8px;padding:14px;text-align:center} .entry{background:var(--s1);border:1px solid var(--b);border-radius:8px;padding:14px;margin-bottom:8px}
.stat-val{font-size:24px;font-weight:700;color:var(--a)}.stat-label{font-size:10px;color:var(--d);text-transform:uppercase;margin-top:2px} .badge{font-size:9px;text-transform:uppercase;letter-spacing:.08em;padding:2px 8px;border-radius:3px;display:inline-block;margin-bottom:6px;font-weight:600}
.badge-fact{background:var(--a);color:white}.badge-entity{background:var(--ok);color:white}
/* Category cards */ .badge-decision{background:#e67e22;color:white}.badge-insight{background:#3498db;color:white}
.cat-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(280px,1fr));gap:10px;margin-bottom:24px} .badge-question{background:var(--warn);color:#000}.badge-verified{background:var(--ok);color:white}
.cat-card{background:var(--s1);border:1px solid var(--b);border-radius:8px;padding:16px;cursor:pointer;transition:all .15s;border-left:3px solid var(--a)} .entry-text{font-size:12px;line-height:1.6}.entry-src{font-size:10px;color:var(--d);margin-top:4px}
.cat-card:hover{border-color:var(--a)} .cat-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(250px,1fr));gap:8px;margin-bottom:16px}
.cat-title{font-weight:600;font-size:14px;margin-bottom:4px}.cat-count{font-size:11px;color:var(--d)} .cat-card{background:var(--s1);border:1px solid var(--b);border-radius:8px;padding:14px;cursor:pointer;border-left:3px solid var(--a);transition:all .15s}
.cat-sources{font-size:10px;color:var(--d);margin-top:6px} .cat-card:hover{border-color:var(--a)}.cat-title{font-weight:600;font-size:13px}.cat-count{font-size:10px;color:var(--d);margin-top:2px}
/* Knowledge entries */
.entry{background:var(--s1);border:1px solid var(--b);border-radius:8px;padding:16px;margin-bottom:10px}
.entry-type{font-size:9px;text-transform:uppercase;letter-spacing:.1em;padding:2px 8px;border-radius:3px;display:inline-block;margin-bottom:6px}
.entry-type.decision{background:var(--a);color:white}
.entry-type.insight{background:var(--ok);color:white}
.entry-type.question{background:var(--warn);color:#000}
.entry-type.topic{background:var(--s2);color:var(--d)}
.entry-content{font-size:13px;line-height:1.7;margin-bottom:6px}
.entry-source{font-size:10px;color:var(--d)}
.entry-actions{display:flex;gap:6px;margin-top:8px}
/* Search */
.search-row{display:flex;gap:8px;margin-bottom:16px} .search-row{display:flex;gap:8px;margin-bottom:16px}
.search-row input{flex:1;background:var(--s1);border:2px solid var(--b);color:var(--t);font-family:var(--f);font-size:14px;padding:12px;border-radius:8px;outline:none} .search-row input{flex:1;background:var(--s1);border:2px solid var(--b);color:var(--t);font-family:var(--f);font-size:14px;padding:12px;border-radius:8px;outline:none}
.search-row input:focus{border-color:var(--a)} .search-row input:focus{border-color:var(--a)}
.ask-input{width:100%;background:var(--s1);border:2px solid var(--b);color:var(--t);font-family:var(--f);font-size:13px;padding:12px;border-radius:8px;resize:vertical;min-height:80px;outline:none;margin-bottom:8px}
/* Ask AI */ .ask-input:focus{border-color:var(--a)}
.ask-area textarea{width:100%;background:var(--s1);border:2px solid var(--b);color:var(--t);font-family:var(--f);font-size:13px;padding:12px;border-radius:8px;resize:vertical;min-height:80px;outline:none;margin-bottom:8px}
.ask-area textarea:focus{border-color:var(--a)}
.ai-answer{background:var(--s2);border:1px solid var(--b);border-radius:8px;padding:16px;margin-top:12px;line-height:1.7;white-space:pre-wrap} .ai-answer{background:var(--s2);border:1px solid var(--b);border-radius:8px;padding:16px;margin-top:12px;line-height:1.7;white-space:pre-wrap}
.ai-meta{font-size:10px;color:var(--d);margin-top:8px} .ai-meta{font-size:10px;color:var(--d);margin-top:6px}
.upload-zone{border:2px dashed var(--b);border-radius:12px;padding:36px;text-align:center;cursor:pointer;margin-bottom:16px}
/* Upload */
.upload-zone{border:2px dashed var(--b);border-radius:12px;padding:40px;text-align:center;cursor:pointer;margin-bottom:16px}
.upload-zone:hover{border-color:var(--a);background:rgba(108,92,231,.03)} .upload-zone:hover{border-color:var(--a);background:rgba(108,92,231,.03)}
.upload-zone h3{margin-bottom:6px}.upload-zone p{color:var(--d);font-size:11px} .upload-zone h3{margin-bottom:4px}.upload-zone p{color:var(--d);font-size:11px}
.file-item{display:flex;align-items:center;justify-content:space-between;padding:8px 12px;border-bottom:1px solid var(--b);font-size:12px} .file-item{display:flex;align-items:center;justify-content:space-between;padding:8px 12px;border-bottom:1px solid var(--b);font-size:12px}
.done{color:var(--ok)}.failed{color:var(--err)}.uploading{color:var(--a)} .done{color:var(--ok)}.failed{color:var(--err)}.uploading{color:var(--a)}
.result{background:var(--s2);border:1px solid var(--b);border-radius:8px;padding:12px;margin-bottom:6px;border-left:3px solid var(--a)}
.section{margin-bottom:28px} .result-score{font-size:10px;color:var(--a);margin-bottom:2px}.result-text{font-size:12px;line-height:1.5}
.section h2{font-size:14px;color:var(--d);text-transform:uppercase;letter-spacing:.08em;margin-bottom:12px} .empty{color:var(--d);text-align:center;padding:32px}
.filter-bar{display:flex;gap:6px;flex-wrap:wrap;margin-bottom:12px}
.filter-btn{background:var(--s2);border:1px solid var(--b);color:var(--d);font-family:var(--f);font-size:11px;padding:4px 10px;border-radius:4px;cursor:pointer}
.filter-btn:hover,.filter-btn.active{border-color:var(--a);color:var(--a)}
</style> </style>
</head> </head>
<body> <body>
<div class="app"> <div class="app">
<header> <header>
<h1>OBSERVER KB</h1> <h1>OBSERVER KB</h1>
<div style="font-size:11px;color:var(--d)" id="hdr-info"></div>
</header>
<div class="tabs"> <div class="tabs">
<button class="tab active" onclick="showTab('knowledge')">Knowledge</button> <button class="tab active" onclick="showTab('knowledge')">Knowledge</button>
<button class="tab" onclick="showTab('explore')">Explore</button> <button class="tab" onclick="showTab('facts')">Facts &amp; Entities</button>
<button class="tab" onclick="showTab('ask')">Ask</button> <button class="tab" onclick="showTab('explore')">Search</button>
<button class="tab" onclick="showTab('ask')">Ask AI</button>
<button class="tab" onclick="showTab('upload')">Ingest</button> <button class="tab" onclick="showTab('upload')">Ingest</button>
</div> </div>
<div class="hdr-stats" id="hdr-stats"></div>
</header>
<div id="tab-knowledge"></div> <div id="tab-knowledge"></div>
<div id="tab-facts" class="hidden"></div>
<div id="tab-explore" class="hidden"></div> <div id="tab-explore" class="hidden"></div>
<div id="tab-ask" class="hidden"></div> <div id="tab-ask" class="hidden"></div>
<div id="tab-upload" class="hidden"></div> <div id="tab-upload" class="hidden"></div>
@@ -90,249 +70,208 @@ header h1{font-size:20px;color:var(--a)}
<script> <script>
const API=`http://${location.hostname}:3100`; const API=`http://${location.hostname}:3100`;
const $=id=>document.getElementById(id); const $=id=>document.getElementById(id);
async function q(sql){return(await(await fetch(API+'/query/sql',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({sql})})).json())}
async function searchHNSW(query,k=10){try{return(await(await fetch(API+'/vectors/hnsw/search',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({index_name:'observer_kb',query,top_k:k})})).json()).results||[]}catch{return[]}}
function showTab(name){ function showTab(name){
['knowledge','explore','ask','upload'].forEach(t=>{ ['knowledge','facts','explore','ask','upload'].forEach((t,i)=>{
$(('tab-'+t)).classList.toggle('hidden',t!==name); $('tab-'+t).classList.toggle('hidden',t!==name);
document.querySelectorAll('.tab')[['knowledge','explore','ask','upload'].indexOf(t)].classList.toggle('active',t===name); document.querySelectorAll('.tab')[i].classList.toggle('active',t===name);
}); });
if(name==='knowledge')loadKnowledge(); ({knowledge:loadKnowledge,facts:loadFacts,explore:loadExplore,ask:loadAsk,upload:loadUpload})[name]();
if(name==='explore')loadExplore();
if(name==='ask')loadAsk();
if(name==='upload')loadUpload();
} }
async function query(sql){ function makeEntry(type,text,source){
const r=await fetch(API+'/query/sql',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({sql})}); const e=document.createElement('div');e.className='entry';
return r.json(); const b=document.createElement('span');b.className='badge badge-'+type;b.textContent=type;
const t=document.createElement('div');t.className='entry-text';t.textContent=text;
const s=document.createElement('div');s.className='entry-src';s.textContent=source;
e.append(b,t,s);return e;
} }
async function searchKB(q,topk=10){ // === KNOWLEDGE (categories + decisions + insights) ===
try{
const r=await fetch(API+'/vectors/hnsw/search',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({index_name:'observer_kb',query:q,top_k:topk})});
return(await r.json()).results||[];
}catch{return[]}
}
async function askAI(question){
const r=await fetch(API+'/vectors/rag',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({index_name:'observer_kb',question,top_k:5})});
return r.json();
}
// === KNOWLEDGE TAB ===
async function loadKnowledge(){ async function loadKnowledge(){
const el=$('tab-knowledge');el.textContent='loading...'; const el=$('tab-knowledge');el.textContent='';
try{ try{
const [topics,decisions,insights,questions]=await Promise.all([ const[topics,decisions,insights,questions,extractions]=await Promise.all([
query("SELECT topic, COUNT(*) as n, COUNT(DISTINCT conversation) as convs FROM knowledge_topics GROUP BY topic ORDER BY n DESC LIMIT 20"), q("SELECT topic,COUNT(*) as n,COUNT(DISTINCT conversation) as c FROM knowledge_topics GROUP BY topic ORDER BY n DESC LIMIT 15"),
query("SELECT conversation, page, decision FROM knowledge_decisions ORDER BY conversation, page"), q("SELECT conversation,page,decision FROM knowledge_decisions LIMIT 12"),
query("SELECT conversation, page, insight FROM knowledge_insights ORDER BY conversation, page"), q("SELECT conversation,page,insight FROM knowledge_insights LIMIT 8"),
query("SELECT conversation, page, question FROM knowledge_questions ORDER BY conversation, page"), q("SELECT COUNT(*) as n FROM knowledge_questions"),
q("SELECT type,COUNT(*) as n FROM processed_extractions GROUP BY type"),
]); ]);
el.textContent=''; const factCount=extractions.rows.find(r=>r.type==='fact')?.n||0;
const entityCount=extractions.rows.find(r=>r.type==='entity')?.n||0;
// Stats // Stats
const statsBar=document.createElement('div');statsBar.className='stats-bar'; const sb=document.createElement('div');sb.className='stats-bar';
[ [{v:topics.row_count,l:'Categories'},{v:factCount,l:'Verified Facts'},{v:entityCount,l:'Entities'},{v:decisions.row_count,l:'Decisions'},{v:insights.row_count,l:'Insights'},{v:questions.rows[0]?.n||0,l:'Open Questions'}].forEach(s=>{
{v:topics.row_count,l:'Topics'},
{v:decisions.row_count,l:'Decisions'},
{v:insights.row_count,l:'Insights'},
{v:questions.row_count,l:'Open Questions'},
].forEach(s=>{
const d=document.createElement('div');d.className='stat'; const d=document.createElement('div');d.className='stat';
const v=document.createElement('div');v.className='stat-val';v.textContent=s.v; const v=document.createElement('div');v.className='stat-val';v.textContent=s.v;
const l=document.createElement('div');l.className='stat-label';l.textContent=s.l; const l=document.createElement('div');l.className='stat-label';l.textContent=s.l;
d.append(v,l);statsBar.appendChild(d); d.append(v,l);sb.appendChild(d);
}); });
el.appendChild(statsBar); el.appendChild(sb);
$('hdr-stats').textContent=factCount+' facts · '+entityCount+' entities · '+decisions.row_count+' decisions';
// Auto-generated categories (topics) // Categories
const catSection=document.createElement('div');catSection.className='section'; const cs=document.createElement('div');cs.className='section';
const catH=document.createElement('h2');catH.textContent='Auto-Generated Categories'; cs.insertAdjacentHTML('beforeend','<h2>Auto-Generated Categories</h2>');
catSection.appendChild(catH); const cg=document.createElement('div');cg.className='cat-grid';
const catGrid=document.createElement('div');catGrid.className='cat-grid';
topics.rows.forEach(t=>{ topics.rows.forEach(t=>{
const card=document.createElement('div');card.className='cat-card'; const c=document.createElement('div');c.className='cat-card';
const title=document.createElement('div');title.className='cat-title';title.textContent=t.topic; const n=document.createElement('div');n.className='cat-title';n.textContent=t.topic;
const count=document.createElement('div');count.className='cat-count';count.textContent=t.n+' mentions across '+t.convs+' conversations'; const m=document.createElement('div');m.className='cat-count';m.textContent=t.n+' mentions · '+t.c+' conversations';
card.append(title,count); c.append(n,m);c.onclick=()=>drillTopic(t.topic);cg.appendChild(c);
card.onclick=()=>drillTopic(t.topic);
catGrid.appendChild(card);
}); });
catSection.appendChild(catGrid); cs.appendChild(cg);el.appendChild(cs);
el.appendChild(catSection);
// Key Decisions // Decisions
const decSection=document.createElement('div');decSection.className='section'; const ds=document.createElement('div');ds.className='section';
const decH=document.createElement('h2');decH.textContent='Extracted Decisions'; ds.insertAdjacentHTML('beforeend','<h2>Key Decisions</h2>');
decSection.appendChild(decH); decisions.rows.forEach(d=>ds.appendChild(makeEntry('decision',d.decision,d.conversation+' p'+d.page)));
decisions.rows.slice(0,15).forEach(d=>{ el.appendChild(ds);
decSection.appendChild(makeEntry('decision',d.decision,d.conversation+' p'+d.page));
});
if(decisions.row_count>15){
const more=document.createElement('div');more.style.cssText='color:var(--d);font-size:11px;padding:8px';
more.textContent='+ '+(decisions.row_count-15)+' more decisions';
decSection.appendChild(more);
}
el.appendChild(decSection);
// Key Insights // Insights
const insSection=document.createElement('div');insSection.className='section'; const is=document.createElement('div');is.className='section';
const insH=document.createElement('h2');insH.textContent='Key Insights'; is.insertAdjacentHTML('beforeend','<h2>Key Insights</h2>');
insSection.appendChild(insH); insights.rows.forEach(d=>is.appendChild(makeEntry('insight',d.insight,d.conversation+' p'+d.page)));
insights.rows.slice(0,10).forEach(d=>{ el.appendChild(is);
insSection.appendChild(makeEntry('insight',d.insight,d.conversation+' p'+d.page));
});
el.appendChild(insSection);
// Open Questions
const qSection=document.createElement('div');qSection.className='section';
const qH=document.createElement('h2');qH.textContent='Open Questions';
qSection.appendChild(qH);
questions.rows.slice(0,10).forEach(d=>{
qSection.appendChild(makeEntry('question',d.question,d.conversation+' p'+d.page));
});
el.appendChild(qSection);
$('hdr-info').textContent=topics.row_count+' topics · '+decisions.row_count+' decisions · '+insights.row_count+' insights';
}catch(e){el.textContent='Error: '+e} }catch(e){el.textContent='Error: '+e}
} }
function makeEntry(type,content,source){
const entry=document.createElement('div');entry.className='entry';
const badge=document.createElement('span');badge.className='entry-type '+type;badge.textContent=type;
const text=document.createElement('div');text.className='entry-content';text.textContent=content;
const src=document.createElement('div');src.className='entry-source';src.textContent='Source: '+source;
entry.append(badge,text,src);
return entry;
}
async function drillTopic(topic){ async function drillTopic(topic){
const el=$('tab-knowledge');el.textContent='loading topic...'; const el=$('tab-knowledge');el.textContent='';
try{ const back=document.createElement('button');back.className='btn btn-sm';back.textContent='← Back';back.onclick=loadKnowledge;
// Find all entries for this topic const h=document.createElement('h2');h.style.cssText='font-size:18px;margin:12px 0';h.textContent=topic;
const related=await searchKB(topic,10); el.append(back,h);
const decisions=await query("SELECT conversation, page, decision FROM knowledge_decisions WHERE decision LIKE '%"+topic.replace(/'/g,"''")+"%' LIMIT 10"); const results=await searchHNSW(topic,10);
const s=document.createElement('div');s.className='section';
el.textContent=''; s.insertAdjacentHTML('beforeend','<h2>Related Content</h2>');
const back=document.createElement('button');back.className='btn btn-sm';back.textContent='Back';back.onclick=loadKnowledge; results.forEach(r=>{
const h2=document.createElement('h2');h2.style.cssText='font-size:18px;margin:12px 0';h2.textContent=topic; const e=document.createElement('div');e.className='result';
el.append(back,h2); const sc=document.createElement('div');sc.className='result-score';sc.textContent='['+r.score.toFixed(3)+'] '+r.doc_id;
const t=document.createElement('div');t.className='result-text';t.textContent=r.chunk_text.substring(0,300);
// Semantic matches e.append(sc,t);s.appendChild(e);
const secR=document.createElement('div');secR.className='section';
const hR=document.createElement('h2');hR.textContent='Related Content (Semantic)';secR.appendChild(hR);
related.forEach(r=>{
const entry=document.createElement('div');entry.className='entry';
const score=document.createElement('div');score.style.cssText='font-size:10px;color:var(--a);margin-bottom:4px';score.textContent='['+r.score.toFixed(3)+'] '+r.doc_id;
const text=document.createElement('div');text.className='entry-content';text.textContent=r.chunk_text.substring(0,300);
entry.append(score,text);secR.appendChild(entry);
}); });
el.appendChild(secR); el.appendChild(s);
}
// Related decisions // === FACTS & ENTITIES (from LLM Team extract mode) ===
if(decisions.rows.length){ async function loadFacts(){
const secD=document.createElement('div');secD.className='section'; const el=$('tab-facts');el.textContent='';
const hD=document.createElement('h2');hD.textContent='Related Decisions';secD.appendChild(hD); try{
decisions.rows.forEach(d=>secD.appendChild(makeEntry('decision',d.decision,d.conversation+' p'+d.page))); const[facts,entities,verified]=await Promise.all([
el.appendChild(secD); q("SELECT conversation,page,content FROM processed_extractions WHERE type='fact' ORDER BY conversation,page LIMIT 30"),
q("SELECT conversation,content,COUNT(*) as n FROM processed_extractions WHERE type='entity' GROUP BY conversation,content ORDER BY n DESC LIMIT 30"),
q("SELECT conversation,page,content FROM processed_validated LIMIT 10"),
]);
const fs=document.createElement('div');fs.className='section';
fs.insertAdjacentHTML('beforeend','<h2>Extracted Facts ('+facts.row_count+' total — via LLM Team Extract Mode)</h2>');
facts.rows.forEach(r=>fs.appendChild(makeEntry('fact',r.content,r.conversation+' p'+r.page)));
el.appendChild(fs);
const es=document.createElement('div');es.className='section';
es.insertAdjacentHTML('beforeend','<h2>Entities ('+entities.row_count+' unique)</h2>');
const eg=document.createElement('div');eg.style.cssText='display:flex;flex-wrap:wrap;gap:6px';
entities.rows.forEach(r=>{
const chip=document.createElement('span');
chip.style.cssText='background:var(--s2);border:1px solid var(--b);padding:4px 10px;border-radius:4px;font-size:11px;cursor:pointer';
chip.textContent=r.content+(r.n>1?' ('+r.n+')':'');
chip.onclick=()=>{showTab('explore');setTimeout(()=>{document.getElementById('searchInput').value=r.content;doSearch()},100)};
eg.appendChild(chip);
});
es.appendChild(eg);el.appendChild(es);
if(verified.rows.length){
const vs=document.createElement('div');vs.className='section';
vs.insertAdjacentHTML('beforeend','<h2>Verified by Secondary Model</h2>');
verified.rows.forEach(r=>vs.appendChild(makeEntry('verified',r.content,r.conversation+' p'+r.page)));
el.appendChild(vs);
} }
}catch(e){el.textContent='Error: '+e} }catch(e){el.textContent='Error: '+e}
} }
// === EXPLORE TAB === // === SEARCH ===
async function loadExplore(){ function loadExplore(){
const el=$('tab-explore');el.textContent=''; const el=$('tab-explore');el.textContent='';
const row=document.createElement('div');row.className='search-row'; const row=document.createElement('div');row.className='search-row';
const input=document.createElement('input');input.placeholder='Search knowledge by meaning...';input.id='exploreInput'; const input=document.createElement('input');input.id='searchInput';input.placeholder='Search by meaning across all knowledge...';
const btn=document.createElement('button');btn.className='btn';btn.textContent='Search'; const btn=document.createElement('button');btn.className='btn';btn.textContent='Search';
btn.onclick=doExplore;input.onkeydown=e=>{if(e.key==='Enter')doExplore()}; btn.onclick=doSearch;input.onkeydown=e=>{if(e.key==='Enter')doSearch()};
row.append(input,btn); row.append(input,btn);
const results=document.createElement('div');results.id='exploreResults'; const res=document.createElement('div');res.id='searchResults';
const hint=document.createElement('div');hint.style.cssText='color:var(--d);font-size:11px;margin-bottom:12px'; const hint=document.createElement('div');hint.className='empty';hint.textContent='Try: "object storage architecture", "data ingestion trade-offs", "CRM options"';
hint.textContent='Semantic search across all extracted knowledge. Try: "data lake architecture", "object storage trade-offs", "CRM options"'; el.append(row,res,hint);
el.append(row,hint,results);
} }
async function doSearch(){
async function doExplore(){ const query=document.getElementById('searchInput').value;if(!query)return;
const q=document.getElementById('exploreInput').value;if(!q)return; const el=$('searchResults');el.textContent='searching...';
const el=$('exploreResults');el.textContent='searching...'; const results=await searchHNSW(query,15);
try{
const results=await searchKB(q,15);
el.textContent=''; el.textContent='';
if(!results.length){el.textContent='No results';return} if(!results.length){el.textContent='No results';return}
results.forEach(r=>{ results.forEach(r=>{
const entry=document.createElement('div');entry.className='entry'; const e=document.createElement('div');e.className='result';
const score=document.createElement('div');score.style.cssText='font-size:10px;color:var(--a);margin-bottom:4px'; const s=document.createElement('div');s.className='result-score';s.textContent='['+r.score.toFixed(3)+'] '+r.doc_id.replace(/_/g,' ');
score.textContent='['+r.score.toFixed(3)+'] '+r.doc_id.replace(/_/g,' '); const t=document.createElement('div');t.className='result-text';t.textContent=r.chunk_text.substring(0,400);
const text=document.createElement('div');text.className='entry-content';text.textContent=r.chunk_text.substring(0,400); e.append(s,t);el.appendChild(e);
entry.append(score,text);el.appendChild(entry);
}); });
}catch(e){el.textContent='Error: '+e}
} }
// === ASK TAB === // === ASK AI ===
function loadAsk(){ function loadAsk(){
const el=$('tab-ask');el.textContent=''; const el=$('tab-ask');el.textContent='';
const area=document.createElement('div');area.className='ask-area'; const ta=document.createElement('textarea');ta.className='ask-input';ta.id='askInput';
const ta=document.createElement('textarea');ta.id='askInput'; ta.placeholder='Ask a question about your knowledge base...\n\nExamples:\n- What architecture decisions were made about Iceberg?\n- Compare Spark vs Flink approaches\n- What approaches were abandoned and why?';
ta.placeholder='Ask a question about your knowledge base...\n\nExamples:\n- What architecture decisions were made about object storage?\n- What approaches were tried and abandoned for data ingestion?\n- Summarize the key differences between Spark and Flink for Iceberg';
const btn=document.createElement('button');btn.className='btn';btn.id='askBtn';btn.textContent='Ask AI'; const btn=document.createElement('button');btn.className='btn';btn.id='askBtn';btn.textContent='Ask AI';
btn.onclick=doAsk; btn.onclick=doAsk;
const resp=document.createElement('div');resp.id='askResponse'; const resp=document.createElement('div');resp.id='askResp';
area.append(ta,btn);el.append(area,resp); el.append(ta,btn,resp);
} }
async function doAsk(){ async function doAsk(){
const q=$('askInput').value;if(!q)return; const question=$('askInput').value;if(!question)return;
$('askBtn').disabled=true;$('askBtn').textContent='thinking...'; $('askBtn').disabled=true;$('askBtn').textContent='thinking...';
const el=$('askResponse');el.textContent='searching knowledge base...'; const el=$('askResp');el.textContent='searching knowledge...';
try{ try{
const r=await askAI(q); const r=await(await fetch(API+'/vectors/rag',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({index_name:'observer_kb',question,top_k:5})})).json();
el.textContent=''; el.textContent='';
const answer=document.createElement('div');answer.className='ai-answer';answer.textContent=r.answer; const ans=document.createElement('div');ans.className='ai-answer';ans.textContent=r.answer;
const meta=document.createElement('div');meta.className='ai-meta'; const meta=document.createElement('div');meta.className='ai-meta';meta.textContent='Model: '+r.model+(r.tokens_generated?' · '+r.tokens_generated+' tokens':'');
meta.textContent='Model: '+r.model+(r.tokens_generated?' · '+r.tokens_generated+' tokens':''); el.append(ans,meta);
el.append(answer,meta); if(r.sources?.length){
const sh=document.createElement('h2');sh.style.cssText='font-size:11px;color:var(--d);margin-top:14px';sh.textContent='SOURCES';el.appendChild(sh);
if(r.sources&&r.sources.length){
const srcH=document.createElement('h2');srcH.style.cssText='font-size:12px;color:var(--d);margin-top:16px';srcH.textContent='Sources';
el.appendChild(srcH);
r.sources.forEach(s=>{ r.sources.forEach(s=>{
const entry=document.createElement('div');entry.className='entry';entry.style.marginTop='6px'; const e=document.createElement('div');e.className='result';e.style.marginTop='4px';
const score=document.createElement('div');score.style.cssText='font-size:10px;color:var(--a)';score.textContent='['+s.score.toFixed(3)+'] '+s.doc_id; const sc=document.createElement('div');sc.className='result-score';sc.textContent='['+s.score.toFixed(3)+'] '+s.doc_id;
const text=document.createElement('div');text.className='entry-content';text.style.fontSize='11px';text.textContent=s.chunk_text.substring(0,200); const t=document.createElement('div');t.className='result-text';t.style.fontSize='11px';t.textContent=s.chunk_text.substring(0,200);
entry.append(score,text);el.appendChild(entry); e.append(sc,t);el.appendChild(e);
}); });
} }
}catch(e){el.textContent='Error: '+e} }catch(e){el.textContent='Error: '+e}
$('askBtn').disabled=false;$('askBtn').textContent='Ask AI'; $('askBtn').disabled=false;$('askBtn').textContent='Ask AI';
} }
// === UPLOAD TAB === // === UPLOAD ===
function loadUpload(){ function loadUpload(){
const el=$('tab-upload');el.textContent=''; const el=$('tab-upload');el.textContent='';
const zone=document.createElement('div');zone.className='upload-zone'; const zone=document.createElement('div');zone.className='upload-zone';
zone.onclick=()=>{const i=document.createElement('input');i.type='file';i.multiple=true;i.accept='.pdf,.txt,.csv,.json';i.onchange=e=>[...e.target.files].forEach(f=>uploadFile(f));i.click()}; zone.onclick=()=>{const i=document.createElement('input');i.type='file';i.multiple=true;i.accept='.pdf,.txt,.csv,.json';i.onchange=e=>[...e.target.files].forEach(uploadFile);i.click()};
zone.ondragover=e=>{e.preventDefault();zone.style.borderColor='var(--a)'}; zone.ondragover=e=>{e.preventDefault();zone.style.borderColor='var(--a)'};
zone.ondragleave=()=>zone.style.borderColor=''; zone.ondragleave=()=>zone.style.borderColor='';
zone.ondrop=e=>{e.preventDefault();zone.style.borderColor='';[...e.dataTransfer.files].forEach(f=>uploadFile(f))}; zone.ondrop=e=>{e.preventDefault();zone.style.borderColor='';[...e.dataTransfer.files].forEach(uploadFile)};
const h3=document.createElement('h3');h3.textContent='Drop files to ingest'; const h=document.createElement('h3');h.textContent='Drop files to ingest';
const p=document.createElement('p');p.textContent='PDF, CSV, JSON, Text — auto-extracted, categorized, and indexed'; const p=document.createElement('p');p.textContent='PDF, CSV, JSON, Text — auto-extracted and indexed';
zone.append(h3,p); zone.append(h,p);
const list=document.createElement('div');list.id='uploadList'; const list=document.createElement('div');list.id='upList';
const note=document.createElement('div');note.style.cssText='color:var(--d);font-size:11px;margin-top:16px;padding:12px;background:var(--s1);border-radius:8px'; el.append(zone,list);
note.textContent='After upload, run the extraction pipeline to process new files: python3 scripts/extract_knowledge.py';
el.append(zone,list,note);
} }
function uploadFile(file){ function uploadFile(file){
const item=document.createElement('div');item.className='file-item'; const item=document.createElement('div');item.className='file-item';
const name=document.createElement('span');name.textContent=file.name; const name=document.createElement('span');name.textContent=file.name;
const status=document.createElement('span');status.className='uploading';status.textContent='uploading...'; const status=document.createElement('span');status.className='uploading';status.textContent='uploading...';
item.append(name,status);$('uploadList').prepend(item); item.append(name,status);$('upList').prepend(item);
const fd=new FormData();fd.append('file',file); const fd=new FormData();fd.append('file',file);
const xhr=new XMLHttpRequest();xhr.open('POST',API+'/ingest/file'); const xhr=new XMLHttpRequest();xhr.open('POST',API+'/ingest/file');
xhr.upload.onprogress=e=>{if(e.lengthComputable)status.textContent=Math.round(e.loaded/e.total*100)+'%'}; xhr.upload.onprogress=e=>{if(e.lengthComputable)status.textContent=Math.round(e.loaded/e.total*100)+'%'};
@@ -341,7 +280,6 @@ xhr.onerror=()=>{status.className='failed';status.textContent='error'};
xhr.send(fd); xhr.send(fd);
} }
// Init
loadKnowledge(); loadKnowledge();
</script> </script>
</body> </body>