- 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>
287 lines
16 KiB
HTML
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 & 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>
|