Observer KB: knowledge-first interface (not a PDF viewer)

Rebuilt from scratch around processed/enriched knowledge:

Knowledge tab (default):
- Stats: topic count, decision count, insight count, question count
- Auto-generated categories with mention counts and cross-conversation coverage
- Click category → drill into semantic matches + related decisions
- Extracted decisions (color-coded badges)
- Key insights
- Open questions (unresolved)

Explore tab:
- Semantic search across all extracted knowledge
- Results show relevance scores and source documents

Ask tab:
- RAG query: question → search → context → LLM answer with cited sources
- Full provenance chain

Ingest tab:
- Drag & drop upload with progress
- Note about running extraction pipeline for new files

This is an editorial workflow, not a file browser.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
root
2026-03-27 23:26:05 -05:00
co-authored by Claude Opus 4.6
parent 1dcbe8e04c
commit 3cf000ccf3
+272 -218
View File
@@ -3,292 +3,346 @@
<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</title> <title>Observer KB — Knowledge Base</title>
<style> <style>
:root{--bg:#0a0a0f;--s1:#12121a;--s2:#1a1a28;--b:#2a2a3a;--t:#e0e0e8;--d:#888898;--a:#6c5ce7;--a2:#4a3db8;--ok:#00d2a0;--err:#ff6b6b;--user:#2d5aa0;--ai:#1a8a5a;--f:'JetBrains Mono','Fira Code',monospace} :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}
*{margin:0;padding:0;box-sizing:border-box}body{font-family:var(--f);background:var(--bg);color:var(--t);font-size:13px} *{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{display:grid;grid-template-columns:280px 1fr;grid-template-rows:52px 1fr;height:100vh;overflow:hidden} .app{max-width:1200px;margin:0 auto;padding:20px}
.header{grid-column:1/-1;display:flex;align-items:center;padding:0 20px;background:var(--s1);border-bottom:1px solid var(--b);gap:20px} header{display:flex;align-items:center;justify-content:space-between;padding:16px 0;border-bottom:1px solid var(--b);margin-bottom:20px}
.header h1{font-size:18px;color:var(--a);letter-spacing:.08em} 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:8px 16px;cursor:pointer;border-bottom:2px solid transparent} .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}
.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}
/* Sidebar */
.sidebar{background:var(--s1);border-right:1px solid var(--b);overflow-y:auto;padding:8px}
.conv-btn{display:block;width:100%;text-align:left;background:none;border:1px solid transparent;color:var(--t);font-family:var(--f);font-size:12px;padding:10px 12px;border-radius:6px;cursor:pointer;margin-bottom:4px}
.conv-btn:hover{border-color:var(--b);background:var(--s2)}
.conv-btn.active{border-color:var(--a);background:rgba(108,92,231,.1)}
.conv-btn .name{font-weight:600;margin-bottom:2px}.conv-btn .meta{font-size:10px;color:var(--d)}
/* Main area */
.main{overflow:hidden;display:flex;flex-direction:column}
/* Chat view */
.chat-area{flex:1;overflow-y:auto;padding:20px;display:flex;flex-direction:column;gap:12px}
.msg{max-width:85%;padding:12px 16px;border-radius:12px;font-size:12px;line-height:1.7;white-space:pre-wrap;word-wrap:break-word}
.msg.user{background:var(--user);align-self:flex-end;border-bottom-right-radius:4px;color:#e8eef8}
.msg.assistant,.msg.ai{background:var(--ai);align-self:flex-start;border-bottom-left-radius:4px;color:#e0f5ec}
.msg.page{background:var(--s2);align-self:flex-start;border-left:3px solid var(--a);color:var(--t)}
.msg-role{font-size:9px;text-transform:uppercase;letter-spacing:.1em;opacity:.7;margin-bottom:4px}
.msg-page{font-size:9px;color:var(--d);margin-top:4px}
/* Search panel */
.search-panel{padding:20px;overflow-y:auto}
.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 16px;border-radius:8px;outline:none}
.search-row input:focus{border-color:var(--a)}
.btn{background:var(--a);color:white;border:none;padding:10px 20px;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:6px 12px;font-size:11px} .btn-sm{padding:5px 10px;font-size:11px}.btn-ok{background:var(--ok)}.btn-warn{background:var(--warn);color:#000}
.btn-save{background:var(--ok)} .hidden{display:none}
.empty{color:var(--d);text-align:center;padding:40px;font-size:12px}
.result{background:var(--s2);border:1px solid var(--b);border-radius:8px;padding:14px;margin-bottom:8px;border-left:3px solid var(--a)} /* Stats bar */
.result-score{font-size:10px;color:var(--a);margin-bottom:4px} .stats-bar{display:grid;grid-template-columns:repeat(auto-fill,minmax(140px,1fr));gap:8px;margin-bottom:24px}
.result-src{font-size:10px;color:var(--d)} .stat{background:var(--s1);border:1px solid var(--b);border-radius:8px;padding:14px;text-align:center}
.result-text{font-size:12px;line-height:1.6;margin:8px 0;color:var(--t)} .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}
.result-actions{display:flex;gap:6px;margin-top:8px}
/* Ask AI panel */ /* Category cards */
.ask-panel{padding:20px;overflow-y:auto} .cat-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(280px,1fr));gap:10px;margin-bottom:24px}
.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} .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)}
.ask-input:focus{border-color:var(--a)} .cat-card:hover{border-color:var(--a)}
.ai-response{background:var(--s2);border:1px solid var(--b);border-radius:8px;padding:16px;margin-top:12px;line-height:1.7;white-space:pre-wrap} .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 */
.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)}
.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:8px}
.sources{margin-top:12px;padding-top:12px;border-top:1px solid var(--b)}
.sources h4{font-size:10px;color:var(--d);text-transform:uppercase;margin-bottom:8px}
/* Heatmap */
.heatmap{padding:20px;overflow-y:auto}
.heatmap h2{font-size:14px;color:var(--d);margin-bottom:12px;text-transform:uppercase;letter-spacing:.08em}
.heat-grid{display:flex;flex-wrap:wrap;gap:4px}
.heat-cell{width:32px;height:32px;border-radius:4px;display:flex;align-items:center;justify-content:center;font-size:9px;cursor:pointer;transition:transform .1s}
.heat-cell:hover{transform:scale(1.3)}
.heat-0{background:var(--s2);color:var(--d)}.heat-1{background:#1a2a3a;color:var(--a)}.heat-2{background:#2a3a5a;color:var(--a)}
.heat-3{background:#3a4a7a;color:white}.heat-4{background:var(--a);color:white}
/* Upload */ /* Upload */
.upload-zone{border:2px dashed var(--b);border-radius:12px;padding:40px;text-align:center;cursor:pointer;margin:20px} .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:6px}.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)}
.empty{color:var(--d);text-align:center;padding:40px;font-size:12px} .section{margin-bottom:28px}
.hidden{display:none} .section h2{font-size:14px;color:var(--d);text-transform:uppercase;letter-spacing:.08em;margin-bottom:12px}
.saved-badge{display:inline-block;background:var(--ok);color:white;font-size:9px;padding:2px 6px;border-radius:3px;margin-left:6px} .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">
<div class="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="showView('chat')">Conversations</button> <button class="tab active" onclick="showTab('knowledge')">Knowledge</button>
<button class="tab" onclick="showView('search')">Search</button> <button class="tab" onclick="showTab('explore')">Explore</button>
<button class="tab" onclick="showView('ask')">Ask AI</button> <button class="tab" onclick="showTab('ask')">Ask</button>
<button class="tab" onclick="showView('heatmap')">Map</button> <button class="tab" onclick="showTab('upload')">Ingest</button>
<button class="tab" onclick="showView('upload')">Upload</button>
</div>
<div class="hdr-stats" id="stats"></div>
</div> </div>
<div class="sidebar" id="sidebar"></div> <div id="tab-knowledge"></div>
<div id="tab-explore" class="hidden"></div>
<div id="tab-ask" class="hidden"></div>
<div id="tab-upload" class="hidden"></div>
</div>
<div class="main">
<div id="v-chat" class="chat-area"><div class="empty">Select a conversation from the sidebar</div></div>
<div id="v-search" class="search-panel hidden"></div>
<div id="v-ask" class="ask-panel hidden"></div>
<div id="v-heatmap" class="heatmap hidden"></div>
<div id="v-upload" class="hidden" style="padding:20px"></div>
</div>
</div>
<script> <script>
const API=`http://${location.hostname}:3100`; const API=`http://${location.hostname}:3100`;
const $=id=>document.getElementById(id); const $=id=>document.getElementById(id);
let CONVS=[];let CUR=null;
// Init function showTab(name){
async function init(){ ['knowledge','explore','ask','upload'].forEach(t=>{
const old=['call_log','placements','clients','threat_intel','timesheets','meta_runs','candidates','job_orders','lab_trials','users','email_log','new_candidates','schema_test_v1','schema_test_v2','stress_test_ingest','stress_ingest','knowledge_topics','knowledge_decisions','knowledge_insights','knowledge_questions']; $(('tab-'+t)).classList.toggle('hidden',t!==name);
try{ document.querySelectorAll('.tab')[['knowledge','explore','ask','upload'].indexOf(t)].classList.toggle('active',t===name);
const ds=await(await fetch(API+'/catalog/datasets')).json();
CONVS=ds.filter(d=>!old.includes(d.name));
$('stats').textContent=CONVS.length+' conversations · '+CONVS.reduce((s,d)=>(d.row_count||0)+s,0)+' pages';
renderSidebar();
}catch(e){$('stats').textContent='offline'}
}
function renderSidebar(){
const sb=$('sidebar');sb.textContent='';
CONVS.forEach(c=>{
const b=document.createElement('button');b.className='conv-btn'+(CUR===c.name?' active':'');
const n=document.createElement('div');n.className='name';n.textContent=c.name.replace(/_/g,' ');
const m=document.createElement('div');m.className='meta';m.textContent=(c.row_count||'?')+' pages';
b.append(n,m);b.onclick=()=>loadConv(c.name);sb.appendChild(b);
}); });
if(name==='knowledge')loadKnowledge();
if(name==='explore')loadExplore();
if(name==='ask')loadAsk();
if(name==='upload')loadUpload();
} }
function showView(v){ async function query(sql){
['chat','search','ask','heatmap','upload'].forEach(id=>{ const r=await fetch(API+'/query/sql',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({sql})});
$('v-'+id).classList.toggle('hidden',id!==v); return r.json();
});
document.querySelectorAll('.tab').forEach((t,i)=>t.classList.toggle('active',['chat','search','ask','heatmap','upload'][i]===v));
if(v==='search')renderSearch();
if(v==='ask')renderAsk();
if(v==='heatmap')renderHeatmap();
if(v==='upload')renderUpload();
} }
// Chat view async function searchKB(q,topk=10){
async function loadConv(name){
CUR=name;renderSidebar();
const el=$('v-chat');el.textContent='';
el.scrollTop=0;
try{ try{
const r=await(await fetch(API+'/query/sql',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({sql:`SELECT turn_number, role, content, page_number FROM ${name} ORDER BY turn_number`})})).json(); 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})});
r.rows.forEach(row=>{ return(await r.json()).results||[];
const msg=document.createElement('div'); }catch{return[]}
msg.className='msg '+(row.role||'page');
const role=document.createElement('div');role.className='msg-role';
role.textContent=(row.role==='user'?'You':(row.role==='assistant'?'ChatGPT':'Page '+row.turn_number));
const content=document.createElement('div');
content.textContent=(row.content||'').substring(0,2000);
const pg=document.createElement('div');pg.className='msg-page';
pg.textContent='page '+row.page_number;
msg.append(role,content,pg);el.appendChild(msg);
});
if(!r.rows.length)el.textContent='No content';
}catch(e){const err=document.createElement("div");err.className="empty";err.textContent="Error loading: "+e;el.appendChild(err)}
} }
// Search view async function askAI(question){
function renderSearch(){ 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})});
const el=$('v-search');el.textContent=''; return r.json();
const row=document.createElement('div');row.className='search-row';
const input=document.createElement('input');input.placeholder='Search across all conversations...';input.id='sinput';
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 results=document.createElement('div');results.id='sresults';
el.append(row,results);
} }
async function doSearch(){ // === KNOWLEDGE TAB ===
const q=$('sinput').value;if(!q)return; async function loadKnowledge(){
const el=$('sresults');el.textContent='searching...'; const el=$('tab-knowledge');el.textContent='loading...';
try{ try{
let r; const [topics,decisions,insights,questions]=await Promise.all([
try{r=await(await fetch(API+'/vectors/hnsw/search',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({index_name:'observer_kb',query:q,top_k:10})})).json()} query("SELECT topic, COUNT(*) as n, COUNT(DISTINCT conversation) as convs FROM knowledge_topics GROUP BY topic ORDER BY n DESC LIMIT 20"),
catch{r=await(await fetch(API+'/vectors/search',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({index_name:'observer_kb',query:q,top_k:10})})).json()} 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"),
]);
el.textContent=''; el.textContent='';
(r.results||[]).forEach(hit=>{
const item=document.createElement('div');item.className='result'; // Stats
const score=document.createElement('div');score.className='result-score';score.textContent='['+hit.score.toFixed(3)+']'; const statsBar=document.createElement('div');statsBar.className='stats-bar';
const src=document.createElement('div');src.className='result-src';src.textContent=hit.doc_id; [
const text=document.createElement('div');text.className='result-text';text.textContent=hit.chunk_text.substring(0,400); {v:topics.row_count,l:'Topics'},
const actions=document.createElement('div');actions.className='result-actions'; {v:decisions.row_count,l:'Decisions'},
const saveBtn=document.createElement('button');saveBtn.className='btn btn-sm btn-save';saveBtn.textContent='Save to KB'; {v:insights.row_count,l:'Insights'},
saveBtn.onclick=()=>{saveToKB(hit);saveBtn.textContent='Saved';saveBtn.disabled=true}; {v:questions.row_count,l:'Open Questions'},
actions.appendChild(saveBtn); ].forEach(s=>{
item.append(score,src,text,actions);el.appendChild(item); 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);
}); });
if(!(r.results||[]).length){el.textContent='No results — try different terms'} el.appendChild(statsBar);
}catch(e){const err=document.createElement("div");err.className="empty";err.textContent="Error loading: "+e;el.appendChild(err)}
// 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';
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);
});
catSection.appendChild(catGrid);
el.appendChild(catSection);
// 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);
// 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';
}catch(e){el.textContent='Error: '+e}
} }
async function saveToKB(hit){ function makeEntry(type,content,source){
try{await fetch(API+'/journal/event',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({entity_type:'saved_finding',entity_id:hit.doc_id,action:'save',field:'content',new_value:hit.chunk_text.substring(0,500),actor:'user',source:'observer_search'})})}catch{} 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;
} }
// Ask AI view async function drillTopic(topic){
function renderAsk(){ const el=$('tab-knowledge');el.textContent='loading topic...';
const el=$('v-ask');el.textContent=''; try{
const input=document.createElement('textarea');input.className='ask-input';input.id='ainput'; // Find all entries for this topic
input.placeholder='Ask a question about your conversations...'; const related=await searchKB(topic,10);
const btn=document.createElement('button');btn.className='btn';btn.textContent='Ask AI';btn.id='abtn'; 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);
});
el.appendChild(secR);
// 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);
}
}catch(e){el.textContent='Error: '+e}
}
// === EXPLORE TAB ===
async 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 btn=document.createElement('button');btn.className='btn';btn.textContent='Search';
btn.onclick=doExplore;input.onkeydown=e=>{if(e.key==='Enter')doExplore()};
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);
}
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);
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);
});
}catch(e){el.textContent='Error: '+e}
}
// === ASK TAB ===
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 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='aresponse'; const resp=document.createElement('div');resp.id='askResponse';
el.append(input,btn,resp); area.append(ta,btn);el.append(area,resp);
} }
async function doAsk(){ async function doAsk(){
const q=$('ainput').value;if(!q)return; const q=$('askInput').value;if(!q)return;
$('abtn').disabled=true;$('abtn').textContent='thinking...'; $('askBtn').disabled=true;$('askBtn').textContent='thinking...';
const el=$('aresponse');el.textContent='searching knowledge base...'; const el=$('askResponse');el.textContent='searching knowledge base...';
try{ try{
const r=await(await fetch(API+'/vectors/rag',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({index_name:'observer_kb',question:q,top_k:5})})).json(); const r=await askAI(q);
el.textContent=''; el.textContent='';
const resp=document.createElement('div');resp.className='ai-response';resp.textContent=r.answer; 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':''); const meta=document.createElement('div');meta.className='ai-meta';
const sources=document.createElement('div');sources.className='sources'; meta.textContent='Model: '+r.model+(r.tokens_generated?' · '+r.tokens_generated+' tokens':'');
const h4=document.createElement('h4');h4.textContent='Sources';sources.appendChild(h4); el.append(answer,meta);
(r.sources||[]).forEach(s=>{
const item=document.createElement('div');item.className='result';item.style.marginBottom='6px'; if(r.sources&&r.sources.length){
const sc=document.createElement('div');sc.className='result-score';sc.textContent='['+s.score.toFixed(3)+'] '+s.doc_id; const srcH=document.createElement('h2');srcH.style.cssText='font-size:12px;color:var(--d);margin-top:16px';srcH.textContent='Sources';
const tx=document.createElement('div');tx.className='result-text';tx.textContent=s.chunk_text.substring(0,200); el.appendChild(srcH);
item.append(sc,tx);sources.appendChild(item); 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 saveBtn=document.createElement('button');saveBtn.className='btn btn-sm btn-save';saveBtn.textContent='Save Answer';saveBtn.style.marginTop='12px'; }
saveBtn.onclick=()=>{saveToKB({doc_id:'ai_answer',chunk_text:q+'\n\n'+r.answer});saveBtn.textContent='Saved';saveBtn.disabled=true}; }catch(e){el.textContent='Error: '+e}
el.append(resp,meta,sources,saveBtn); $('askBtn').disabled=false;$('askBtn').textContent='Ask AI';
}catch(e){const err=document.createElement("div");err.className="empty";err.textContent="Error loading: "+e;el.appendChild(err)}
$('abtn').disabled=false;$('abtn').textContent='Ask AI';
} }
// Heatmap view // === UPLOAD TAB ===
function renderHeatmap(){ function loadUpload(){
const el=$('v-heatmap');el.textContent=''; const el=$('tab-upload');el.textContent='';
const h2=document.createElement('h2');h2.textContent='Knowledge Map';
const desc=document.createElement('p');desc.style.cssText='color:var(--d);font-size:11px;margin-bottom:16px';
desc.textContent='Size = pages extracted. Click to open conversation.';
el.append(h2,desc);
const grid=document.createElement('div');grid.style.cssText='display:grid;grid-template-columns:repeat(auto-fill,minmax(200px,1fr));gap:10px';
CONVS.forEach(c=>{
const card=document.createElement('div');
const pages=c.row_count||0;
const level=pages>100?4:pages>50?3:pages>20?2:pages>5?1:0;
card.className='heat-cell';
card.style.cssText=`width:auto;height:auto;padding:16px;border-radius:8px;cursor:pointer;background:var(--s2);border:2px solid ${level>=3?'var(--a)':level>=2?'var(--b)':'transparent'}`;
const name=document.createElement('div');name.style.cssText='font-weight:600;font-size:13px;margin-bottom:4px';name.textContent=c.name.replace(/_/g,' ');
const meta=document.createElement('div');meta.style.cssText='font-size:11px;color:var(--d)';meta.textContent=pages+' pages';
const bar=document.createElement('div');bar.style.cssText=`height:4px;background:var(--a);border-radius:2px;margin-top:8px;width:${Math.min(100,pages)}%`;
card.append(name,meta,bar);
card.onclick=()=>{showView('chat');loadConv(c.name)};
grid.appendChild(card);
});
el.appendChild(grid);
}
// Upload view
function renderUpload(){
const el=$('v-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(uploadFile);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(f=>uploadFile(f));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(uploadFile)}; zone.ondrop=e=>{e.preventDefault();zone.style.borderColor='';[...e.dataTransfer.files].forEach(f=>uploadFile(f))};
const h3=document.createElement('h3');h3.textContent='Drop files here or click to browse'; const h3=document.createElement('h3');h3.textContent='Drop files to ingest';
const p=document.createElement('p');p.textContent='PDF, CSV, JSON, Text'; const p=document.createElement('p');p.textContent='PDF, CSV, JSON, Text — auto-extracted, categorized, and indexed';
zone.append(h3,p); zone.append(h3,p);
const list=document.createElement('div');list.id='ulist'; const list=document.createElement('div');list.id='uploadList';
el.append(zone,list); 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);
} }
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);$('ulist').prepend(item); item.append(name,status);$('uploadList').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.onload=()=>{status.className=xhr.status<300?'done':'failed';status.textContent=xhr.status<300?'done':'failed';init()}; 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.onerror=()=>{status.className='failed';status.textContent='error'};
xhr.send(fd); xhr.send(fd);
} }
init(); // Init
loadKnowledge();
</script> </script>
</body> </body>
</html> </html>