<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Brain Memory Explorer</title>
<style>
*{box-sizing:border-box;margin:0;padding:0}
body{font-family:system-ui,sans-serif;background:#0f172a;color:#e2e8f0;min-height:100vh}
header{background:#1e293b;border-bottom:1px solid #334155;padding:1rem 2rem;display:flex;align-items:center;gap:1rem}
header h1{font-size:1.25rem;font-weight:700;color:#38bdf8}
header span{color:#94a3b8;font-size:.875rem}
main{max-width:1200px;margin:0 auto;padding:2rem}
.search-bar{display:flex;gap:.5rem;margin-bottom:2rem}
.search-bar input{flex:1;background:#1e293b;border:1px solid #334155;border-radius:.5rem;padding:.75rem 1rem;color:#e2e8f0;font-size:1rem;outline:none}
.search-bar input:focus{border-color:#38bdf8}
.search-bar button{background:#0ea5e9;border:none;border-radius:.5rem;padding:.75rem 1.5rem;color:#fff;font-size:1rem;cursor:pointer}
.search-bar button:hover{background:#0284c7}
.tabs{display:flex;gap:.5rem;margin-bottom:1.5rem}
.tab{background:#1e293b;border:1px solid #334155;border-radius:.5rem;padding:.5rem 1rem;cursor:pointer;font-size:.875rem;color:#94a3b8}
.tab.active{background:#0ea5e9;border-color:#0ea5e9;color:#fff}
#status{color:#94a3b8;font-size:.875rem;margin-bottom:1rem;min-height:1.25rem}
#status.error{color:#f87171}
.grid{display:grid;gap:1rem}
.card{background:#1e293b;border:1px solid #334155;border-radius:.75rem;padding:1.25rem}
.card .meta{font-size:.75rem;color:#64748b;margin-bottom:.5rem}
.card .subject{font-weight:600;color:#38bdf8;margin-bottom:.25rem}
.card .predicate{color:#94a3b8;font-size:.875rem;margin-bottom:.25rem}
.card .object{color:#e2e8f0}
.card .badge{display:inline-block;background:#0f172a;border:1px solid #334155;border-radius:.25rem;padding:.15rem .4rem;font-size:.7rem;color:#64748b;margin-right:.25rem}
.card .conf{color:#4ade80;font-size:.75rem}
.empty{color:#475569;text-align:center;padding:3rem;font-size:.9rem}
.key-bar{display:flex;align-items:center;gap:.5rem;margin-left:auto}
.key-bar label{font-size:.75rem;color:#64748b}
.key-bar input{background:#0f172a;border:1px solid #334155;border-radius:.25rem;padding:.35rem .5rem;color:#94a3b8;font-size:.75rem;width:10rem;outline:none}
.key-bar input:focus{border-color:#38bdf8}
.event-card{background:#1e293b;border:1px solid #334155;border-left:3px solid #0ea5e9;border-radius:.5rem;padding:.75rem 1rem;margin-bottom:.5rem}
.event-card.kind-error{border-left-color:#f87171}
.event-card.kind-audit_appended{border-left-color:#4ade80}
.event-card.kind-signal_received{border-left-color:#38bdf8}
.event-card.kind-tool_call_started,.event-card.kind-tool_call_finished{border-left-color:#fbbf24}
.event-card.kind-confirmation_requested{border-left-color:#a78bfa}
.event-row{display:flex;align-items:center;gap:.5rem;font-size:.8rem}
.event-kind{font-weight:600;color:#94a3b8;text-transform:uppercase;letter-spacing:.04em}
.event-id{color:#64748b;font-family:ui-monospace,monospace;font-size:.7rem}
.event-ts{margin-left:auto;color:#64748b;font-size:.7rem}
.event-card details{margin-top:.4rem}
.event-card details summary{cursor:pointer;color:#64748b;font-size:.7rem;list-style:none}
.event-card details summary::-webkit-details-marker{display:none}
.event-card details summary::before{content:'▸ ';color:#475569}
.event-card details[open] summary::before{content:'▾ '}
.event-card pre{background:#0f172a;border-radius:.25rem;padding:.5rem;font-size:.7rem;color:#cbd5e1;overflow-x:auto;margin-top:.4rem;font-family:ui-monospace,monospace}
.cancel-btn{background:#7f1d1d;border:none;border-radius:.25rem;padding:.2rem .55rem;color:#fecaca;font-size:.7rem;cursor:pointer;margin-left:.25rem}
.cancel-btn:hover{background:#b91c1c;color:#fff}
.live-bar{display:flex;align-items:center;gap:.5rem;margin-bottom:.5rem;font-size:.75rem;color:#64748b}
.live-bar input{background:#0f172a;border:1px solid #334155;border-radius:.25rem;padding:.25rem .5rem;color:#94a3b8;font-size:.75rem;width:9rem;outline:none}
.live-bar input:focus{border-color:#38bdf8}
.live-bar .dot{width:.5rem;height:.5rem;border-radius:50%;background:#475569}
.live-bar .dot.on{background:#4ade80}
</style>
</head>
<body>
<header>
<h1>🧠 Brain</h1>
<span>Memory Explorer</span>
<div class="key-bar">
<label for="api-key">API Key</label>
<input id="api-key" type="password" placeholder="Enter API key">
<span id="api-status" style="font-size:.75rem"></span>
</div>
</header>
<main>
<div class="search-bar">
<input id="q" type="search" placeholder="Search memory… (e.g. Rust, project goals)" autofocus>
<button onclick="search()">Search</button>
</div>
<div class="tabs">
<div class="tab active" id="tab-search" onclick="switchTab('search')">Search Results</div>
<div class="tab" id="tab-facts" onclick="switchTab('facts')">All Facts</div>
<div class="tab" id="tab-live" onclick="switchTab('live')">Live</div>
<div class="tab" id="tab-audit" onclick="switchTab('audit')">Audit</div>
</div>
<div id="status"></div>
<div id="results" class="grid"></div>
<div id="live-pane" style="display:none">
<div class="live-bar">
<span class="dot" id="live-dot"></span>
<span id="live-state">disconnected</span>
<label for="live-kind">kind</label>
<input id="live-kind" placeholder="signal_received…">
<label for="live-tool">tool_id</label>
<input id="live-tool" placeholder="mcp:fs:read">
<button onclick="restartLive()">Apply</button>
<button onclick="clearEvents()">Clear</button>
</div>
<div id="events"></div>
</div>
<div id="audit-pane" style="display:none">
<div class="live-bar">
<label for="audit-since">since</label>
<input id="audit-since" placeholder="today, 1h, RFC3339…">
<button onclick="loadAudit()">Load</button>
</div>
<pre id="audit-output" style="background:#0f172a;border:1px solid #334155;border-radius:.5rem;padding:1rem;color:#cbd5e1;font-size:.8rem;white-space:pre-wrap;font-family:ui-monospace,monospace"></pre>
</div>
</main>
<script>
const API = '';
let apiKey = localStorage.getItem('brain_api_key') || '';
const keyInput = document.getElementById('api-key');
keyInput.value = apiKey;
function applyKey(){
apiKey = keyInput.value.trim();
localStorage.setItem('brain_api_key', apiKey);
keyInput.style.borderColor='';
loadFacts();
}
keyInput.addEventListener('change', applyKey);
keyInput.addEventListener('keydown', function(e){ if(e.key==='Enter') applyKey(); });
function hdr(){ return {'Authorization':'Bearer '+apiKey,'Content-Type':'application/json'} }
function flagKeyInput(msg){
keyInput.style.borderColor='#f87171';
keyInput.focus();
setStatus(msg || 'Invalid API key — enter your key above',true);
}
async function checkHealth(){
try{
const r=await fetch(API+'/health');
const d=await r.json();
document.getElementById('api-status').textContent='● '+d.status+' v'+d.version;
document.getElementById('api-status').style.color='#4ade80';
}catch(e){
document.getElementById('api-status').textContent='● unreachable';
document.getElementById('api-status').style.color='#f87171';
}
}
function setStatus(msg,err){
const el=document.getElementById('status');
el.textContent=msg;
el.className=err?'error':'';
}
function renderFact(f){
const dist=f.distance!=null?' · dist '+f.distance.toFixed(3):'';
return `<div class="card">
<div class="meta"><span class="badge">${f.namespace}</span><span class="badge">${f.category}</span><span class="conf">conf ${f.confidence.toFixed(2)}</span>${dist}</div>
<div class="subject">${esc(f.subject)}</div>
<div class="predicate">${esc(f.predicate)}</div>
<div class="object">${esc(f.object)}</div>
</div>`;
}
function esc(s){ const d=document.createElement('div');d.textContent=s;return d.innerHTML; }
async function search(){
const q=document.getElementById('q').value.trim();
if(!q){await loadFacts();return;}
if(!apiKey){flagKeyInput('Enter your API key above to search');return;}
setStatus('Searching…');
try{
const r=await fetch(API+'/v1/memory/search',{method:'POST',headers:hdr(),body:JSON.stringify({query:q,top_k:20})});
if(r.status===401){flagKeyInput();return;}
if(!r.ok){setStatus('Search failed: '+r.status,true);return;}
const facts=await r.json();
setStatus(facts.length+' result'+(facts.length!==1?'s':''));
document.getElementById('results').innerHTML=facts.length?facts.map(renderFact).join(''):'<p class="empty">No matching facts found.</p>';
}catch(e){setStatus('Error: '+e.message,true);}
}
async function loadFacts(){
if(!apiKey){flagKeyInput('Enter your API key above to browse memories');return;}
setStatus('Loading…');
try{
const r=await fetch(API+'/v1/memory/facts',{headers:hdr()});
if(r.status===401){flagKeyInput();return;}
if(!r.ok){setStatus('Failed to load facts: '+r.status,true);return;}
const facts=await r.json();
keyInput.style.borderColor='#4ade80';
setStatus(facts.length+' stored fact'+(facts.length!==1?'s':''));
document.getElementById('results').innerHTML=facts.length?facts.map(renderFact).join(''):'<p class="empty">No facts stored yet. Send a "Remember…" signal to add some.</p>';
}catch(e){setStatus('Error: '+e.message,true);}
}
function switchTab(t){
document.querySelectorAll('.tab').forEach(el=>el.classList.remove('active'));
document.getElementById('tab-'+t).classList.add('active');
document.getElementById('results').style.display='none';
document.getElementById('live-pane').style.display='none';
document.getElementById('audit-pane').style.display='none';
stopLive();
if(t==='facts'){document.getElementById('results').style.display='grid';loadFacts();}
else if(t==='live'){document.getElementById('live-pane').style.display='block';startLive();}
else if(t==='audit'){document.getElementById('audit-pane').style.display='block';loadAudit();}
else{document.getElementById('results').style.display='grid';document.getElementById('results').innerHTML='';setStatus('');}
}
let liveCtrl=null;
function setLiveState(s,on){
document.getElementById('live-state').textContent=s;
document.getElementById('live-dot').classList.toggle('on',!!on);
}
function clearEvents(){document.getElementById('events').innerHTML='';}
function restartLive(){stopLive();startLive();}
function stopLive(){
if(liveCtrl){liveCtrl.abort();liveCtrl=null;}
setLiveState('disconnected',false);
}
function renderEvent(ev){
const card=document.createElement('div');
card.className='event-card kind-'+(ev.kind||'unknown');
const id=String(ev.id||'').slice(0,8);
const cancel=ev.kind==='signal_received'
? `<button class="cancel-btn" onclick="cancelEvent('${esc(ev.id)}')">cancel</button>` : '';
card.innerHTML=`<div class="event-row">
<span class="event-kind">${esc(ev.kind||'?')}</span>
<span class="event-id">${esc(id)}</span>
${cancel}
<span class="event-ts">${esc(ev.ts||'')}</span>
</div>
<details><summary>payload</summary><pre>${esc(JSON.stringify(ev,null,2))}</pre></details>`;
const list=document.getElementById('events');
list.insertBefore(card,list.firstChild);
while(list.children.length>200)list.removeChild(list.lastChild);
}
async function cancelEvent(signalId){
if(!apiKey){flagKeyInput('Enter your API key to cancel');return;}
try{
await fetch(API+'/v1/signals',{method:'POST',headers:hdr(),
body:JSON.stringify({source:'cli',channel:'ui',sender:'live',content:'cancel signal '+signalId})});
}catch(e){setStatus('cancel failed: '+e.message,true);}
}
async function startLive(){
if(!apiKey){flagKeyInput('Enter your API key to subscribe');return;}
stopLive();
liveCtrl=new AbortController();
const params=new URLSearchParams();
const k=document.getElementById('live-kind').value.trim();
const t=document.getElementById('live-tool').value.trim();
if(k)params.set('kind',k);
if(t)params.set('tool_id',t);
const url=API+'/v1/events'+(params.toString()?'?'+params:'');
setLiveState('connecting…',false);
let resp;
try{
resp=await fetch(url,{headers:{...hdr(),Accept:'text/event-stream'},signal:liveCtrl.signal});
}catch(e){
if(e.name!=='AbortError'){setLiveState('error: '+e.message,false);}
return;
}
if(!resp.ok){setLiveState('error '+resp.status,false);return;}
setLiveState('connected',true);
const reader=resp.body.getReader();
const dec=new TextDecoder();
let buf='', eventName='', dataBuf='';
try{
while(true){
const {value,done}=await reader.read();
if(done)break;
buf+=dec.decode(value,{stream:true});
let nl;
while((nl=buf.indexOf('\n'))!==-1){
const line=buf.slice(0,nl).replace(/\r$/,'');
buf=buf.slice(nl+1);
if(line===''){
if(eventName==='brain_event'&&dataBuf){
try{renderEvent(JSON.parse(dataBuf));}catch(_){}
}
eventName=''; dataBuf='';
}else if(line.startsWith('event:'))eventName=line.slice(6).trim();
else if(line.startsWith('data:'))dataBuf+=(dataBuf?'\n':'')+line.slice(5).trimStart();
}
}
}catch(e){
if(e.name!=='AbortError'){setLiveState('error: '+e.message,false);}
}
setLiveState('disconnected',false);
}
async function loadAudit(){
if(!apiKey){flagKeyInput('Enter your API key to view audit');return;}
const since=document.getElementById('audit-since').value.trim();
const content=since?`show audit since ${since}`:'show audit log';
const out=document.getElementById('audit-output');
out.textContent='loading…';
try{
const r=await fetch(API+'/v1/signals',{method:'POST',headers:hdr(),
body:JSON.stringify({source:'cli',channel:'ui',sender:'audit',content})});
if(r.status===401){flagKeyInput();out.textContent='';return;}
const j=await r.json();
out.textContent=(j.response&&j.response.text)||JSON.stringify(j,null,2);
}catch(e){out.textContent='error: '+e.message;}
}
document.getElementById('q').addEventListener('keydown',e=>{if(e.key==='Enter')search();});
window.addEventListener('beforeunload',stopLive);
checkHealth();
loadFacts();
</script>
</body>
</html>