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:
+166
-228
@@ -3,85 +3,65 @@
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Observer KB — Knowledge Base</title>
|
||||
<title>Observer KB</title>
|
||||
<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}
|
||||
.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)}
|
||||
.tabs{display:flex;gap:2px;margin-bottom:20px}
|
||||
.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}
|
||||
.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}.btn-ok{background:var(--ok)}.btn-warn{background:var(--warn);color:#000}
|
||||
.btn:hover{opacity:.9}.btn:disabled{opacity:.5}.btn-sm{padding:5px 10px;font-size:11px}
|
||||
.hidden{display:none}
|
||||
.empty{color:var(--d);text-align:center;padding:40px;font-size:12px}
|
||||
|
||||
/* Stats bar */
|
||||
.stats-bar{display:grid;grid-template-columns:repeat(auto-fill,minmax(140px,1fr));gap:8px;margin-bottom:24px}
|
||||
.stat{background:var(--s1);border:1px solid var(--b);border-radius:8px;padding:14px;text-align:center}
|
||||
.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}
|
||||
|
||||
/* Category cards */
|
||||
.cat-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(280px,1fr));gap:10px;margin-bottom:24px}
|
||||
.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)}
|
||||
.cat-card:hover{border-color:var(--a)}
|
||||
.cat-title{font-weight:600;font-size:14px;margin-bottom:4px}.cat-count{font-size:11px;color:var(--d)}
|
||||
.cat-sources{font-size:10px;color:var(--d);margin-top:6px}
|
||||
|
||||
/* 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 */
|
||||
.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 AI */
|
||||
.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)}
|
||||
.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:8px}
|
||||
|
||||
/* Upload */
|
||||
.upload-zone{border:2px dashed var(--b);border-radius:12px;padding:40px;text-align:center;cursor:pointer;margin-bottom:16px}
|
||||
.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: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}
|
||||
.done{color:var(--ok)}.failed{color:var(--err)}.uploading{color:var(--a)}
|
||||
|
||||
.section{margin-bottom:28px}
|
||||
.section h2{font-size:14px;color:var(--d);text-transform:uppercase;letter-spacing:.08em;margin-bottom:12px}
|
||||
.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)}
|
||||
.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 style="font-size:11px;color:var(--d)" id="hdr-info"></div>
|
||||
</header>
|
||||
|
||||
<div class="tabs">
|
||||
<button class="tab active" onclick="showTab('knowledge')">Knowledge</button>
|
||||
<button class="tab" onclick="showTab('explore')">Explore</button>
|
||||
<button class="tab" onclick="showTab('ask')">Ask</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>
|
||||
@@ -90,249 +70,208 @@ header h1{font-size:20px;color:var(--a)}
|
||||
<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','explore','ask','upload'].forEach(t=>{
|
||||
$(('tab-'+t)).classList.toggle('hidden',t!==name);
|
||||
document.querySelectorAll('.tab')[['knowledge','explore','ask','upload'].indexOf(t)].classList.toggle('active',t===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);
|
||||
});
|
||||
if(name==='knowledge')loadKnowledge();
|
||||
if(name==='explore')loadExplore();
|
||||
if(name==='ask')loadAsk();
|
||||
if(name==='upload')loadUpload();
|
||||
({knowledge:loadKnowledge,facts:loadFacts,explore:loadExplore,ask:loadAsk,upload:loadUpload})[name]();
|
||||
}
|
||||
|
||||
async function query(sql){
|
||||
const r=await fetch(API+'/query/sql',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({sql})});
|
||||
return r.json();
|
||||
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;
|
||||
}
|
||||
|
||||
async function searchKB(q,topk=10){
|
||||
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 ===
|
||||
// === KNOWLEDGE (categories + decisions + insights) ===
|
||||
async function loadKnowledge(){
|
||||
const el=$('tab-knowledge');el.textContent='loading...';
|
||||
|
||||
const el=$('tab-knowledge');el.textContent='';
|
||||
try{
|
||||
const [topics,decisions,insights,questions]=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"),
|
||||
query("SELECT conversation, page, decision FROM knowledge_decisions ORDER BY conversation, page"),
|
||||
query("SELECT conversation, page, insight FROM knowledge_insights ORDER BY conversation, page"),
|
||||
query("SELECT conversation, page, question FROM knowledge_questions ORDER BY conversation, page"),
|
||||
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"),
|
||||
]);
|
||||
|
||||
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
|
||||
const statsBar=document.createElement('div');statsBar.className='stats-bar';
|
||||
[
|
||||
{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 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);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)
|
||||
const catSection=document.createElement('div');catSection.className='section';
|
||||
const catH=document.createElement('h2');catH.textContent='Auto-Generated Categories';
|
||||
catSection.appendChild(catH);
|
||||
const catGrid=document.createElement('div');catGrid.className='cat-grid';
|
||||
// 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 card=document.createElement('div');card.className='cat-card';
|
||||
const title=document.createElement('div');title.className='cat-title';title.textContent=t.topic;
|
||||
const count=document.createElement('div');count.className='cat-count';count.textContent=t.n+' mentions across '+t.convs+' conversations';
|
||||
card.append(title,count);
|
||||
card.onclick=()=>drillTopic(t.topic);
|
||||
catGrid.appendChild(card);
|
||||
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);
|
||||
});
|
||||
catSection.appendChild(catGrid);
|
||||
el.appendChild(catSection);
|
||||
cs.appendChild(cg);el.appendChild(cs);
|
||||
|
||||
// Key Decisions
|
||||
const decSection=document.createElement('div');decSection.className='section';
|
||||
const decH=document.createElement('h2');decH.textContent='Extracted Decisions';
|
||||
decSection.appendChild(decH);
|
||||
decisions.rows.slice(0,15).forEach(d=>{
|
||||
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);
|
||||
// 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);
|
||||
|
||||
// Key Insights
|
||||
const insSection=document.createElement('div');insSection.className='section';
|
||||
const insH=document.createElement('h2');insH.textContent='Key Insights';
|
||||
insSection.appendChild(insH);
|
||||
insights.rows.slice(0,10).forEach(d=>{
|
||||
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';
|
||||
// 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}
|
||||
}
|
||||
|
||||
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){
|
||||
const el=$('tab-knowledge');el.textContent='loading topic...';
|
||||
try{
|
||||
// Find all entries for this topic
|
||||
const related=await searchKB(topic,10);
|
||||
const decisions=await query("SELECT conversation, page, decision FROM knowledge_decisions WHERE decision LIKE '%"+topic.replace(/'/g,"''")+"%' LIMIT 10");
|
||||
|
||||
el.textContent='';
|
||||
const back=document.createElement('button');back.className='btn btn-sm';back.textContent='Back';back.onclick=loadKnowledge;
|
||||
const h2=document.createElement('h2');h2.style.cssText='font-size:18px;margin:12px 0';h2.textContent=topic;
|
||||
el.append(back,h2);
|
||||
|
||||
// Semantic matches
|
||||
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);
|
||||
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(secR);
|
||||
el.appendChild(s);
|
||||
}
|
||||
|
||||
// Related decisions
|
||||
if(decisions.rows.length){
|
||||
const secD=document.createElement('div');secD.className='section';
|
||||
const hD=document.createElement('h2');hD.textContent='Related Decisions';secD.appendChild(hD);
|
||||
decisions.rows.forEach(d=>secD.appendChild(makeEntry('decision',d.decision,d.conversation+' p'+d.page)));
|
||||
el.appendChild(secD);
|
||||
// === 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}
|
||||
}
|
||||
|
||||
// === EXPLORE TAB ===
|
||||
async function loadExplore(){
|
||||
// === SEARCH ===
|
||||
function loadExplore(){
|
||||
const el=$('tab-explore');el.textContent='';
|
||||
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';
|
||||
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);
|
||||
const results=document.createElement('div');results.id='exploreResults';
|
||||
const hint=document.createElement('div');hint.style.cssText='color:var(--d);font-size:11px;margin-bottom:12px';
|
||||
hint.textContent='Semantic search across all extracted knowledge. Try: "data lake architecture", "object storage trade-offs", "CRM options"';
|
||||
el.append(row,hint,results);
|
||||
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 doExplore(){
|
||||
const q=document.getElementById('exploreInput').value;if(!q)return;
|
||||
const el=$('exploreResults');el.textContent='searching...';
|
||||
try{
|
||||
const results=await searchKB(q,15);
|
||||
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 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.replace(/_/g,' ');
|
||||
const text=document.createElement('div');text.className='entry-content';text.textContent=r.chunk_text.substring(0,400);
|
||||
entry.append(score,text);el.appendChild(entry);
|
||||
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);
|
||||
});
|
||||
}catch(e){el.textContent='Error: '+e}
|
||||
}
|
||||
|
||||
// === ASK TAB ===
|
||||
// === ASK AI ===
|
||||
function loadAsk(){
|
||||
const el=$('tab-ask');el.textContent='';
|
||||
const area=document.createElement('div');area.className='ask-area';
|
||||
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 object storage?\n- What approaches were tried and abandoned for data ingestion?\n- Summarize the key differences between Spark and Flink for Iceberg';
|
||||
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='askResponse';
|
||||
area.append(ta,btn);el.append(area,resp);
|
||||
const resp=document.createElement('div');resp.id='askResp';
|
||||
el.append(ta,btn,resp);
|
||||
}
|
||||
|
||||
async function doAsk(){
|
||||
const q=$('askInput').value;if(!q)return;
|
||||
const question=$('askInput').value;if(!question)return;
|
||||
$('askBtn').disabled=true;$('askBtn').textContent='thinking...';
|
||||
const el=$('askResponse');el.textContent='searching knowledge base...';
|
||||
const el=$('askResp');el.textContent='searching knowledge...';
|
||||
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='';
|
||||
const answer=document.createElement('div');answer.className='ai-answer';answer.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(answer,meta);
|
||||
|
||||
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);
|
||||
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 entry=document.createElement('div');entry.className='entry';entry.style.marginTop='6px';
|
||||
const score=document.createElement('div');score.style.cssText='font-size:10px;color:var(--a)';score.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);
|
||||
entry.append(score,text);el.appendChild(entry);
|
||||
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 TAB ===
|
||||
// === 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(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.ondragleave=()=>zone.style.borderColor='';
|
||||
zone.ondrop=e=>{e.preventDefault();zone.style.borderColor='';[...e.dataTransfer.files].forEach(f=>uploadFile(f))};
|
||||
const h3=document.createElement('h3');h3.textContent='Drop files to ingest';
|
||||
const p=document.createElement('p');p.textContent='PDF, CSV, JSON, Text — auto-extracted, categorized, and indexed';
|
||||
zone.append(h3,p);
|
||||
const list=document.createElement('div');list.id='uploadList';
|
||||
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';
|
||||
note.textContent='After upload, run the extraction pipeline to process new files: python3 scripts/extract_knowledge.py';
|
||||
el.append(zone,list,note);
|
||||
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);$('uploadList').prepend(item);
|
||||
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)+'%'};
|
||||
@@ -341,7 +280,6 @@ xhr.onerror=()=>{status.className='failed';status.textContent='error'};
|
||||
xhr.send(fd);
|
||||
}
|
||||
|
||||
// Init
|
||||
loadKnowledge();
|
||||
</script>
|
||||
</body>
|
||||
|
||||
Reference in New Issue
Block a user