Files
lakehouse/apps/observer-kb/index.html
T
rootandClaude Opus 4.6 4d64e4a618 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>
2026-03-28 00:25:39 -05:00

287 lines
16 KiB
HTML

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Observer KB</title>
<style>
: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}
.app{max-width:1200px;margin:0 auto;padding: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)}
.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: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:hover{opacity:.9}.btn:disabled{opacity:.5}.btn-sm{padding:5px 10px;font-size:11px}
.hidden{display:none}
.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}
.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}
.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}
.entry{background:var(--s1);border:1px solid var(--b);border-radius:8px;padding:14px;margin-bottom:8px}
.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}
.badge-decision{background:#e67e22;color:white}.badge-insight{background:#3498db;color:white}
.badge-question{background:var(--warn);color:#000}.badge-verified{background:var(--ok);color:white}
.entry-text{font-size:12px;line-height:1.6}.entry-src{font-size:10px;color:var(--d);margin-top:4px}
.cat-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(250px,1fr));gap:8px;margin-bottom:16px}
.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-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}
.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: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-input: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-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-zone:hover{border-color:var(--a);background:rgba(108,92,231,.03)}
.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}
.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)}
.result-score{font-size:10px;color:var(--a);margin-bottom:2px}.result-text{font-size:12px;line-height:1.5}
.empty{color:var(--d);text-align:center;padding:32px}
</style>
</head>
<body>
<div class="app">
<header>
<h1>OBSERVER KB</h1>
<div class="tabs">
<button class="tab active" onclick="showTab('knowledge')">Knowledge</button>
<button class="tab" onclick="showTab('facts')">Facts &amp; Entities</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>
</div>
<div class="hdr-stats" id="hdr-stats"></div>
</header>
<div id="tab-knowledge"></div>
<div id="tab-facts" class="hidden"></div>
<div id="tab-explore" class="hidden"></div>
<div id="tab-ask" class="hidden"></div>
<div id="tab-upload" class="hidden"></div>
</div>
<script>
const API=`http://${location.hostname}:3100`;
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){
['knowledge','facts','explore','ask','upload'].forEach((t,i)=>{
$('tab-'+t).classList.toggle('hidden',t!==name);
document.querySelectorAll('.tab')[i].classList.toggle('active',t===name);
});
({knowledge:loadKnowledge,facts:loadFacts,explore:loadExplore,ask:loadAsk,upload:loadUpload})[name]();
}
function makeEntry(type,text,source){
const e=document.createElement('div');e.className='entry';
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;
}
// === KNOWLEDGE (categories + decisions + insights) ===
async function loadKnowledge(){
const el=$('tab-knowledge');el.textContent='';
try{
const[topics,decisions,insights,questions,extractions]=await Promise.all([
q("SELECT topic,COUNT(*) as n,COUNT(DISTINCT conversation) as c FROM knowledge_topics GROUP BY topic ORDER BY n DESC LIMIT 15"),
q("SELECT conversation,page,decision FROM knowledge_decisions LIMIT 12"),
q("SELECT conversation,page,insight FROM knowledge_insights LIMIT 8"),
q("SELECT COUNT(*) as n FROM knowledge_questions"),
q("SELECT type,COUNT(*) as n FROM processed_extractions GROUP BY type"),
]);
const factCount=extractions.rows.find(r=>r.type==='fact')?.n||0;
const entityCount=extractions.rows.find(r=>r.type==='entity')?.n||0;
// Stats
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=>{
const d=document.createElement('div');d.className='stat';
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;
d.append(v,l);sb.appendChild(d);
});
el.appendChild(sb);
$('hdr-stats').textContent=factCount+' facts · '+entityCount+' entities · '+decisions.row_count+' decisions';
// Categories
const cs=document.createElement('div');cs.className='section';
cs.insertAdjacentHTML('beforeend','<h2>Auto-Generated Categories</h2>');
const cg=document.createElement('div');cg.className='cat-grid';
topics.rows.forEach(t=>{
const c=document.createElement('div');c.className='cat-card';
const n=document.createElement('div');n.className='cat-title';n.textContent=t.topic;
const m=document.createElement('div');m.className='cat-count';m.textContent=t.n+' mentions · '+t.c+' conversations';
c.append(n,m);c.onclick=()=>drillTopic(t.topic);cg.appendChild(c);
});
cs.appendChild(cg);el.appendChild(cs);
// Decisions
const ds=document.createElement('div');ds.className='section';
ds.insertAdjacentHTML('beforeend','<h2>Key Decisions</h2>');
decisions.rows.forEach(d=>ds.appendChild(makeEntry('decision',d.decision,d.conversation+' p'+d.page)));
el.appendChild(ds);
// Insights
const is=document.createElement('div');is.className='section';
is.insertAdjacentHTML('beforeend','<h2>Key Insights</h2>');
insights.rows.forEach(d=>is.appendChild(makeEntry('insight',d.insight,d.conversation+' p'+d.page)));
el.appendChild(is);
}catch(e){el.textContent='Error: '+e}
}
async function drillTopic(topic){
const el=$('tab-knowledge');el.textContent='';
const back=document.createElement('button');back.className='btn btn-sm';back.textContent='← Back';back.onclick=loadKnowledge;
const h=document.createElement('h2');h.style.cssText='font-size:18px;margin:12px 0';h.textContent=topic;
el.append(back,h);
const results=await searchHNSW(topic,10);
const s=document.createElement('div');s.className='section';
s.insertAdjacentHTML('beforeend','<h2>Related Content</h2>');
results.forEach(r=>{
const e=document.createElement('div');e.className='result';
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);
e.append(sc,t);s.appendChild(e);
});
el.appendChild(s);
}
// === FACTS & ENTITIES (from LLM Team extract mode) ===
async function loadFacts(){
const el=$('tab-facts');el.textContent='';
try{
const[facts,entities,verified]=await Promise.all([
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}
}
// === SEARCH ===
function loadExplore(){
const el=$('tab-explore');el.textContent='';
const row=document.createElement('div');row.className='search-row';
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';
btn.onclick=doSearch;input.onkeydown=e=>{if(e.key==='Enter')doSearch()};
row.append(input,btn);
const res=document.createElement('div');res.id='searchResults';
const hint=document.createElement('div');hint.className='empty';hint.textContent='Try: "object storage architecture", "data ingestion trade-offs", "CRM options"';
el.append(row,res,hint);
}
async function doSearch(){
const query=document.getElementById('searchInput').value;if(!query)return;
const el=$('searchResults');el.textContent='searching...';
const results=await searchHNSW(query,15);
el.textContent='';
if(!results.length){el.textContent='No results';return}
results.forEach(r=>{
const e=document.createElement('div');e.className='result';
const s=document.createElement('div');s.className='result-score';s.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);
e.append(s,t);el.appendChild(e);
});
}
// === ASK AI ===
function loadAsk(){
const el=$('tab-ask');el.textContent='';
const ta=document.createElement('textarea');ta.className='ask-input';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?';
const btn=document.createElement('button');btn.className='btn';btn.id='askBtn';btn.textContent='Ask AI';
btn.onclick=doAsk;
const resp=document.createElement('div');resp.id='askResp';
el.append(ta,btn,resp);
}
async function doAsk(){
const question=$('askInput').value;if(!question)return;
$('askBtn').disabled=true;$('askBtn').textContent='thinking...';
const el=$('askResp');el.textContent='searching knowledge...';
try{
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='';
const ans=document.createElement('div');ans.className='ai-answer';ans.textContent=r.answer;
const meta=document.createElement('div');meta.className='ai-meta';meta.textContent='Model: '+r.model+(r.tokens_generated?' · '+r.tokens_generated+' tokens':'');
el.append(ans,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);
r.sources.forEach(s=>{
const e=document.createElement('div');e.className='result';e.style.marginTop='4px';
const sc=document.createElement('div');sc.className='result-score';sc.textContent='['+s.score.toFixed(3)+'] '+s.doc_id;
const t=document.createElement('div');t.className='result-text';t.style.fontSize='11px';t.textContent=s.chunk_text.substring(0,200);
e.append(sc,t);el.appendChild(e);
});
}
}catch(e){el.textContent='Error: '+e}
$('askBtn').disabled=false;$('askBtn').textContent='Ask AI';
}
// === UPLOAD ===
function loadUpload(){
const el=$('tab-upload');el.textContent='';
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(uploadFile);i.click()};
zone.ondragover=e=>{e.preventDefault();zone.style.borderColor='var(--a)'};
zone.ondragleave=()=>zone.style.borderColor='';
zone.ondrop=e=>{e.preventDefault();zone.style.borderColor='';[...e.dataTransfer.files].forEach(uploadFile)};
const h=document.createElement('h3');h.textContent='Drop files to ingest';
const p=document.createElement('p');p.textContent='PDF, CSV, JSON, Text — auto-extracted and indexed';
zone.append(h,p);
const list=document.createElement('div');list.id='upList';
el.append(zone,list);
}
function uploadFile(file){
const item=document.createElement('div');item.className='file-item';
const name=document.createElement('span');name.textContent=file.name;
const status=document.createElement('span');status.className='uploading';status.textContent='uploading...';
item.append(name,status);$('upList').prepend(item);
const fd=new FormData();fd.append('file',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.onload=()=>{status.className=xhr.status<300?'done':'failed';status.textContent=xhr.status<300?'ingested':'failed'};
xhr.onerror=()=>{status.className='failed';status.textContent='error'};
xhr.send(fd);
}
loadKnowledge();
</script>
</body>
</html>