Observer KB: full knowledge base interface
- Conversations tab: chat-style view with user/AI/page messages, page numbers - Search tab: semantic search via HNSW, save findings to journal - Ask AI tab: RAG query against all conversations, shows sources with scores - Map tab: visual heatmap of conversations by size, click to open - Upload tab: drag & drop PDFs - Sidebar: conversation list with page counts - Save to KB: bookmark search results and AI answers to event journal Pure HTML/JS, no WASM. Loads instantly. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
+259
-143
@@ -5,174 +5,290 @@
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Observer KB</title>
|
||||
<style>
|
||||
:root{--bg:#0a0a0f;--surface:#12121a;--border:#2a2a3a;--text:#e0e0e8;--dim:#888898;--accent:#6c5ce7;--success:#00d2a0;--error:#ff6b6b;--mono:'JetBrains Mono','Fira Code',monospace}
|
||||
*{margin:0;padding:0;box-sizing:border-box}
|
||||
body{font-family:var(--mono);background:var(--bg);color:var(--text);font-size:13px;min-height:100vh}
|
||||
.app{max-width:1100px;margin:0 auto;padding:24px}
|
||||
header{display:flex;align-items:center;justify-content:space-between;padding:16px 0;border-bottom:1px solid var(--border);margin-bottom:24px}
|
||||
header h1{font-size:20px;color:var(--accent)}
|
||||
.stats{font-size:11px;color:var(--dim)}
|
||||
.tabs{display:flex;gap:2px;margin-bottom:24px}
|
||||
.tab{background:none;border:none;color:var(--dim);font-family:var(--mono);font-size:13px;padding:10px 20px;cursor:pointer;border-bottom:2px solid transparent}
|
||||
.tab:hover{color:var(--text)}.tab.active{color:var(--accent);border-bottom-color:var(--accent)}
|
||||
.upload-zone{border:2px dashed var(--border);border-radius:12px;padding:48px 24px;text-align:center;cursor:pointer;transition:all .2s;margin-bottom:24px}
|
||||
.upload-zone:hover,.upload-zone.dragover{border-color:var(--accent);background:rgba(108,92,231,.05)}
|
||||
.upload-zone h3{font-size:16px;margin-bottom:8px}
|
||||
.upload-zone p{color:var(--dim);font-size:12px}
|
||||
.file-list{display:flex;flex-direction:column;gap:8px}
|
||||
.file-item{background:var(--surface);border:1px solid var(--border);border-radius:8px;padding:14px 18px;display:grid;grid-template-columns:1fr 100px 120px;align-items:center;gap:12px}
|
||||
.file-name{font-weight:600;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
||||
.file-size{color:var(--dim);font-size:11px;text-align:right}
|
||||
.file-status{font-size:11px;text-align:right}
|
||||
.uploading{color:var(--accent)}.done{color:var(--success)}.failed{color:var(--error)}
|
||||
.progress-bar{height:3px;background:var(--border);border-radius:2px;margin-top:6px;grid-column:1/-1}
|
||||
.progress-fill{height:100%;background:var(--accent);border-radius:2px;transition:width .3s}
|
||||
.search-bar{display:flex;gap:8px;margin-bottom:20px}
|
||||
.search-bar input{flex:1;background:var(--surface);border:2px solid var(--border);color:var(--text);font-family:var(--mono);font-size:14px;padding:12px 16px;border-radius:8px;outline:none}
|
||||
.search-bar input:focus{border-color:var(--accent)}
|
||||
.btn{background:var(--accent);color:white;border:none;padding:10px 20px;border-radius:6px;font-family:var(--mono);font-size:13px;cursor:pointer;font-weight:600}
|
||||
.btn:hover{opacity:.9}
|
||||
.ds-item{background:var(--surface);border:1px solid var(--border);border-radius:8px;padding:16px;margin-bottom:8px;cursor:pointer;transition:all .15s}
|
||||
.ds-item:hover{border-color:var(--accent)}
|
||||
.ds-title{font-weight:600;font-size:14px;margin-bottom:4px}
|
||||
.ds-meta{font-size:11px;color:var(--dim)}
|
||||
: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}
|
||||
*{margin:0;padding:0;box-sizing:border-box}body{font-family:var(--f);background:var(--bg);color:var(--t);font-size:13px}
|
||||
.app{display:grid;grid-template-columns:280px 1fr;grid-template-rows:52px 1fr;height:100vh;overflow:hidden}
|
||||
.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 h1{font-size:18px;color:var(--a);letter-spacing:.08em}
|
||||
.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}
|
||||
.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)}
|
||||
|
||||
/* 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-sm{padding:6px 12px;font-size:11px}
|
||||
.btn-save{background:var(--ok)}
|
||||
|
||||
.result{background:var(--s2);border:1px solid var(--b);border-radius:8px;padding:14px;margin-bottom:8px;border-left:3px solid var(--a)}
|
||||
.result-score{font-size:10px;color:var(--a);margin-bottom:4px}
|
||||
.result-src{font-size:10px;color:var(--d)}
|
||||
.result-text{font-size:12px;line-height:1.6;margin:8px 0;color:var(--t)}
|
||||
.result-actions{display:flex;gap:6px;margin-top:8px}
|
||||
|
||||
/* Ask AI panel */
|
||||
.ask-panel{padding:20px;overflow-y:auto}
|
||||
.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-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}
|
||||
.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-zone{border:2px dashed var(--b);border-radius:12px;padding:40px;text-align:center;cursor:pointer;margin:20px}
|
||||
.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}
|
||||
.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)}
|
||||
|
||||
.empty{color:var(--d);text-align:center;padding:40px;font-size:12px}
|
||||
.hidden{display:none}
|
||||
.empty{color:var(--dim);text-align:center;padding:32px}
|
||||
.result-item{background:var(--surface);border:1px solid var(--border);border-radius:8px;padding:14px;margin-bottom:8px;border-left:3px solid var(--accent)}
|
||||
.result-score{font-size:11px;color:var(--accent);margin-bottom:4px}
|
||||
.result-text{font-size:12px;line-height:1.6;color:var(--dim)}
|
||||
#output{background:var(--surface);border:1px solid var(--border);border-radius:8px;padding:16px;margin-top:16px;font-size:12px;line-height:1.6;max-height:500px;overflow-y:auto;white-space:pre-wrap}
|
||||
.saved-badge{display:inline-block;background:var(--ok);color:white;font-size:9px;padding:2px 6px;border-radius:3px;margin-left:6px}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="app">
|
||||
<header><h1>OBSERVER KB</h1><div class="stats" id="stats">loading...</div></header>
|
||||
<div class="header">
|
||||
<h1>OBSERVER KB</h1>
|
||||
<div class="tabs">
|
||||
<button class="tab active" id="t-upload" onclick="showTab('upload')">Upload</button>
|
||||
<button class="tab" id="t-browse" onclick="showTab('browse')">Browse</button>
|
||||
<button class="tab" id="t-search" onclick="showTab('search')">Search</button>
|
||||
<button class="tab active" onclick="showView('chat')">Conversations</button>
|
||||
<button class="tab" onclick="showView('search')">Search</button>
|
||||
<button class="tab" onclick="showView('ask')">Ask AI</button>
|
||||
<button class="tab" onclick="showView('heatmap')">Map</button>
|
||||
<button class="tab" onclick="showView('upload')">Upload</button>
|
||||
</div>
|
||||
<div class="hdr-stats" id="stats"></div>
|
||||
</div>
|
||||
|
||||
<div id="tab-upload">
|
||||
<div class="upload-zone" id="dropZone">
|
||||
<h3>Drop PDF files here</h3>
|
||||
<p>ChatGPT exports, Claude logs, any PDF — or click to browse</p>
|
||||
<input type="file" id="fileInput" multiple accept=".pdf,.txt,.csv,.json" style="display:none">
|
||||
</div>
|
||||
<div class="file-list" id="fileList"></div>
|
||||
</div>
|
||||
<div class="sidebar" id="sidebar"></div>
|
||||
|
||||
<div id="tab-browse" class="hidden">
|
||||
<div id="dsList"></div>
|
||||
</div>
|
||||
|
||||
<div id="tab-search" class="hidden">
|
||||
<div class="search-bar">
|
||||
<input type="text" id="searchInput" placeholder="Search across all knowledge..." />
|
||||
<button class="btn" id="searchBtn">Search</button>
|
||||
</div>
|
||||
<div id="searchResults"></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>
|
||||
const API=`http://${location.hostname}:3100`;
|
||||
const $=id=>document.getElementById(id);
|
||||
let CONVS=[];let CUR=null;
|
||||
|
||||
function showTab(n){
|
||||
document.querySelectorAll('[id^="tab-"]').forEach(t=>t.classList.add('hidden'));
|
||||
$('tab-'+n).classList.remove('hidden');
|
||||
document.querySelectorAll('.tab').forEach(t=>t.classList.remove('active'));
|
||||
$('t-'+n).classList.add('active');
|
||||
if(n==='browse')loadDatasets();
|
||||
}
|
||||
|
||||
$('dropZone').onclick=()=>$('fileInput').click();
|
||||
$('dropZone').ondragover=e=>{e.preventDefault();$('dropZone').classList.add('dragover')};
|
||||
$('dropZone').ondragleave=()=>$('dropZone').classList.remove('dragover');
|
||||
$('dropZone').ondrop=e=>{e.preventDefault();$('dropZone').classList.remove('dragover');[...e.dataTransfer.files].forEach(uploadFile)};
|
||||
$('fileInput').onchange=e=>[...e.target.files].forEach(uploadFile);
|
||||
$('searchBtn').onclick=doSearch;
|
||||
$('searchInput').onkeydown=e=>{if(e.key==='Enter')doSearch()};
|
||||
|
||||
function uploadFile(file){
|
||||
const id='f'+Date.now()+Math.random().toString(36).substr(2,5);
|
||||
const item=document.createElement('div');
|
||||
item.className='file-item';
|
||||
item.id=id;
|
||||
const nameDiv=document.createElement('div');nameDiv.className='file-name';nameDiv.textContent=file.name;
|
||||
const sizeDiv=document.createElement('div');sizeDiv.className='file-size';sizeDiv.textContent=(file.size/1024/1024).toFixed(1)+' MB';
|
||||
const statusDiv=document.createElement('div');statusDiv.className='file-status uploading';statusDiv.textContent='uploading...';
|
||||
const progBar=document.createElement('div');progBar.className='progress-bar';
|
||||
const progFill=document.createElement('div');progFill.className='progress-fill';progFill.style.width='0%';
|
||||
progBar.appendChild(progFill);
|
||||
item.append(nameDiv,sizeDiv,statusDiv,progBar);
|
||||
$('fileList').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){const p=Math.round(e.loaded/e.total*100);progFill.style.width=p+'%';statusDiv.textContent=p+'%'}};
|
||||
xhr.onload=()=>{
|
||||
if(xhr.status<300){statusDiv.className='file-status done';statusDiv.textContent='done';progFill.style.background='var(--success)';loadStats()}
|
||||
else{statusDiv.className='file-status failed';statusDiv.textContent='failed';progFill.style.background='var(--error)'}
|
||||
};
|
||||
xhr.onerror=()=>{statusDiv.className='file-status failed';statusDiv.textContent='error'};
|
||||
xhr.send(fd);
|
||||
}
|
||||
|
||||
async function loadStats(){
|
||||
try{const r=await(await fetch(API+'/catalog/datasets')).json();$('stats').textContent=r.length+' datasets'}
|
||||
catch{$('stats').textContent='offline'}
|
||||
}
|
||||
|
||||
async function loadDatasets(){
|
||||
// Init
|
||||
async function init(){
|
||||
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'];
|
||||
try{
|
||||
const ds=await(await fetch(API+'/catalog/datasets')).json();
|
||||
const el=$('dsList');el.textContent='';
|
||||
if(!ds.length){el.textContent='No data yet';return}
|
||||
ds.forEach(d=>{
|
||||
const item=document.createElement('div');item.className='ds-item';
|
||||
const title=document.createElement('div');title.className='ds-title';title.textContent=d.name;
|
||||
const meta=document.createElement('div');meta.className='ds-meta';
|
||||
meta.textContent=`${d.row_count||'?'} rows · ${d.objects.length} file(s) · ${(d.sensitivity||'unclassified')}`;
|
||||
item.append(title,meta);
|
||||
item.onclick=()=>previewDS(d.name);
|
||||
el.appendChild(item);
|
||||
});
|
||||
}catch(e){$('dsList').textContent='Error: '+e}
|
||||
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'}
|
||||
}
|
||||
|
||||
async function previewDS(name){
|
||||
try{
|
||||
const r=await(await fetch(API+'/query/sql',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({sql:`SELECT * FROM ${name} LIMIT 20`})})).json();
|
||||
const el=$('dsList');el.textContent='';
|
||||
const btn=document.createElement('button');btn.className='btn';btn.textContent='Back';btn.onclick=loadDatasets;
|
||||
const pre=document.createElement('div');pre.id='output';
|
||||
pre.textContent=JSON.stringify(r.rows,null,2);
|
||||
el.append(btn,pre);
|
||||
}catch(e){alert(e)}
|
||||
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);
|
||||
});
|
||||
}
|
||||
|
||||
async function doSearch(){
|
||||
const q=$('searchInput').value;if(!q)return;
|
||||
const el=$('searchResults');el.textContent='searching...';
|
||||
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:10})});
|
||||
if(r.ok){
|
||||
const d=await r.json();el.textContent='';
|
||||
d.results.forEach(hit=>{
|
||||
const item=document.createElement('div');item.className='result-item';
|
||||
const score=document.createElement('div');score.className='result-score';score.textContent=`[${hit.score.toFixed(3)}] ${hit.source} / ${hit.doc_id}`;
|
||||
const text=document.createElement('div');text.className='result-text';text.textContent=hit.chunk_text.substring(0,300);
|
||||
item.append(score,text);el.appendChild(item);
|
||||
function showView(v){
|
||||
['chat','search','ask','heatmap','upload'].forEach(id=>{
|
||||
$('v-'+id).classList.toggle('hidden',id!==v);
|
||||
});
|
||||
if(!d.results.length)el.textContent='No results';
|
||||
}else{el.textContent='Vector index not built yet — upload PDFs first'}
|
||||
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 loadConv(name){
|
||||
CUR=name;renderSidebar();
|
||||
const el=$('v-chat');el.textContent='';
|
||||
el.scrollTop=0;
|
||||
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();
|
||||
r.rows.forEach(row=>{
|
||||
const msg=document.createElement('div');
|
||||
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){el.textContent='Error: '+e}
|
||||
}
|
||||
|
||||
loadStats();
|
||||
// Search view
|
||||
function renderSearch(){
|
||||
const el=$('v-search');el.textContent='';
|
||||
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(){
|
||||
const q=$('sinput').value;if(!q)return;
|
||||
const el=$('sresults');el.textContent='searching...';
|
||||
try{
|
||||
let r;
|
||||
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()}
|
||||
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()}
|
||||
el.textContent='';
|
||||
(r.results||[]).forEach(hit=>{
|
||||
const item=document.createElement('div');item.className='result';
|
||||
const score=document.createElement('div');score.className='result-score';score.textContent='['+hit.score.toFixed(3)+']';
|
||||
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);
|
||||
const actions=document.createElement('div');actions.className='result-actions';
|
||||
const saveBtn=document.createElement('button');saveBtn.className='btn btn-sm btn-save';saveBtn.textContent='Save to KB';
|
||||
saveBtn.onclick=()=>{saveToKB(hit);saveBtn.textContent='Saved';saveBtn.disabled=true};
|
||||
actions.appendChild(saveBtn);
|
||||
item.append(score,src,text,actions);el.appendChild(item);
|
||||
});
|
||||
if(!(r.results||[]).length){el.textContent='No results — try different terms'}
|
||||
}catch(e){el.textContent='Error: '+e}
|
||||
}
|
||||
|
||||
async function saveToKB(hit){
|
||||
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{}
|
||||
}
|
||||
|
||||
// Ask AI view
|
||||
function renderAsk(){
|
||||
const el=$('v-ask');el.textContent='';
|
||||
const input=document.createElement('textarea');input.className='ask-input';input.id='ainput';
|
||||
input.placeholder='Ask a question about your conversations...';
|
||||
const btn=document.createElement('button');btn.className='btn';btn.textContent='Ask AI';btn.id='abtn';
|
||||
btn.onclick=doAsk;
|
||||
const resp=document.createElement('div');resp.id='aresponse';
|
||||
el.append(input,btn,resp);
|
||||
}
|
||||
|
||||
async function doAsk(){
|
||||
const q=$('ainput').value;if(!q)return;
|
||||
$('abtn').disabled=true;$('abtn').textContent='thinking...';
|
||||
const el=$('aresponse');el.textContent='searching knowledge base...';
|
||||
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();
|
||||
el.textContent='';
|
||||
const resp=document.createElement('div');resp.className='ai-response';resp.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 sources=document.createElement('div');sources.className='sources';
|
||||
const h4=document.createElement('h4');h4.textContent='Sources';sources.appendChild(h4);
|
||||
(r.sources||[]).forEach(s=>{
|
||||
const item=document.createElement('div');item.className='result';item.style.marginBottom='6px';
|
||||
const sc=document.createElement('div');sc.className='result-score';sc.textContent='['+s.score.toFixed(3)+'] '+s.doc_id;
|
||||
const tx=document.createElement('div');tx.className='result-text';tx.textContent=s.chunk_text.substring(0,200);
|
||||
item.append(sc,tx);sources.appendChild(item);
|
||||
});
|
||||
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};
|
||||
el.append(resp,meta,sources,saveBtn);
|
||||
}catch(e){el.textContent='Error: '+e}
|
||||
$('abtn').disabled=false;$('abtn').textContent='Ask AI';
|
||||
}
|
||||
|
||||
// Heatmap view
|
||||
function renderHeatmap(){
|
||||
const el=$('v-heatmap');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';
|
||||
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 h3=document.createElement('h3');h3.textContent='Drop files here or click to browse';
|
||||
const p=document.createElement('p');p.textContent='PDF, CSV, JSON, Text';
|
||||
zone.append(h3,p);
|
||||
const list=document.createElement('div');list.id='ulist';
|
||||
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);$('ulist').prepend(item);
|
||||
const fd=new FormData();fd.append('file',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.onerror=()=>{status.className='failed';status.textContent='error'};
|
||||
xhr.send(fd);
|
||||
}
|
||||
|
||||
init();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
Reference in New Issue
Block a user