0
TOOLS#49ec6fd4
Brain -- AI Context Vault
@GigaChad·deposited 2d ago·updated 2d ago·23 views
TOOLS#49ec6fd4
Brain -- AI Context Vault
GI
@GigaChad
23Views
0Comments
0Forks
0Saves
SHARE · REMIX
Brain -- AI Context Vault — a HTML Tools widget by @GigaChad.
CONTROLS
#obsidian#markdown#brain
No comments yet. Be the first!
✦ Remix with AI
SDK in this widget
vibes.onReadyvibes.savevibes.loadvibes.deletevibes.listKeys
Generated prompt
You are helping me modify a vibe-coded widget from itjustvibes.com.
[VIBE CODE: "Brain -- AI Context Vault" by @GigaChad]
Source: https://itjustvibes.com/GigaChad/brain-ai-context-vault
Type: HTML
--- SOURCE CODE ---
```html
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
<title>Brain -- AI Context Vault</title>
<style>:root{--bg:#0a0b10;--surface:#12141d;--s2:#181b27;--s3:#1e2231;--s4:#262b3d;--border:#232739;--border-soft:#1c2030;--border-strong:#34394f;--accent:#4cc9f0;--amber:#ffb43d;--mint:#6df0a4;--coral:#ff6b5d;--grape:#9d7aff;--txt:#ebecf2;--dim:#8d92a6;--faint:#5a5f74;--faintest:#3d4156;--body:'Inter',system-ui,-apple-system,Segoe UI,Roboto,sans-serif;--disp:'Space Grotesk',var(--body);--code:'JetBrains Mono',ui-monospace,Menlo,monospace}*{box-sizing:border-box}html,body{margin:0;height:100%}body{background:var(--bg);color:var(--txt);font-family:var(--body);font-size:14px;-webkit-font-smoothing:antialiased;overflow:hidden}button{font-family:inherit;color:inherit;cursor:pointer}.app{display:flex;flex-direction:column;height:100vh;height:100dvh}.top{display:flex;align-items:center;gap:8px;padding:8px 10px;background:var(--surface);border-bottom:1px solid var(--border);flex-shrink:0}.logo{font-family:var(--disp);font-weight:700;font-size:15px;letter-spacing:.2px;display:flex;align-items:center;gap:7px;white-space:nowrap}.dot{width:9px;height:9px;border-radius:3px;background:var(--grape);box-shadow:0 0 10px var(--grape)}.lock{font-size:11px;color:var(--mint);display:none}.lock.on{display:inline}.search{flex:1;min-width:60px;display:flex;align-items:center;gap:6px;background:var(--s2);border:1px solid var(--border);border-radius:8px;padding:6px 9px}.search input{flex:1;background:none;border:none;outline:none;color:var(--txt);font-size:13px}.search input::placeholder{color:var(--faint)}.kbd{font-family:var(--code);font-size:10px;color:var(--faint);border:1px solid var(--border-strong);border-radius:4px;padding:1px 5px}.tbtn{background:var(--s2);border:1px solid var(--border);border-radius:8px;padding:7px 10px;font-size:12px;color:var(--dim);display:flex;align-items:center;gap:6px;white-space:nowrap}.tbtn:hover{color:var(--txt);border-color:var(--border-strong)}.tbtn.on{color:var(--bg);background:var(--accent);border-color:var(--accent);font-weight:600}.tbtn.grape.on{background:var(--grape);border-color:var(--grape)}.meter{display:flex;flex-direction:column;gap:3px;width:80px}.meter .bar{height:5px;background:var(--s3);border-radius:3px;overflow:hidden}.meter .fill{height:100%;background:var(--mint);width:0%}.meter span{font-size:9px;color:var(--faint);font-family:var(--code)}.banner{background:linear-gradient(90deg,#2a1d10,#1e2231);border-bottom:1px solid var(--border);color:var(--amber);font-size:12px;padding:6px 12px;display:none;align-items:center;gap:8px}.banner b{color:var(--txt)}.main{flex:1;display:flex;min-height:0}.side{width:230px;flex-shrink:0;background:var(--surface);border-right:1px solid var(--border);display:flex;flex-direction:column;min-height:0}.side .head{padding:9px 11px;display:flex;align-items:center;justify-content:space-between;border-bottom:1px solid var(--border-soft)}.side .head h3{margin:0;font-size:11px;text-transform:uppercase;letter-spacing:.7px;color:var(--faint);font-weight:600}.newbtn{background:var(--grape);color:#0a0b10;border:none;border-radius:7px;width:26px;height:26px;font-size:18px;line-height:1;display:flex;align-items:center;justify-content:center;font-weight:700}.tags{display:flex;flex-wrap:wrap;gap:5px;padding:8px 11px;border-bottom:1px solid var(--border-soft)}.tag{font-size:11px;font-family:var(--code);color:var(--accent);background:var(--s2);border:1px solid var(--border);border-radius:20px;padding:2px 9px}.tag.on{background:var(--accent);color:var(--bg);border-color:var(--accent)}.tag.muted{color:var(--faint)}.list{flex:1;overflow-y:auto;padding:6px}.item{padding:9px 10px;border-radius:8px;margin-bottom:3px;border:1px solid transparent}.item:hover{background:var(--s2)}.item.on{background:var(--s2);border-color:var(--border-strong)}.item .t{font-size:13px;font-weight:600;color:var(--txt);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.item .m{font-size:11px;color:var(--faint);margin-top:2px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.empty{color:var(--faint);font-size:12px;text-align:center;padding:24px 12px}.pane{flex:1;display:flex;min-width:0}.col{flex:1;min-width:0;display:flex;flex-direction:column}.col.src{border-right:1px solid var(--border)}.titlebar{display:flex;align-items:center;gap:8px;padding:9px 14px;border-bottom:1px solid var(--border-soft);background:var(--surface)}.titlebar input{flex:1;background:none;border:none;outline:none;color:var(--txt);font-family:var(--disp);font-weight:700;font-size:18px}.icbtn{background:var(--s2);border:1px solid var(--border);border-radius:7px;color:var(--dim);font-size:12px;padding:5px 9px}.icbtn:hover{color:var(--txt);border-color:var(--border-strong)}.icbtn.del:hover{color:var(--coral);border-color:var(--coral)}textarea{flex:1;width:100%;resize:none;background:var(--bg);border:none;outline:none;color:var(--txt);font-family:var(--code);font-size:13.5px;line-height:1.65;padding:16px 18px}.render{flex:1;overflow-y:auto;padding:14px 20px;line-height:1.7}.render h1{font-family:var(--disp);font-size:24px;margin:.4em 0 .3em}.render h2{font-family:var(--disp);font-size:19px;margin:.9em 0 .3em;border-bottom:1px solid var(--border-soft);padding-bottom:4px}.render h3{font-family:var(--disp);font-size:16px;margin:.8em 0 .2em}.render p{margin:.5em 0}.render ul,.render ol{margin:.4em 0;padding-left:22px}.render li{margin:2px 0}.render code{font-family:var(--code);background:var(--s2);border:1px solid var(--border);border-radius:5px;padding:1px 5px;font-size:12.5px;color:var(--mint)}.render pre{background:var(--s2);border:1px solid var(--border);border-radius:9px;padding:12px 14px;overflow-x:auto}.render pre code{background:none;border:none;padding:0;color:var(--txt)}.render blockquote{border-left:3px solid var(--grape);margin:.5em 0;padding:2px 0 2px 14px;color:var(--dim)}.render a{color:var(--accent);text-decoration:none}.render a:hover{text-decoration:underline}.render hr{border:none;border-top:1px solid var(--border);margin:1em 0}.render .chk{accent-color:var(--mint);margin-right:6px}.wl{color:var(--grape);text-decoration:none;border-bottom:1px dashed var(--grape);cursor:pointer}.wl.miss{color:var(--coral);border-bottom-color:var(--coral)}.wl:hover{background:rgba(157,122,255,.12)}.htag{color:var(--accent);background:var(--s2);border-radius:5px;padding:0 5px;font-size:.9em;font-family:var(--code);cursor:pointer}.back{width:240px;flex-shrink:0;background:var(--surface);border-left:1px solid var(--border);overflow-y:auto;padding:4px}.bk-sec{padding:10px 12px}.bk-sec h4{margin:0 0 7px;font-size:11px;text-transform:uppercase;letter-spacing:.6px;color:var(--faint)}.bk{padding:8px 10px;background:var(--s2);border:1px solid var(--border);border-radius:8px;margin-bottom:6px}.bk .bt{font-size:12.5px;font-weight:600;color:var(--grape)}.bk .bc{font-size:11px;color:var(--dim);margin-top:3px;line-height:1.4}.bk:hover{border-color:var(--border-strong)}.none{color:var(--faintest);font-size:11.5px;font-style:italic}.graph{flex:1;position:relative;background:radial-gradient(circle at 50% 40%,#0e1018,#08090d)}.graph canvas{width:100%;height:100%;display:block}.ghint{position:absolute;top:10px;left:12px;font-size:11px;color:var(--faint);background:rgba(18,20,29,.7);border:1px solid var(--border);border-radius:8px;padding:5px 9px}.ac{position:absolute;z-index:40;background:var(--s3);border:1px solid var(--border-strong);border-radius:9px;box-shadow:0 14px 40px rgba(0,0,0,.5);overflow:hidden;display:none;min-width:200px;max-width:300px}.ac div{padding:8px 12px;font-size:13px}.ac div.sel{background:var(--grape);color:#0a0b10;font-weight:600}.ac div .new{color:var(--mint);font-size:11px}.ov{position:fixed;inset:0;background:rgba(5,6,10,.66);display:none;align-items:flex-start;justify-content:center;z-index:60;padding-top:9vh;backdrop-filter:blur(3px)}.qmod{width:min(560px,92vw);background:var(--s3);border:1px solid var(--border-strong);border-radius:14px;overflow:hidden;box-shadow:0 24px 70px rgba(0,0,0,.6)}.qmod input{width:100%;background:var(--s2);border:none;border-bottom:1px solid var(--border);outline:none;color:var(--txt);font-size:16px;padding:15px 18px}.qres{max-height:46vh;overflow-y:auto}.qr{padding:11px 18px;border-bottom:1px solid var(--border-soft);cursor:pointer}.qr.sel{background:var(--s2)}.qr .qt{font-weight:600}.qr .qm{font-size:11px;color:var(--faint);margin-top:2px}.modc{width:min(640px,94vw);max-height:84vh;overflow-y:auto;background:var(--s3);border:1px solid var(--border-strong);border-radius:14px;padding:18px}.modc h2{font-family:var(--disp);margin:0 0 4px;font-size:18px}.modc p{color:var(--dim);font-size:12.5px;margin:0 0 14px}.sec{border-top:1px solid var(--border-soft);padding:14px 0 4px}.sec h3{font-family:var(--disp);font-size:13px;margin:0 0 4px;color:var(--txt)}.sec .d{color:var(--faint);font-size:11.5px;margin:0 0 11px;line-height:1.5}.exprow{display:flex;gap:8px;flex-wrap:wrap;margin-bottom:6px}.expbtn{flex:1;min-width:150px;text-align:left;background:var(--s2);border:1px solid var(--border);border-radius:10px;padding:11px 13px}.expbtn:hover{border-color:var(--grape)}.expbtn b{display:block;font-size:13px;margin-bottom:3px}.expbtn small{color:var(--faint);font-size:11px;line-height:1.4;display:block}.expbtn.danger:hover{border-color:var(--coral)}.closebar{text-align:right;margin-top:10px}.row{display:flex;align-items:center;justify-content:space-between;gap:10px;margin-bottom:6px}.sw{position:relative;width:46px;height:26px;background:var(--s4);border-radius:20px;border:1px solid var(--border-strong);flex-shrink:0;transition:background .15s}.sw.on{background:var(--mint)}.sw i{position:absolute;top:2px;left:2px;width:20px;height:20px;border-radius:50%;background:#fff;transition:left .15s}.sw.on i{left:22px}.pin{width:100%;background:var(--s2);border:1px solid var(--border);border-radius:8px;color:var(--txt);font-size:15px;padding:11px 13px;outline:none;margin-bottom:8px}.pin:focus{border-color:var(--grape)}.btns{display:flex;gap:8px;justify-content:flex-end}.btn{border:1px solid var(--border);background:var(--s2);border-radius:9px;padding:9px 15px;font-size:13px}.btn.primary{background:var(--grape);border-color:var(--grape);color:#0a0b10;font-weight:700}.btn.warn{color:var(--coral);border-color:var(--coral)}.err{color:var(--coral);font-size:12px;min-height:16px;margin-bottom:6px}textarea.paste{width:100%;height:160px;background:var(--bg);border:1px solid var(--border);border-radius:9px;color:var(--txt);font-family:var(--code);font-size:12.5px;padding:11px;resize:vertical}.locked{position:fixed;inset:0;background:var(--bg);z-index:90;display:none;flex-direction:column;align-items:center;justify-content:center;gap:14px;text-align:center;padding:24px}.locked .big{font-size:40px}.locked h2{font-family:var(--disp);margin:0}.locked p{color:var(--dim);max-width:340px}.toast{position:fixed;bottom:18px;left:50%;transform:translateX(-50%);background:var(--s4);border:1px solid var(--border-strong);color:var(--txt);padding:10px 18px;border-radius:30px;font-size:13px;z-index:100;opacity:0;transition:opacity .2s;pointer-events:none}.toast.show{opacity:1}.mtabs{display:none}@media(max-width:780px){.side{position:absolute;z-index:30;height:100%;left:0;top:0;width:84vw;max-width:300px;transform:translateX(-100%);transition:transform .2s;box-shadow:8px 0 30px rgba(0,0,0,.5)}.side.open{transform:none}.back{display:none}.back.mopen{display:block;position:absolute;right:0;top:0;height:100%;z-index:30;width:84vw;max-width:300px;box-shadow:-8px 0 30px rgba(0,0,0,.5)}.col.src{display:none}.col.src.mshow{display:flex}.col.render-col{display:flex}.col.render-col.mhide{display:none}.mtabs{display:flex;gap:6px}.meter{display:none}}.menubtn{display:none}@media(max-width:780px){.menubtn{display:flex}}</style>
</head>
<body>
<div class="app">
<div class="top">
<button class="tbtn menubtn" id="mMenu" title="Notes">☰</button>
<div class="logo"><span class="dot"></span><span>Brain</span><span class="lock" id="lockBadge" title="Encrypted">🔒</span></div>
<div class="search"><span style="color:var(--faint)">⌕</span>
<input id="search" placeholder="Search notes..."><span class="kbd">⌘K</span></div>
<button class="tbtn on" id="vEdit">✎ Edit</button>
<button class="tbtn grape" id="vGraph">⌗ Graph</button>
<button class="tbtn" id="vBack" title="Backlinks">↹</button>
<button class="tbtn" id="expBtn" title="Export context for AI">⤴ Context</button>
<button class="tbtn" id="setBtn" title="Settings / Import / Export">⚙</button>
<div class="meter" id="meter" title="Storage used">
<div class="bar"><div class="fill" id="mFill"></div></div><span id="mTxt">-- / 500KB</span></div>
</div>
<div class="banner" id="banner"><span>●</span><span><b>Preview mode.</b> Changes are not saved here. Paste this widget into <b>itjustvibes.com</b> to enable per-user saving.</span></div>
<div class="main">
<aside class="side" id="side">
<div class="head"><h3>Notes <span id="nCount" style="color:var(--faint)"></span></h3>
<button class="newbtn" id="newBtn" title="New note">+</button></div>
<div class="tags" id="tagBar"></div>
<div class="list" id="list"></div>
</aside>
<div class="pane" id="pane">
<div class="col src render-col" id="editView" style="flex:1;display:flex;flex-direction:row">
<div class="col src mshow" id="srcCol">
<div class="titlebar">
<button class="tbtn mtabs" id="tabPrev">Preview</button>
<input id="title" placeholder="Untitled note">
<button class="icbtn" id="dupBtn" title="Duplicate">⧉</button>
<button class="icbtn del" id="delBtn" title="Delete">🗑</button>
</div>
<textarea id="editor" placeholder="Write in Markdown. Use [[Note Name]] to link, #tags to organize."></textarea>
</div>
<div class="col render-col" id="renderCol">
<div class="titlebar">
<button class="tbtn mtabs" id="tabEdit">Edit</button>
<span style="font-size:11px;color:var(--faint);text-transform:uppercase;letter-spacing:.6px">Preview</span>
<div style="flex:1"></div>
<button class="icbtn" id="copyOne" title="Copy this note + its links for AI">⤴ note</button>
</div>
<div class="render" id="render"></div>
</div>
</div>
<div class="graph" id="graphView" style="display:none">
<div class="ghint">Tap a node to open · drag to nudge</div>
<canvas id="gcanvas"></canvas>
</div>
</div>
<aside class="back" id="backPanel">
<div class="bk-sec"><h4>Linked mentions <span id="blCount" style="color:var(--grape)"></span></h4><div id="backlinks"></div></div>
<div class="bk-sec"><h4>Outgoing links</h4><div id="outlinks"></div></div>
</aside>
</div>
<div class="ac" id="ac"></div>
</div>
<div class="ov" id="qov"><div class="qmod">
<input id="qinput" placeholder="Jump to a note... (type to filter, Enter to open/create)">
<div class="qres" id="qres"></div></div></div>
<div class="ov" id="eov"><div class="modc">
<h2>Export AI context</h2>
<p>Bundle your vault into one Markdown block to paste into Claude, ChatGPT, or any AI tool.</p>
<div class="exprow">
<button class="expbtn" id="eAll"><b>📋 Copy whole vault</b><small>Every note as one Markdown doc.</small></button>
<button class="expbtn" id="eNote"><b>🎯 Copy current + linked</b><small>This note plus everything it links to.</small></button>
</div>
<div class="closebar"><button class="icbtn" id="eClose">Close</button></div>
</div></div>
<div class="ov" id="sov"><div class="modc">
<h2>Settings</h2>
<p>Manage privacy, and move your vault in and out -- fully Obsidian-compatible.</p>
<div class="sec">
<h3>🔒 Encryption</h3>
<div class="d" id="encDesc">Off -- notes are stored isolated to your account, but the platform can technically read them. Turn on for zero-knowledge privacy: notes are encrypted in your browser with a passphrase only you know.</div>
<div class="row"><span style="font-size:13px" id="encLabel">Encrypt my vault</span>
<div class="sw" id="encSw"><i></i></div></div>
<div class="exprow" id="encExtra" style="display:none">
<button class="expbtn" id="encChange"><b>Change passphrase</b><small>Re-key the whole vault.</small></button>
<button class="expbtn" id="encLock"><b>Lock now</b><small>Clear decrypted notes from memory.</small></button>
</div>
</div>
<div class="sec">
<h3>⤓ Import</h3>
<div class="d">Bring in an existing Obsidian vault or notes. Wikilinks and #tags come across automatically.</div>
<div class="exprow">
<button class="expbtn" id="impFolder"><b>📁 Import folder</b><small>Point at an Obsidian vault folder (.md files).</small></button>
<button class="expbtn" id="impFiles"><b>🗎 Import files</b><small>Pick one or more .md files.</small></button>
</div>
<div class="exprow">
<button class="expbtn" id="impPaste"><b>📋 Paste Markdown</b><small>Split on # headings into notes.</small></button>
<button class="expbtn" id="impJson"><b>↺ Restore backup</b><small>Replace vault from a .json backup.</small></button>
</div>
</div>
<div class="sec">
<h3>⤴ Export & backup</h3>
<div class="d">Your data is always portable. Open the .zip in Obsidian, or keep the .json for exact restore.</div>
<div class="exprow">
<button class="expbtn" id="expZip"><b>🗜 Vault as .zip</b><small>One .md per note -- a real Obsidian vault.</small></button>
<button class="expbtn" id="expMd"><b>📄 Vault as .md</b><small>Single Markdown file, all notes.</small></button>
<button class="expbtn" id="expJson"><b>💾 Backup .json</b><small>Exact round-trip for this widget.</small></button>
</div>
</div>
<div class="sec">
<h3>Storage</h3>
<div class="d" id="storeInfo">--</div>
<div class="exprow"><button class="expbtn danger" id="clearVault"><b style="color:var(--coral)">🗑 Clear vault</b><small>Delete every note. Cannot be undone.</small></button></div>
</div>
<div class="closebar"><button class="icbtn" id="sClose">Close</button></div>
</div></div>
<div class="ov" id="aov"><div class="qmod" style="padding:18px"><div id="aBody"></div></div></div>
<div class="locked" id="lockedScreen">
<div class="big">🔒</div><h2>Vault locked</h2>
<p>This brain is encrypted. Enter your passphrase to unlock.</p>
<button class="btn primary" id="unlockBtn">Unlock</button>
</div>
<input type="file" id="fileIn" accept=".md,.markdown,.txt,.json" multiple style="display:none">
<input type="file" id="dirIn" webkitdirectory directory multiple style="display:none">
<div class="toast" id="toast"></div>
<script>
"use strict";
(function(){
var $=function(id){return document.getElementById(id);};
function toast(m){var t=$("toast");t.textContent=m;t.classList.add("show");clearTimeout(t._t);t._t=setTimeout(function(){t.classList.remove("show");},1900);}
var SUB=(window.crypto&&crypto.subtle)?crypto.subtle:null;
var CK=null, ENC=false; var te=new TextEncoder(), tdc=new TextDecoder();
function b64e(buf){var u=new Uint8Array(buf),s="";for(var i=0;i<u.length;i++)s+=String.fromCharCode(u[i]);return btoa(s);}
function b64d(str){var s=atob(str),u=new Uint8Array(s.length);for(var i=0;i<s.length;i++)u[i]=s.charCodeAt(i);return u.buffer;}
async function deriveKey(pass,saltB64){
var base=await SUB.importKey("raw",te.encode(pass),{name:"PBKDF2"},false,["deriveKey"]);
return SUB.deriveKey({name:"PBKDF2",salt:b64d(saltB64),iterations:150000,hash:"SHA-256"},base,{name:"AES-GCM",length:256},false,["encrypt","decrypt"]);
}
async function encStr(plain){var iv=crypto.getRandomValues(new Uint8Array(12));
var ct=await SUB.encrypt({name:"AES-GCM",iv:iv},CK,te.encode(plain));return b64e(iv.buffer)+":"+b64e(ct);}
async function decStr(blob){var p=blob.split(":");var iv=new Uint8Array(b64d(p[0]));
return tdc.decode(await SUB.decrypt({name:"AES-GCM",iv:iv},CK,b64d(p[1])));}
var HAS=(typeof window!=="undefined"&&!!window.vibes&&typeof window.vibes.save==="function");
var IDX="brain-index", ENCKEY="brain-enc", SHARD_MAX=68000, CAP=500000;
var mem={};
// Persistent per-user state via the It Just Vibes SDK (window.vibes.save / load / delete / listKeys).
function loadRaw(k){if(HAS)return window.vibes.load(k);return Promise.resolve(k in mem?mem[k]:null);}
function saveRaw(k,v){if(HAS)return window.vibes.save(k,v);mem[k]=v;return Promise.resolve();}
function delRaw(k){if(HAS&&window.vibes.delete)return window.vibes.delete(k);delete mem[k];return Promise.resolve();}
async function rawGet(k){var v=await loadRaw(k);
if(v&&v.__enc){ if(!ENC||!CK) throw new Error("locked"); return JSON.parse(await decStr(v.d)); }
return v;}
async function rawSet(k,v){var out=v;
if(ENC&&CK&&k!==ENCKEY){ out={__enc:1,d:await encStr(JSON.stringify(v))}; }
return saveRaw(k,out);}
var index=null, shards={}, bytes=0;
function newId(){return "n"+Date.now().toString(36)+Math.random().toString(36).slice(2,6);}
function sizeOf(o){try{return JSON.stringify(o).length;}catch(e){return 0;}}
function recalcBytes(){bytes=sizeOf(index);for(var s in shards)bytes+=sizeOf(shards[s]);}
function shardWithRoom(addLen){var sids=Object.keys(shards);
for(var i=0;i<sids.length;i++){if(sizeOf(shards[sids[i]])+addLen<SHARD_MAX)return sids[i];}
var ns="s"+sids.length;shards[ns]={};return ns;}
function persistShard(sid){return rawSet("brain-"+sid,shards[sid]);}
function persistIndex(){return rawSet(IDX,index);}
async function bootStore(){
index=await rawGet(IDX);
if(!index||typeof index!=="object")index={v:1,notes:{},settings:{}};
if(!index.notes)index.notes={}; if(!index.settings)index.settings={};
var sids={};Object.keys(index.notes).forEach(function(id){sids[index.notes[id].s||"s0"]=1;});
if(HAS&&window.vibes.listKeys){try{var keys=await window.vibes.listKeys();(keys||[]).forEach(function(k){if(/^brain-s\d+$/.test(k))sids[k.slice(6)]=1;});}catch(e){}}
var ks=Object.keys(sids);if(!ks.length)ks=["s0"];
for(var i=0;i<ks.length;i++){var sd=await rawGet("brain-"+ks[i]);shards[ks[i]]=(sd&&typeof sd==="object")?sd:{};}
recalcBytes();
if(Object.keys(index.notes).length===0)await seed();
}
async function putNote(id,title,body){
var meta=index.notes[id];
var sid=meta?meta.s:shardWithRoom(body.length+title.length+40);
if(meta&&sizeOf(shards[sid])+body.length>SHARD_MAX&&Object.keys(shards[sid]).length>1){delete shards[sid][id];sid=shardWithRoom(body.length);}
if(!shards[sid])shards[sid]={};
shards[sid][id]=body; index.notes[id]={t:title,u:Date.now(),s:sid};
recalcBytes(); await persistShard(sid); await persistIndex();
}
async function removeNote(id){var m=index.notes[id];if(!m)return;
if(shards[m.s])delete shards[m.s][id]; delete index.notes[id]; recalcBytes();
if(shards[m.s]&&Object.keys(shards[m.s]).length===0&&m.s!=="s0"){delete shards[m.s];await delRaw("brain-"+m.s);}
else if(shards[m.s])await persistShard(m.s);
await persistIndex();}
function bodyOf(id){var m=index.notes[id];if(!m)return "";return (shards[m.s]&&shards[m.s][id])||"";}
function allNotes(){return Object.keys(index.notes).map(function(id){
return {id:id,title:index.notes[id].t,updated:index.notes[id].u,body:bodyOf(id)};});}
function parseLinks(b){var o=[],re=/\[\[([^\]|]+)(?:\|[^\]]+)?\]\]/g,m;while((m=re.exec(b))){var t=m[1].trim();if(t&&o.indexOf(t)<0)o.push(t);}return o;}
function parseTags(b){var o=[],re=/(^|\s)#([a-z0-9][\w/-]*)/gi,m;while((m=re.exec(b))){var t=m[2].toLowerCase();if(o.indexOf(t)<0)o.push(t);}return o;}
function findByTitle(t){t=t.trim().toLowerCase();var n=allNotes();for(var i=0;i<n.length;i++)if(n[i].title.trim().toLowerCase()===t)return n[i];return null;}
function allTags(){var c={};allNotes().forEach(function(x){parseTags(x.body).forEach(function(t){c[t]=(c[t]||0)+1;});});
return Object.keys(c).sort(function(a,b){return c[b]-c[a];}).map(function(t){return{tag:t,n:c[t]};});}
function esc(s){return s.replace(/&/g,"&"+"amp;").replace(/</g,"&"+"lt;").replace(/>/g,"&"+"gt;");}
function inline(s){s=esc(s);var codes=[];
s=s.replace(/`([^`]+)`/g,function(_,c){codes.push(c);return "\u0000"+(codes.length-1)+"\u0000";});
s=s.replace(/\[\[([^\]|]+)(?:\|([^\]]+))?\]\]/g,function(_,t,a){var label=esc((a||t).trim()),tgt=t.trim();
return '<a class="wl'+(findByTitle(tgt)?"":" miss")+'" data-wl="'+encodeURIComponent(tgt)+'">'+label+'</a>';});
s=s.replace(/\[([^\]]+)\]\((https?:[^)\s]+)\)/g,'<a href="$2" target="_blank" rel="noopener">$1</a>');
s=s.replace(/\*\*([^*]+)\*\*/g,"<strong>$1</strong>");
s=s.replace(/(^|[\s(])\*([^*\n]+)\*/g,"$1<em>$2</em>");
s=s.replace(/(^|[\s(])_([^_\n]+)_/g,"$1<em>$2</em>");
s=s.replace(/(^|\s)#([a-z0-9][\w/-]*)/gi,'$1<span class="htag" data-tag="$2">#$2</span>');
s=s.replace(/\u0000(\d+)\u0000/g,function(_,i){return "<code>"+esc(codes[+i])+"</code>";});
return s;}
function mdRender(src){var lines=src.split("\n"),out=[],i=0;
while(i<lines.length){var ln=lines[i];
if(/^```/.test(ln)){var buf=[];i++;while(i<lines.length&&!/^```/.test(lines[i])){buf.push(lines[i]);i++;}i++;out.push("<pre><code>"+esc(buf.join("\n"))+"</code></pre>");continue;}
var h=ln.match(/^(#{1,3})\s+(.*)$/);if(h){out.push("<h"+h[1].length+">"+inline(h[2])+"</h"+h[1].length+">");i++;continue;}
if(/^\s*---\s*$/.test(ln)){out.push("<hr>");i++;continue;}
if(/^>\s?/.test(ln)){var q=[];while(i<lines.length&&/^>\s?/.test(lines[i])){q.push(lines[i].replace(/^>\s?/,""));i++;}out.push("<blockquote>"+inline(q.join(" "))+"</blockquote>");continue;}
if(/^\s*([-*]|\d+\.)\s+/.test(ln)){var ol=/^\s*\d+\.\s+/.test(ln),items=[];
while(i<lines.length&&/^\s*([-*]|\d+\.)\s+/.test(lines[i])){var it=lines[i].replace(/^\s*([-*]|\d+\.)\s+/,"");
var cb=it.match(/^\[([ xX])\]\s+(.*)$/);
if(cb)items.push('<li><input type="checkbox" class="chk" disabled '+(cb[1].trim()?"checked":"")+">"+inline(cb[2])+"</li>");
else items.push("<li>"+inline(it)+"</li>");i++;}
out.push((ol?"<ol>":"<ul>")+items.join("")+(ol?"</ol>":"</ul>"));continue;}
if(/^\s*$/.test(ln)){i++;continue;}
var para=[ln];i++;
while(i<lines.length&&!/^\s*$/.test(lines[i])&&!/^(#{1,3}\s|>|\s*([-*]|\d+\.)\s|```|---\s*$)/.test(lines[i])){para.push(lines[i]);i++;}
out.push("<p>"+inline(para.join(" "))+"</p>");}
return out.join("\n");}
var cur=null, view="edit", tagFilter=null, dirty=false, saveTimer=null;
var els={list:$("list"),tagBar:$("tagBar"),title:$("title"),editor:$("editor"),render:$("render"),
backlinks:$("backlinks"),outlinks:$("outlinks"),nCount:$("nCount"),blCount:$("blCount"),
search:$("search"),mFill:$("mFill"),mTxt:$("mTxt"),banner:$("banner"),
editView:$("editView"),graphView:$("graphView"),ac:$("ac")};
function fmtTime(ts){var d=Math.floor((Date.now()-ts)/1000);if(d<60)return"just now";if(d<3600)return Math.floor(d/60)+"m ago";if(d<86400)return Math.floor(d/3600)+"h ago";return Math.floor(d/86400)+"d ago";}
function snippet(b){return b.replace(/[#*`>\[\]]/g,"").replace(/\s+/g," ").trim().slice(0,60);}
function renderMeter(){var pct=Math.min(100,Math.round(bytes/CAP*100));els.mFill.style.width=pct+"%";
els.mFill.style.background=pct>90?"var(--coral)":pct>70?"var(--amber)":"var(--mint)";
els.mTxt.textContent=(Math.round(bytes/1024))+"KB / 500KB";}
function renderTags(){var tags=allTags(),h="";
if(tagFilter)h+='<span class="tag on" data-tag="'+tagFilter+'">#'+tagFilter+' ✕</span>';
tags.forEach(function(t){if(t.tag!==tagFilter)h+='<span class="tag muted" data-tag="'+t.tag+'">#'+t.tag+' '+t.n+'</span>';});
els.tagBar.innerHTML=h||'<span style="color:var(--faintest);font-size:11px">no #tags yet</span>';}
function renderList(){var q=els.search.value.trim().toLowerCase();
var notes=allNotes().sort(function(a,b){return b.updated-a.updated;});
if(tagFilter)notes=notes.filter(function(n){return parseTags(n.body).indexOf(tagFilter)>=0;});
if(q)notes=notes.filter(function(n){return (n.title+" "+n.body).toLowerCase().indexOf(q)>=0;});
els.nCount.textContent=allNotes().length;
if(!notes.length){els.list.innerHTML='<div class="empty">No notes.<br>Tap + to create one.</div>';return;}
els.list.innerHTML=notes.map(function(n){return '<div class="item'+(cur===n.id?" on":"")+'" data-id="'+n.id+'">'+
'<div class="t">'+esc(n.title||"Untitled")+'</div><div class="m">'+(snippet(n.body)||"empty")+' · '+fmtTime(n.updated)+'</div></div>';}).join("");}
function renderBacklinks(){if(!cur){els.backlinks.innerHTML="";els.outlinks.innerHTML="";els.blCount.textContent="";return;}
var ct=(index.notes[cur].t||"").trim().toLowerCase();
var ins=allNotes().filter(function(n){return n.id!==cur&&parseLinks(n.body).some(function(l){return l.toLowerCase()===ct;});});
els.blCount.textContent=ins.length||"";
els.backlinks.innerHTML=ins.length?ins.map(function(n){return '<div class="bk" data-id="'+n.id+'"><div class="bt">'+esc(n.title)+'</div><div class="bc">'+esc(snippet(n.body))+'</div></div>';}).join(""):'<div class="none">No notes link here yet.</div>';
var outs=parseLinks(bodyOf(cur));
els.outlinks.innerHTML=outs.length?outs.map(function(t){var hit=findByTitle(t);
return '<div class="bk" data-wl="'+encodeURIComponent(t)+'"><div class="bt" style="color:'+(hit?"var(--grape)":"var(--coral)")+'">'+esc(t)+(hit?"":" (new)")+'</div></div>';}).join(""):'<div class="none">No outgoing links.</div>';}
function renderEditor(){if(!cur){els.title.value="";els.editor.value="";els.render.innerHTML='<div class="empty">Select or create a note.</div>';return;}
var m=index.notes[cur];els.title.value=m.t;els.editor.value=bodyOf(cur);els.render.innerHTML=mdRender(els.editor.value);}
function renderAll(){renderList();renderTags();renderEditor();renderBacklinks();renderMeter();if(view==="graph")drawGraph();}
function openNote(id){if(dirty)flushSave();cur=id;setView("edit");renderAll();closeMobilePanels();els.editor.focus();}
function openTitle(title){var hit=findByTitle(title);if(hit){openNote(hit.id);return;}
var id=newId();putNote(id,title,"").then(function(){openNote(id);});}
async function createNote(){var id=newId();await putNote(id,"Untitled","");cur=id;renderAll();setView("edit");els.title.focus();els.title.select();closeMobilePanels();}
function scheduleSave(){dirty=true;clearTimeout(saveTimer);saveTimer=setTimeout(flushSave,600);}
function flushSave(){if(!cur||!dirty)return;clearTimeout(saveTimer);dirty=false;
putNote(cur,els.title.value||"Untitled",els.editor.value).then(function(){renderList();renderTags();renderMeter();renderBacklinks();}).catch(function(e){toast("Save failed: "+(e.message||e));});}
async function deleteCur(){if(!cur)return;
var ok=await ask({kind:"confirm",title:"Delete note",msg:"Delete \""+(index.notes[cur].t||"Untitled")+"\"? This cannot be undone.",okText:"Delete",warn:true});
if(!ok)return;await removeNote(cur);var n=allNotes();cur=n.length?n.sort(function(a,b){return b.updated-a.updated;})[0].id:null;renderAll();}
async function dupCur(){if(!cur)return;flushSave();var id=newId();await putNote(id,(index.notes[cur].t||"Untitled")+" copy",bodyOf(cur));openNote(id);}
function setView(v){view=v;$("vEdit").classList.toggle("on",v==="edit");$("vGraph").classList.toggle("on",v==="graph");
els.editView.style.display=v==="edit"?"flex":"none";els.graphView.style.display=v==="graph"?"block":"none";if(v==="graph")drawGraph();}
var gcanvas=$("gcanvas"),gx=gcanvas.getContext("2d"),gnodes=[],gedges=[],dragNode=null,graphAnim=null,ticks=0;
function buildGraph(){var notes=allNotes(),byT={};
gnodes=notes.map(function(n,k){byT[n.title.trim().toLowerCase()]=k;
return {id:n.id,t:n.title,x:Math.cos(k)*120+(gcanvas.width/2||300),y:Math.sin(k*1.7)*120+(gcanvas.height/2||200),vx:0,vy:0,deg:0};});
gedges=[];notes.forEach(function(n,k){parseLinks(n.body).forEach(function(l){var j=byT[l.toLowerCase()];if(j!=null&&j!==k){gedges.push([k,j]);gnodes[k].deg++;gnodes[j].deg++;}});});}
function sizeCanvas(){var r=gcanvas.parentElement.getBoundingClientRect();gcanvas.width=r.width;gcanvas.height=r.height;}
function stepGraph(){var W=gcanvas.width,H=gcanvas.height,cx=W/2,cy=H/2;
for(var i=0;i<gnodes.length;i++){var a=gnodes[i];if(a===dragNode)continue;a.vx+=(cx-a.x)*0.0016;a.vy+=(cy-a.y)*0.0016;
for(var j=0;j<gnodes.length;j++){if(i===j)continue;var b=gnodes[j];var dx=a.x-b.x,dy=a.y-b.y,d2=dx*dx+dy*dy+0.01,d=Math.sqrt(d2),f=1400/d2;a.vx+=dx/d*f;a.vy+=dy/d*f;}}
gedges.forEach(function(e){var a=gnodes[e[0]],b=gnodes[e[1]];var dx=b.x-a.x,dy=b.y-a.y,d=Math.sqrt(dx*dx+dy*dy)||1,f=(d-90)*0.01;a.vx+=dx/d*f;a.vy+=dy/d*f;b.vx-=dx/d*f;b.vy-=dy/d*f;});
gnodes.forEach(function(a){if(a===dragNode)return;a.vx*=0.86;a.vy*=0.86;a.x+=a.vx;a.y+=a.vy;});}
function paintGraph(){var W=gcanvas.width,H=gcanvas.height;gx.clearRect(0,0,W,H);
gx.strokeStyle="rgba(157,122,255,.28)";gx.lineWidth=1;
gedges.forEach(function(e){var a=gnodes[e[0]],b=gnodes[e[1]];gx.beginPath();gx.moveTo(a.x,a.y);gx.lineTo(b.x,b.y);gx.stroke();});
gnodes.forEach(function(a){var r=5+Math.min(9,a.deg*1.6),on=a.id===cur;gx.beginPath();gx.arc(a.x,a.y,r,0,7);
gx.fillStyle=on?"#4cc9f0":"#9d7aff";gx.shadowColor=gx.fillStyle;gx.shadowBlur=on?14:6;gx.fill();gx.shadowBlur=0;
gx.fillStyle="#ebecf2";gx.font="11px Inter,sans-serif";gx.textAlign="center";gx.fillText(a.t.slice(0,18),a.x,a.y+r+12);});}
function drawGraph(){sizeCanvas();buildGraph();ticks=0;cancelAnimationFrame(graphAnim);
(function loop(){if(view!=="graph")return;if(ticks<240||dragNode){stepGraph();ticks++;}paintGraph();graphAnim=requestAnimationFrame(loop);})();}
function gpos(ev){var r=gcanvas.getBoundingClientRect();var t=ev.touches?ev.touches[0]:ev;return {x:t.clientX-r.left,y:t.clientY-r.top};}
function ghit(p){for(var i=0;i<gnodes.length;i++){var a=gnodes[i],dx=a.x-p.x,dy=a.y-p.y;if(dx*dx+dy*dy<260)return a;}return null;}
gcanvas.addEventListener("mousedown",function(e){dragNode=ghit(gpos(e));});
gcanvas.addEventListener("mousemove",function(e){if(dragNode){var p=gpos(e);dragNode.x=p.x;dragNode.y=p.y;dragNode.vx=dragNode.vy=0;}});
window.addEventListener("mouseup",function(e){if(dragNode){var p=gpos(e),h=ghit(p);if(h===dragNode)openNote(dragNode.id);}dragNode=null;});
gcanvas.addEventListener("touchstart",function(e){dragNode=ghit(gpos(e));},{passive:true});
gcanvas.addEventListener("touchmove",function(e){if(dragNode){var p=gpos(e);dragNode.x=p.x;dragNode.y=p.y;e.preventDefault();}},{passive:false});
gcanvas.addEventListener("touchend",function(){if(dragNode)openNote(dragNode.id);dragNode=null;});
function acState(){var ta=els.editor,pos=ta.selectionStart,txt=ta.value.slice(0,pos);var m=txt.match(/\[\[([^\]\n]*)$/);
if(!m){els.ac.style.display="none";return;}
var frag=m[1].toLowerCase();var matches=allNotes().filter(function(n){return n.title.toLowerCase().indexOf(frag)>=0;}).slice(0,6);
var html="";matches.forEach(function(n,i){html+='<div class="'+(i===0?"sel":"")+'" data-ins="'+esc(n.title)+'">'+esc(n.title)+'</div>';});
if(frag)html+='<div class="'+(matches.length?"":"sel")+'" data-ins="'+esc(m[1])+'" data-new="1">'+esc(m[1])+' <span class="new">+ create</span></div>';
if(!html){els.ac.style.display="none";return;}
els.ac.innerHTML=html;var r=ta.getBoundingClientRect();
els.ac.style.left=Math.min(r.left+14,window.innerWidth-220)+"px";els.ac.style.top=(r.bottom-150)+"px";els.ac.style.display="block";}
function acInsert(title){var ta=els.editor,pos=ta.selectionStart,before=ta.value.slice(0,pos),after=ta.value.slice(pos);
before=before.replace(/\[\[[^\]\n]*$/,"[["+title+"]]");ta.value=before+after;ta.selectionStart=ta.selectionEnd=before.length;
els.ac.style.display="none";els.render.innerHTML=mdRender(ta.value);scheduleSave();}
els.ac.addEventListener("mousedown",function(e){var d=e.target.closest("[data-ins]");if(d){e.preventDefault();acInsert(d.getAttribute("data-ins"));}});
function vaultMarkdown(){return allNotes().sort(function(a,b){return a.title.localeCompare(b.title);}).map(function(n){return "# "+(n.title||"Untitled")+"\n\n"+n.body.trim();}).join("\n\n---\n\n");}
function noteWithLinks(id){var seen={},order=[];(function add(nid){if(seen[nid])return;seen[nid]=1;order.push(nid);
parseLinks(bodyOf(nid)).forEach(function(l){var h=findByTitle(l);if(h)add(h.id);});})(id);
return order.map(function(nid){return "# "+(index.notes[nid].t||"Untitled")+"\n\n"+bodyOf(nid).trim();}).join("\n\n---\n\n");}
function copyText(t){if(navigator.clipboard&&navigator.clipboard.writeText){navigator.clipboard.writeText(t).then(function(){toast("Copied to clipboard");},function(){fallbackCopy(t);});}else fallbackCopy(t);}
function fallbackCopy(t){var ta=document.createElement("textarea");ta.value=t;ta.style.position="fixed";ta.style.opacity="0";document.body.appendChild(ta);ta.select();try{document.execCommand("copy");toast("Copied");}catch(e){toast("Copy failed");}document.body.removeChild(ta);}
function downloadBlob(name,blob){try{var u=URL.createObjectURL(blob);var a=document.createElement("a");a.href=u;a.download=name;document.body.appendChild(a);a.click();document.body.removeChild(a);setTimeout(function(){URL.revokeObjectURL(u);},1500);toast("Download started");}catch(e){toast("Download blocked in this sandbox");}}
function downloadText(name,text,type){downloadBlob(name,new Blob([text],{type:type||"text/markdown"}));}
function backupObj(){return {app:"vibes-brain",v:1,exported:new Date().toISOString(),notes:allNotes().map(function(n){return {title:n.title,body:n.body,updated:n.updated};})};}
var CRC=(function(){var t=[];for(var n=0;n<256;n++){var c=n;for(var k=0;k<8;k++)c=c&1?0xEDB88320^(c>>>1):c>>>1;t[n]=c>>>0;}return t;})();
function crc32(u8){var c=0xFFFFFFFF;for(var i=0;i<u8.length;i++)c=CRC[(c^u8[i])&0xFF]^(c>>>8);return (c^0xFFFFFFFF)>>>0;}
function u32(v){return [v&255,(v>>>8)&255,(v>>>16)&255,(v>>>24)&255];}
function u16(v){return [v&255,(v>>>8)&255];}
function zipVault(){var enc=new TextEncoder(),files=allNotes(),used={};var parts=[],central=[],offset=0;
function fname(t){var b=(t||"Untitled").replace(/[\/\\:*?"<>|\n\r]+/g,"-").trim()||"Untitled";var nm=b+".md",i=2;while(used[nm.toLowerCase()]){nm=b+" "+(i++)+".md";}used[nm.toLowerCase()]=1;return nm;}
files.forEach(function(n){
var nm=fname(n.title),nameB=enc.encode(nm),data=enc.encode(n.body||"");var crc=crc32(data);
var lh=[].concat(u32(0x04034b50),u16(20),u16(0),u16(0),u16(0),u16(0),u32(crc),u32(data.length),u32(data.length),u16(nameB.length),u16(0));
var chunk=new Uint8Array(lh.length+nameB.length+data.length);chunk.set(lh,0);chunk.set(nameB,lh.length);chunk.set(data,lh.length+nameB.length);
parts.push(chunk);
var ch=[].concat(u32(0x02014b50),u16(20),u16(20),u16(0),u16(0),u16(0),u16(0),u32(crc),u32(data.length),u32(data.length),u16(nameB.length),u16(0),u16(0),u16(0),u16(0),u32(0),u32(offset));
var cd=new Uint8Array(ch.length+nameB.length);cd.set(ch,0);cd.set(nameB,ch.length);central.push(cd);
offset+=chunk.length;});
var cdSize=central.reduce(function(a,c){return a+c.length;},0);
var eocd=new Uint8Array([].concat(u32(0x06054b50),u16(0),u16(0),u16(files.length),u16(files.length),u32(cdSize),u32(offset),u16(0)));
var total=offset+cdSize+eocd.length,out=new Uint8Array(total),p=0;
parts.forEach(function(c){out.set(c,p);p+=c.length;});central.forEach(function(c){out.set(c,p);p+=c.length;});out.set(eocd,p);
return new Blob([out],{type:"application/zip"});}
function uniqueTitle(t){if(!findByTitle(t))return t;var i=2;while(findByTitle(t+" "+i))i++;return t+" "+i;}
async function importFileList(list){var files=Array.prototype.slice.call(list);
var json=files.filter(function(f){return /\.json$/i.test(f.name);});
if(json.length){try{var obj=JSON.parse(await json[0].text());await restoreBackup(obj);}catch(e){toast("Bad backup file");}return;}
var added=0;
for(var k=0;k<files.length;k++){var f=files[k];if(!/\.(md|markdown|txt)$/i.test(f.name))continue;
var txt=await f.text();var base=(f.name.split("/").pop()||f.name).replace(/\.(md|markdown|txt)$/i,"");
await putNote(newId(),uniqueTitle(base||"Untitled"),txt);added++;}
if(!cur){var n=allNotes();if(n.length)cur=n[0].id;}
renderAll();toast("Imported "+added+" note"+(added===1?"":"s"));}
async function restoreBackup(obj){var notes=(obj&&obj.notes)||[];
var ok=await ask({kind:"confirm",title:"Restore backup",msg:"Replace your current vault with "+notes.length+" notes from this backup? Your current notes will be removed.",okText:"Replace",warn:true});
if(!ok)return;
var oldS=Object.keys(shards);for(var s in shards)delete shards[s];shards={s0:{}};index={v:1,notes:{},settings:index.settings||{}};
for(var oi=0;oi<oldS.length;oi++){if(oldS[oi]!=="s0")await delRaw("brain-"+oldS[oi]);}
for(var i=0;i<notes.length;i++){await putNote(newId(),notes[i].title||"Untitled",notes[i].body||"");}
var n=allNotes();cur=n.length?n[0].id:null;renderAll();toast("Restored "+notes.length+" notes");}
function parsePaste(text){var lines=text.split("\n"),notes=[],c=null;
lines.forEach(function(ln){var m=ln.match(/^#\s+(.+)/);
if(m){if(c)notes.push(c);c={title:m[1].trim(),body:""};}
else if(/^---\s*$/.test(ln)){if(c){notes.push(c);c=null;}}
else{if(!c)c={title:"",body:""};c.body+=(c.body?"\n":"")+ln;}});
if(c)notes.push(c);
return notes.map(function(n){return {title:(n.title||"").trim(),body:n.body.trim()};}).filter(function(n){return n.title||n.body;});}
function ask(o){return new Promise(function(res){
var ov=$("aov"),body=$("aBody"),done=false;
function close(v){if(done)return;done=true;ov.style.display="none";body.innerHTML="";res(v);}
var html='<div style="padding:4px 2px"><h2 style="font-family:var(--disp);margin:0 0 6px;font-size:17px">'+esc(o.title||"")+'</h2>';
if(o.msg)html+='<p style="color:var(--dim);font-size:13px;margin:0 0 12px;line-height:1.5">'+esc(o.msg)+'</p>';
if(o.kind==="pass"){html+='<div class="err" id="aErr"></div><input class="pin" id="aP1" type="password" placeholder="Passphrase" autocomplete="new-password">';
if(o.confirm)html+='<input class="pin" id="aP2" type="password" placeholder="Confirm passphrase">';
html+='<p style="color:var(--faint);font-size:11px;margin:2px 0 12px">There is no recovery. If you forget this passphrase, the notes cannot be decrypted.</p>';}
html+='<div class="btns"><button class="btn" id="aCancel">Cancel</button><button class="btn '+(o.warn?"warn":"primary")+'" id="aOk">'+esc(o.okText||"OK")+'</button></div></div>';
body.innerHTML=html;ov.style.display="flex";
var ok=$("aOk"),cancel=$("aCancel");
if(o.kind==="pass"){var p1=$("aP1");setTimeout(function(){p1.focus();},30);
ok.onclick=function(){var v=p1.value;if(!v||v.length<6){$("aErr").textContent="Use at least 6 characters.";return;}
if(o.confirm&&v!==$("aP2").value){$("aErr").textContent="Passphrases don't match.";return;}close(v);};
body.addEventListener("keydown",function(e){if(e.key==="Enter"){e.preventDefault();ok.click();}});}
else ok.onclick=function(){close(true);};
cancel.onclick=function(){close(o.kind==="pass"?null:false);};
ov.onclick=function(e){if(e.target===ov)close(o.kind==="pass"?null:false);};
});}
async function readEncMeta(){var v=await loadRaw(ENCKEY);return (v&&typeof v==="object")?v:{on:false};}
async function enableEnc(){
if(!SUB){toast("Encryption needs a secure (HTTPS) context");return;}
var p=await ask({kind:"pass",confirm:true,title:"Set a passphrase",msg:"Your notes will be encrypted in your browser. Only this passphrase can unlock them.",okText:"Encrypt vault"});
if(!p)return;
var salt=b64e(crypto.getRandomValues(new Uint8Array(16)).buffer);
CK=await deriveKey(p,salt);ENC=true;
var check=await encStr("brain-verify");
await saveRaw(ENCKEY,{on:true,salt:salt,check:check});
await persistIndex();for(var s in shards)await persistShard(s);
toast("Encryption on -- vault secured");renderSettings();updateLockBadge();}
async function disableEnc(){
var ok=await ask({kind:"confirm",title:"Turn off encryption",msg:"Your notes will be stored unencrypted again. Continue?",okText:"Turn off",warn:true});
if(!ok)return;
ENC=false;await saveRaw(ENCKEY,{on:false});
await persistIndex();for(var s in shards)await persistShard(s);
CK=null;toast("Encryption off");renderSettings();updateLockBadge();}
async function changePass(){
var p=await ask({kind:"pass",confirm:true,title:"New passphrase",msg:"Re-encrypt the whole vault under a new passphrase.",okText:"Change"});
if(!p)return;var salt=b64e(crypto.getRandomValues(new Uint8Array(16)).buffer);
CK=await deriveKey(p,salt);var check=await encStr("brain-verify");
await saveRaw(ENCKEY,{on:true,salt:salt,check:check});
await persistIndex();for(var s in shards)await persistShard(s);
toast("Passphrase changed");}
function lockNow(){ if(!ENC){toast("Vault is not encrypted");return;} window.location.reload(); }
function updateLockBadge(){$("lockBadge").classList.toggle("on",ENC);}
function renderSettings(){
var sw=$("encSw");sw.classList.toggle("on",ENC);
$("encExtra").style.display=ENC?"flex":"none";
$("encLabel").textContent=ENC?"Vault encrypted":"Encrypt my vault";
$("encDesc").textContent=ENC
? "On -- every note is encrypted in your browser with AES-256. The platform stores only ciphertext and cannot read your content. Lose the passphrase = lose the data."
: (SUB? "Off -- notes are isolated to your account by access controls, but platform operators can technically read them. Turn on for zero-knowledge privacy."
: "Encryption unavailable -- needs a secure (HTTPS) context. It will work once this widget is live on itjustvibes.com.");
$("storeInfo").textContent=allNotes().length+" notes · "+Math.round(bytes/1024)+"KB of 500KB used"+(HAS?"":" (preview -- not saved)");}
var qSel=0,qList=[];
function openQuick(){$("qov").style.display="flex";var qi=$("qinput");qi.value="";qFilter("");qi.focus();}
function closeQuick(){$("qov").style.display="none";}
function qFilter(q){q=q.toLowerCase();
qList=allNotes().filter(function(n){return n.title.toLowerCase().indexOf(q)>=0||n.body.toLowerCase().indexOf(q)>=0;}).sort(function(a,b){return b.updated-a.updated;}).slice(0,8);qSel=0;
var html=qList.map(function(n,i){return '<div class="qr'+(i===0?" sel":"")+'" data-id="'+n.id+'"><div class="qt">'+esc(n.title)+'</div><div class="qm">'+esc(snippet(n.body))+'</div></div>';}).join("");
if(q&&!findByTitle(q))html+='<div class="qr'+(qList.length?"":" sel")+'" data-new="'+esc(q)+'"><div class="qt" style="color:var(--mint)">+ Create "'+esc(q)+'"</div></div>';
$("qres").innerHTML=html||'<div class="qr"><div class="qm">No matches</div></div>';}
document.addEventListener("click",function(e){var t=e.target;
var item=t.closest(".item");if(item){openNote(item.getAttribute("data-id"));return;}
var wl=t.closest("[data-wl]");if(wl){openTitle(decodeURIComponent(wl.getAttribute("data-wl")));return;}
var ht=t.closest("[data-tag]");if(ht){var tg=ht.getAttribute("data-tag");tagFilter=(tagFilter===tg?null:tg);renderList();renderTags();return;}
var bk=t.closest(".bk[data-id]");if(bk){openNote(bk.getAttribute("data-id"));return;}
var qr=t.closest(".qr");if(qr){if(qr.getAttribute("data-id"))openNote(qr.getAttribute("data-id"));else if(qr.getAttribute("data-new"))openTitle(qr.getAttribute("data-new"));closeQuick();return;}});
$("newBtn").onclick=createNote;$("delBtn").onclick=deleteCur;$("dupBtn").onclick=dupCur;
$("vEdit").onclick=function(){setView("edit");};$("vGraph").onclick=function(){setView("graph");};
$("vBack").onclick=function(){$("backPanel").classList.toggle("mopen");};
$("mMenu").onclick=function(){$("side").classList.toggle("open");};
$("expBtn").onclick=function(){$("eov").style.display="flex";};
$("eClose").onclick=function(){$("eov").style.display="none";};
$("eAll").onclick=function(){copyText(vaultMarkdown());$("eov").style.display="none";};
$("eNote").onclick=function(){if(cur)copyText(noteWithLinks(cur));$("eov").style.display="none";};
$("copyOne").onclick=function(){if(cur)copyText(noteWithLinks(cur));};
$("tabPrev").onclick=function(){$("srcCol").classList.remove("mshow");$("renderCol").classList.remove("mhide");};
$("tabEdit").onclick=function(){$("srcCol").classList.add("mshow");$("renderCol").classList.add("mhide");};
$("setBtn").onclick=function(){renderSettings();$("sov").style.display="flex";};
$("sClose").onclick=function(){$("sov").style.display="none";};
$("encSw").onclick=function(){ENC?disableEnc():enableEnc();};
$("encChange").onclick=changePass;$("encLock").onclick=lockNow;
$("expZip").onclick=function(){downloadBlob("brain-vault.zip",zipVault());};
$("expMd").onclick=function(){downloadText("brain-vault.md",vaultMarkdown());};
$("expJson").onclick=function(){downloadText("brain-backup.json",JSON.stringify(backupObj(),null,2),"application/json");};
$("impFiles").onclick=function(){$("fileIn").click();};
$("impFolder").onclick=function(){$("dirIn").click();};
$("impJson").onclick=function(){$("fileIn").click();};
$("fileIn").onchange=function(e){if(e.target.files&&e.target.files.length){importFileList(e.target.files);$("sov").style.display="none";}e.target.value="";};
$("dirIn").onchange=function(e){if(e.target.files&&e.target.files.length){importFileList(e.target.files);$("sov").style.display="none";}e.target.value="";};
$("impPaste").onclick=async function(){
var ov=$("aov"),body=$("aBody");
body.innerHTML='<div style="padding:4px 2px"><h2 style="font-family:var(--disp);margin:0 0 8px;font-size:17px">Paste Markdown</h2>'+
'<p style="color:var(--dim);font-size:12px;margin:0 0 8px">Each <code>#</code> heading (or <code>---</code> divider) becomes a note.</p>'+
'<textarea class="paste" id="pasteTa" placeholder="Paste Markdown here. Each # heading (or --- divider) starts a new note."></textarea>'+
'<div class="btns" style="margin-top:10px"><button class="btn" id="pCancel">Cancel</button><button class="btn primary" id="pOk">Import</button></div></div>';
ov.style.display="flex";$("pasteTa").focus();
$("pCancel").onclick=function(){ov.style.display="none";body.innerHTML="";};
$("pOk").onclick=async function(){var notes=parsePaste($("pasteTa").value);ov.style.display="none";body.innerHTML="";
if(!notes.length){toast("Nothing to import");return;}
for(var i=0;i<notes.length;i++)await putNote(newId(),uniqueTitle(notes[i].title||("Imported "+(i+1))),notes[i].body);
var n=allNotes();if(!cur&&n.length)cur=n[0].id;renderAll();$("sov").style.display="none";toast("Imported "+notes.length+" note"+(notes.length===1?"":"s"));};
};
$("clearVault").onclick=async function(){
var ok=await ask({kind:"confirm",title:"Clear vault",msg:"Permanently delete all "+allNotes().length+" notes? This cannot be undone.",okText:"Delete everything",warn:true});
if(!ok)return;var oldS=Object.keys(shards);for(var s in shards)delete shards[s];shards={s0:{}};index.notes={};recalcBytes();
await persistIndex();await persistShard("s0");for(var oi=0;oi<oldS.length;oi++){if(oldS[oi]!=="s0")await delRaw("brain-"+oldS[oi]);}cur=null;renderAll();$("sov").style.display="none";toast("Vault cleared");};
els.title.addEventListener("input",function(){if(cur){index.notes[cur].t=els.title.value;renderList();scheduleSave();}});
els.editor.addEventListener("input",function(){els.render.innerHTML=mdRender(els.editor.value);acState();scheduleSave();});
els.editor.addEventListener("keyup",acState);els.editor.addEventListener("click",acState);
els.editor.addEventListener("blur",function(){setTimeout(function(){els.ac.style.display="none";flushSave();},150);});
els.search.addEventListener("input",renderList);
$("qinput").addEventListener("input",function(){qFilter(this.value);});
$("qov").addEventListener("click",function(e){if(e.target.id==="qov")closeQuick();});
$("eov").addEventListener("click",function(e){if(e.target.id==="eov")e.target.style.display="none";});
$("sov").addEventListener("click",function(e){if(e.target.id==="sov")e.target.style.display="none";});
document.addEventListener("keydown",function(e){
if((e.metaKey||e.ctrlKey)&&e.key==="k"){e.preventDefault();openQuick();return;}
if($("qov").style.display==="flex"){
if(e.key==="Escape")closeQuick();
if(e.key==="ArrowDown"||e.key==="ArrowUp"){e.preventDefault();var qs=$("qres").querySelectorAll(".qr");if(!qs.length)return;
qs[qSel]&&qs[qSel].classList.remove("sel");qSel=(qSel+(e.key==="ArrowDown"?1:qs.length-1))%qs.length;qs[qSel].classList.add("sel");}
if(e.key==="Enter"){e.preventDefault();var sel=$("qres").querySelector(".qr.sel");if(sel){if(sel.getAttribute("data-id"))openNote(sel.getAttribute("data-id"));else if(sel.getAttribute("data-new"))openTitle(sel.getAttribute("data-new"));closeQuick();}}
return;}
if(e.key==="Escape"){els.ac.style.display="none";}});
window.addEventListener("resize",function(){if(view==="graph")sizeCanvas();});
function closeMobilePanels(){$("side").classList.remove("open");$("backPanel").classList.remove("mopen");}
async function seed(){var a=newId(),b=newId(),c=newId(),d=newId();
await putNote(a,"Start Here",
"Welcome to your **Brain** -- a free, always-online knowledge vault that lives on It Just Vibes.\n\n"+
"It works like Obsidian, using the open **Markdown + [[wikilink]]** format, so your notes stay portable.\n\n"+
"## Try it\n- Link notes by typing `[[` then a name -- e.g. [[AI Context]]\n- Organize with #tags like #howto\n- Hit **⌘K** to jump between notes\n- Open the **⌗ Graph** to see how ideas connect\n- Click **⚙** to import an Obsidian vault, export, or turn on encryption\n\nNext: read [[How linking works]], [[AI Context]], and [[Privacy & portability]].");
await putNote(b,"How linking works",
"Type two square brackets `[[` and a note name to create a link. #howto\n\n"+
"If the note doesn't exist yet, clicking the link creates it. Backlinks show on the right automatically -- open [[Start Here]] to see this note under *Linked mentions*.");
await putNote(c,"AI Context",
"This vault is built to be an **AI context layer** for your app development. #ai #workflow\n\n"+
"## How to use it\n1. Keep notes on your stack, decisions, schemas, and gotchas here.\n2. Click **⤴ Context → Copy whole vault** (or *current + linked*).\n3. Paste that block into Claude/ChatGPT as grounding context.\n\nLink related notes with [[wikilinks]] so a focused export pulls in the right neighbors. See [[Project Notes]].");
await putNote(d,"Privacy & portability",
"Two things keep this safe and yours. #privacy\n\n"+
"**Portable:** Use **⚙ → Export → Vault as .zip** to get one `.md` per note -- open it directly in Obsidian. Import a folder of `.md` files the same way.\n\n"+
"**Private:** Turn on **⚙ → Encryption** to encrypt every note in your browser with a passphrase. The platform then stores only ciphertext and can't read your content.");
await putNote(newId(),"Project Notes",
"A scratchpad for the app you're building. #project\n\n- [ ] Define the data model\n- [ ] List external APIs\n- [x] Set up the context vault\n\n> Tip: one note per concept. Link liberally with [[wikilinks]].");
cur=a;}
async function unlockFlow(meta){
if(!SUB){toast("Can't decrypt here -- needs HTTPS context");$("lockedScreen").style.display="flex";return false;}
while(true){
var p=await ask({kind:"pass",title:"Unlock vault",msg:"Enter your passphrase to decrypt your notes.",okText:"Unlock"});
if(p===null){$("lockedScreen").style.display="flex";return false;}
try{CK=await deriveKey(p,meta.salt);var chk=await decStr(meta.check);if(chk==="brain-verify"){ENC=true;$("lockedScreen").style.display="none";return true;}}
catch(e){}
toast("Wrong passphrase");
}
}
$("unlockBtn").onclick=async function(){var meta=await readEncMeta();if(await unlockFlow(meta)){await finishBoot();}};
async function finishBoot(){
try{await bootStore();}catch(e){toast("Storage error: "+(e.message||e));index={v:1,notes:{},settings:{}};shards={s0:{}};await seed();}
if(!cur){var n=allNotes();if(n.length)cur=n.sort(function(a,b){return b.updated-a.updated;})[0].id;}
setView("edit");renderAll();updateLockBadge();
}
function start(){(async function(){
var meta=await readEncMeta();
if(meta.on){var ok=await unlockFlow(meta);if(!ok)return;}
await finishBoot();
})();}
if(typeof window!=="undefined"&&window.vibes&&typeof window.vibes.onReady==="function"){
HAS=true;var started=false,go=function(){if(started)return;started=true;start();};
try{window.vibes.onReady(go);}catch(e){go();}
setTimeout(go,2000);
}else{
if(!HAS)els.banner.style.display="flex";
start();
}
})();
</script>
</body>
</html>
```
[REQUESTED CHANGES]
(no specific request — apply your best judgment)
--- HOW TO RESPOND (READ FIRST) ---
Before writing any code, follow this exact process:
1. **ANALYZE** the widget source code provided above and identify:
a. Which Vibes SDK features it already uses (vibes.save, vibes.load, vibes.shared.join, etc.)
b. Which SDK features would genuinely benefit THIS specific widget — tailored to what it does, not a dump of everything available.
2. **PRESENT A NUMBERED LIST** covering:
- SDK features currently active in this widget
- New SDK features that would concretely improve this widget (be specific: why this widget, what it enables)
3. **WAIT** — do not write any code yet. Reply with your analysis and numbered list, then stop and ask the user which numbered items they want.
4. **IMPLEMENT ONLY** the items the user confirms, plus any explicit change they requested. Do not add unrequested features.
**IMPORTANT — Shared state room names:**
If you add vibes.shared.join(), do NOT use a hardcoded string literal as the room name (e.g. vibes.shared.join("lobby")) unless the user explicitly wants ALL viewers to share one single global state. A hardcoded room name means every person who visits this widget reads and writes the same shared state — it is a global room. For per-user or per-session isolation, derive the room name from a variable (e.g. a user ID, session token, or random value). When in doubt, use vibes.save/vibes.load for per-user persistence instead.
--- VIBES SDK CONTEXT ---
## Vibes SDK Reference
You are building an HTML widget for It Just Vibes (itjustvibes.com). The Vibes SDK is auto-injected — do NOT add a script tag. Just use `window.vibes` (or just `vibes`).
### Setup
Wrap your startup code in `vibes.onReady`:
```js
vibes.onReady(async () => {
const saved = await vibes.load("myKey");
// your widget logic here
});
```
### State (Per-User Persistence)
Every user gets their own isolated state per widget. All methods return Promises.
| Method | Description |
|--------|-------------|
| `await vibes.save(key, value)` | Save JSON-serializable data |
| `await vibes.load(key)` | Load saved data (returns `null` if not found) |
| `await vibes.delete(key)` | Delete a saved key |
| `await vibes.listKeys()` | Get array of all saved key names |
**Key rules:**
- Keys are strings, max 64 characters, alphanumeric + dashes/underscores
- Values must be JSON-serializable (objects, arrays, strings, numbers, booleans)
- Max 100KB per value, 500KB per widget per user, 5MB per user total
- Max 100 keys per widget per user
### Fetch Proxy
Use direct `fetch()` for APIs with permissive CORS headers (`Access-Control-Allow-Origin: *`). Use `vibes.fetch` for APIs without CORS headers (the proxy handles cross-origin requests):
```js
const resp = await vibes.fetch("https://api.example.com/data", {
method: "GET", // GET, POST, PUT, DELETE
headers: {}, // optional headers
body: null, // optional body (string)
timeout: null // optional timeout in ms
});
const data = await resp.json(); // or resp.text()
console.log(resp.status, resp.ok);
```
### Multiplayer (Shared State)
Real-time shared state across all users viewing the same widget.
```js
// Join a room (call once at startup)
await vibes.shared.join("lobby", { persistent: true });
// Set shared state (broadcasts to all users)
await vibes.shared.set("score", { player1: 10, player2: 7 });
// Read shared state (synchronous, returns last known value)
const score = vibes.shared.get("score");
// Listen for changes to a specific key
vibes.shared.onChange("score", (newValue) => {
console.log("Score updated:", newValue);
});
// Listen for any shared state change
vibes.shared.onAny((key, value) => {
console.log(key, "changed to", value);
});
// Get number of connected users
const count = await vibes.shared.getUserCount();
// Leave room
vibes.shared.leave();
// Clear all shared state for this room
await vibes.shared.clear();
```
**Shared state options:**
- `{ persistent: true }` — state survives page reloads (stored server-side)
- Default room name is "__default__" if omitted
### Rules
1. **Do NOT use localStorage, sessionStorage, or window.storage** — they are blocked or undefined in the sandbox. Use `vibes.save`/`vibes.load` instead.
2. **Do NOT add a script tag to import the SDK** — it is auto-injected.
3. **Wrap startup code in `vibes.onReady()`** — the SDK may not be ready immediately.
4. **Await all SDK calls** — every method (except `vibes.shared.get`) returns a Promise.
5. **Do NOT import external JS libraries via script tags** — they will be blocked. Include library code inline or use a CDN link in a `<link>` tag for CSS only.
6. **Keep total code under 80KB** — that is the default widget size limit (your account limit may be higher).
### Rate Limits
| Operation | Limit |
|-----------|-------|
| Writes (save + delete combined) | 30/min |
| Reads (load) | 60/min |
| List keys | 30/min |
| Fetch proxy | Rate limited per widget |
### Error Handling
All SDK methods can reject. Wrap in try/catch:
```js
try {
await vibes.save("key", value);
} catch (err) {
console.error("Save failed:", err.message);
}
```
Fetch proxy errors include `err.code`: `"RATE_LIMITED"`, `"BLOCKED"`, `"FETCH_ERROR"`.
### Data Export
Export rows of data as CSV or Excel directly from your widget.
```js
// Register dataset for the platform's "Export data" button
// AND optionally trigger an immediate download
vibes.exportData(rows, { filename: 'results.csv' })
// rows: Array of arrays (each inner array is one row; first row = headers)
// options.filename: sets the download filename and format (csv or xlsx)
// options.directDownload: false to only register without downloading (default: true)
```
| Option | Type | Default | Description |
|--------|------|---------|-------------|
| `filename` | string | `'data.csv'` | Download filename; extension determines format (`.csv` or `.xlsx`) |
| `directDownload` | boolean | `true` | Trigger immediate browser download in addition to registering the dataset |
**Notes:**
- First call registers the dataset so the widget chrome shows an "Export data" button
- Use `.csv` extension for CSV, `.xlsx` for Excel
- CSV injection safety is automatic (formula-starting cells prefixed with `'`)
- Data stays in the browser — no server round-trip
--- FORK & RESUBMIT INSTRUCTION ---
Return the complete, self-contained HTML file with all changes applied.
It should be ready to paste into itjustvibes.com/submit to create a new fork.
Include this comment at the top of the output:
/* Forked from: https://itjustvibes.com/GigaChad/brain-ai-context-vault */