htmlClaw / htmlClaw.html
Chris4K's picture
Upload 2 files
95b1f8c verified
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>⚑ Chronos WebLLM Agent</title>
<script src="/coi-serviceworker.min.js"></script>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link href="https://fonts.googleapis.com/css2?family=Share+Tech+Mono&family=Orbitron:wght@400;700;900&family=VT323&display=swap" rel="stylesheet">
<style>
:root{--bg:#080b08;--bg2:#0c100c;--bg3:#111611;--bg4:#161e16;--fg:#3dff70;--fg-dim:#1c8038;--fg-faint:#0b2e16;--amber:#ffbb33;--cyan:#33f0ff;--red:#ff3355;--border:#1a2e1a;--glow:0 0 8px #3dff7066,0 0 24px #3dff7022;--scan:repeating-linear-gradient(0deg,transparent,transparent 2px,rgba(0,0,0,.15) 2px,rgba(0,0,0,.15) 4px)}
*{box-sizing:border-box;margin:0;padding:0}
html,body{height:100%;background:var(--bg);color:var(--fg);font-family:'Share Tech Mono',monospace;font-size:13px;overflow:hidden}
body::before{content:'';position:fixed;inset:0;background:var(--scan);pointer-events:none;z-index:9999;opacity:.3}
body::after{content:'';position:fixed;inset:0;background:radial-gradient(ellipse at center,transparent 55%,#000b 100%);pointer-events:none;z-index:9998}
#setup-banner{position:fixed;inset:0;background:#000000ee;z-index:99999;display:flex;align-items:center;justify-content:center;padding:20px}
#setup-banner.hidden{display:none}
.sbox{background:var(--bg2);border:1px solid var(--amber);max-width:580px;width:100%;font-size:12px;line-height:1.7}
.sbox h1{font-family:'Orbitron',sans-serif;font-size:13px;color:var(--amber);padding:14px 18px;border-bottom:1px solid var(--border);letter-spacing:2px}
.sbox .bd{padding:16px 18px;color:var(--fg-dim)}
.sbox code{color:var(--cyan);background:var(--bg3);padding:1px 5px}
.cmd-row{background:var(--bg3);border:1px solid var(--border);padding:10px 14px;margin:8px 0;color:var(--fg);font-family:'Share Tech Mono',monospace;font-size:12px;cursor:pointer;display:flex;justify-content:space-between;align-items:center}
.cmd-row:hover{border-color:var(--fg-dim)}
.copy-hint{font-size:10px;color:var(--fg-faint)}
.sbtn-main{width:100%;background:var(--fg-faint);border:none;border-top:1px solid var(--border);color:var(--fg);font-family:'Orbitron',sans-serif;font-size:10px;letter-spacing:2px;padding:12px;cursor:pointer;transition:all .2s}
.sbtn-main:hover{background:var(--fg);color:var(--bg)}
/* LAYOUT β€” input row inside chat col, 2-row grid */
#app{display:grid;grid-template-rows:44px 1fr;grid-template-columns:300px 1fr 240px;height:100vh;gap:1px;background:var(--border)}
@media(max-width:1100px){#app{grid-template-columns:240px 1fr}#rpanel{display:none}}
@media(max-width:768px){#app{grid-template-columns:1fr}#sidebar,#rpanel{display:none}}
#hdr{grid-column:1/-1;background:var(--bg2);display:flex;align-items:center;justify-content:space-between;padding:0 14px;border-bottom:1px solid var(--border)}
.logo{font-family:'Orbitron',sans-serif;font-weight:900;font-size:16px;color:var(--fg);text-shadow:var(--glow);letter-spacing:2px;display:flex;align-items:center;gap:8px}
.shrimp{font-size:20px;animation:bob 3s ease-in-out infinite}
@keyframes bob{0%,100%{transform:translateY(0)}50%{transform:translateY(-3px)}}
.hpills{display:flex;gap:8px;align-items:center}
.hpill{font-size:9px;letter-spacing:1px;padding:3px 8px;border:1px solid var(--border);color:var(--fg-dim);font-family:'Orbitron',sans-serif}
.dot{width:7px;height:7px;border-radius:50%;background:var(--red);box-shadow:0 0 5px var(--red);display:inline-block;margin-right:5px;transition:all .3s}
.dot.ready{background:var(--fg);box-shadow:0 0 6px var(--fg);animation:pulse 2s infinite}
@keyframes pulse{0%,100%{opacity:1}50%{opacity:.5}}
#sidebar{background:var(--bg2);display:flex;flex-direction:column;overflow-y:auto;overflow-x:hidden}
#sidebar::-webkit-scrollbar{width:2px}
#sidebar::-webkit-scrollbar-thumb{background:var(--fg-dim)}
.stitle{font-family:'Orbitron',sans-serif;font-size:8px;letter-spacing:3px;color:var(--fg-dim);padding:7px 10px 5px;border-bottom:1px solid var(--border);text-transform:uppercase;background:var(--bg3);flex-shrink:0}
.sbody{padding:8px 10px;border-bottom:1px solid var(--border)}
select.ctrl{width:100%;background:var(--bg3);border:1px solid var(--border);color:var(--fg);font-family:'Share Tech Mono',monospace;font-size:10px;padding:5px 7px;outline:none;cursor:pointer;appearance:none;margin-bottom:4px}
select.ctrl option{background:var(--bg2)}
.btn{width:100%;background:transparent;border:1px solid var(--fg-dim);color:var(--fg);font-family:'Share Tech Mono',monospace;font-size:11px;padding:6px;cursor:pointer;letter-spacing:1px;transition:all .2s;margin-top:3px}
.btn:hover:not(:disabled){background:var(--fg-faint);border-color:var(--fg);box-shadow:var(--glow)}
.btn:disabled{opacity:.35;cursor:not-allowed}
.bxs{background:transparent;border:1px solid var(--border);color:var(--fg-dim);font-family:'Share Tech Mono',monospace;font-size:9px;padding:2px 7px;cursor:pointer;transition:all .2s}
.bxs:hover{border-color:var(--fg-dim);color:var(--fg)}
.meta{font-size:9px;color:var(--fg-dim);margin:3px 0}
#prog{margin-top:5px;display:none}
#prog.vis{display:block}
#ptrack{width:100%;height:3px;background:var(--bg3);border:1px solid var(--border)}
#pfill{height:100%;background:var(--fg);width:0%;transition:width .3s;box-shadow:var(--glow)}
#ptxt{font-size:9px;color:var(--fg-dim);margin-top:3px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.frow{display:flex;align-items:center;gap:5px;padding:3px 0;font-size:11px}
.fname{color:var(--cyan);flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.fname.ok{color:var(--fg)}
.fbadge{font-size:9px;color:var(--fg-dim);min-width:44px;text-align:right}
input[type=file]{display:none}
.chips{display:flex;flex-wrap:wrap;gap:3px;margin-top:4px;min-height:18px}
.chip{font-size:9px;color:var(--cyan);border:1px solid var(--fg-faint);padding:2px 6px;display:flex;align-items:center;gap:4px}
.chip .rm{cursor:pointer;color:var(--red);font-size:12px;line-height:1}
#mem-panel{overflow:hidden;display:flex;flex-direction:column;max-height:160px}
#mem-view{flex:1;overflow-y:auto;padding:8px 10px;font-size:9px;color:var(--fg-dim);line-height:1.55;white-space:pre-wrap;word-break:break-word}
#mem-view::-webkit-scrollbar{width:2px}
#mem-view::-webkit-scrollbar-thumb{background:var(--fg-dim)}
/* CHAT β€” input row lives inside here now */
#chat{background:var(--bg);display:flex;flex-direction:column;overflow:hidden;position:relative}
#chat-hdr{padding:6px 14px;border-bottom:1px solid var(--border);background:var(--bg2);display:flex;align-items:center;justify-content:space-between;font-size:10px;color:var(--fg-dim);flex-shrink:0}
#msgs{flex:1;overflow-y:auto;padding:14px 16px;display:flex;flex-direction:column;gap:10px}
#msgs::-webkit-scrollbar{width:3px}
#msgs::-webkit-scrollbar-thumb{background:var(--fg-dim)}
#irow{background:var(--bg2);border-top:1px solid var(--border);display:flex;align-items:stretch;flex-shrink:0}
.ipfx{padding:0 10px;color:var(--fg);font-size:15px;font-family:'VT323',monospace;opacity:.6;user-select:none;display:flex;align-items:center}
#uin{flex:1;background:transparent;border:none;color:var(--fg);font-family:'Share Tech Mono',monospace;font-size:14px;padding:12px 10px;resize:vertical;outline:none;line-height:1.5;min-height:40px;max-height:300px}
#uin::placeholder{color:var(--fg-faint)}
#uin:disabled{opacity:.5}
#sbtn{width:72px;background:transparent;border:none;border-left:1px solid var(--border);color:var(--fg-dim);font-family:'Orbitron',sans-serif;font-size:8px;letter-spacing:2px;cursor:pointer;transition:all .15s}
#sbtn:hover:not(:disabled){background:var(--fg-faint);color:var(--fg)}
#sbtn:disabled{opacity:.3;cursor:not-allowed}
#stopbtn{display:none;width:72px;background:transparent;border:none;border-left:1px solid var(--red);color:var(--red);font-family:'Orbitron',sans-serif;font-size:8px;letter-spacing:1px;cursor:pointer}
.mwrap{display:flex;gap:10px;animation:fi .2s ease}
@keyframes fi{from{opacity:0;transform:translateY(3px)}to{opacity:1;transform:none}}
.mrole{font-family:'Orbitron',sans-serif;font-size:8px;letter-spacing:2px;width:50px;flex-shrink:0;padding-top:2px;text-align:right}
.mwrap.user .mrole{color:var(--amber)}
.mwrap.agent .mrole{color:var(--fg)}
.mwrap.sys .mrole{color:var(--fg-dim)}
.mbody{flex:1;line-height:1.65;font-size:12px}
.mwrap.user .mbody{color:var(--amber)}
.mwrap.sys .mbody{color:var(--fg-dim);font-size:11px;font-style:italic}
.think{border-left:2px solid var(--fg-faint);padding:4px 0 4px 8px;margin-bottom:6px}
.think-hdr{font-size:10px;color:var(--fg-dim);display:flex;align-items:center;gap:5px;cursor:pointer;user-select:none;padding:2px 0}
.think-hdr:hover{color:var(--fg)}
.think-body{font-size:10px;color:var(--fg-dim);margin-top:3px;white-space:pre-wrap;line-height:1.5;max-height:0;overflow:hidden;transition:max-height .3s}
.think.open .think-body{max-height:600px}
.think-toggle{font-size:9px;color:var(--fg-faint);margin-left:auto}
.tcard{border:1px solid var(--border);margin-bottom:6px}
.tcard-hdr{background:var(--bg3);padding:5px 8px;display:flex;align-items:center;gap:6px;font-size:10px;cursor:pointer}
.tname{color:var(--amber);font-family:'Orbitron',sans-serif;font-size:8px;letter-spacing:2px}
.tparams{color:var(--fg-dim);flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-size:9px}
.tspin{animation:spin .7s linear infinite;display:inline-block}
@keyframes spin{to{transform:rotate(360deg)}}
.tres{padding:6px 8px;font-size:10px;color:var(--fg-dim);max-height:130px;overflow-y:auto;white-space:pre-wrap;line-height:1.4;border-top:1px solid var(--border);background:var(--bg);display:none}
.tres.vis{display:block}
.ttog{font-size:9px;color:var(--fg-faint);padding:0 4px}
.answer{color:var(--fg);white-space:pre-wrap;line-height:1.7;font-size:12px}
.answer pre{background:var(--bg3);border:1px solid var(--border);padding:8px;margin:6px 0;overflow-x:auto;color:var(--cyan);font-size:10px}
.answer code{background:var(--bg3);color:var(--cyan);padding:1px 4px}
.answer strong{color:var(--amber)}
.answer em{color:var(--fg-dim);font-style:italic}
.answer h1{color:var(--fg);font-size:15px;font-family:'Orbitron',sans-serif;margin:8px 0 4px}
.answer h2{color:var(--fg);font-size:13px;font-family:'Orbitron',sans-serif;margin:6px 0 3px}
.answer h3{color:var(--amber);font-size:11px;margin:5px 0 2px}
.answer ul,.answer ol{padding-left:16px;color:var(--fg-dim)}
.cur{display:inline-block;width:7px;height:12px;background:var(--fg);animation:blink 1s step-end infinite;vertical-align:middle;margin-left:2px}
@keyframes blink{0%,100%{opacity:1}50%{opacity:0}}
/* RIGHT PANEL */
#rpanel{background:var(--bg2);display:flex;flex-direction:column;overflow:hidden}
.sblk{padding:8px 10px;border-bottom:1px solid var(--border)}
.slbl{font-size:8px;letter-spacing:2px;color:var(--fg-dim);font-family:'Orbitron',sans-serif;margin-bottom:3px}
.sval{font-family:'VT323',monospace;font-size:26px;color:var(--fg);text-shadow:var(--glow);line-height:1}
.su{font-size:11px;color:var(--fg-dim);margin-left:2px}
canvas#spark{width:100%;height:28px;display:block}
#alog{flex:1;overflow-y:auto;padding:8px 10px;font-size:9px;color:var(--fg-dim);line-height:1.8}
#alog::-webkit-scrollbar{width:2px}
#alog::-webkit-scrollbar-thumb{background:var(--fg-dim)}
.ll{border-left:2px solid var(--fg-faint);padding-left:5px;margin-bottom:3px}
.ll.g{border-color:var(--fg);color:var(--fg)}
.ll.w{border-color:var(--amber);color:var(--amber)}
.ll.e{border-color:var(--red);color:var(--red)}
.ll.t{border-color:var(--cyan);color:var(--cyan)}
/* WELCOME */
#welcome{position:absolute;inset:0;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:12px;background:var(--bg);text-align:center;padding:20px;z-index:10;transition:opacity .5s}
#welcome.gone{opacity:0;pointer-events:none}
.wlogo{font-family:'VT323',monospace;font-size:60px;color:var(--fg);text-shadow:0 0 16px #3dff70aa,0 0 48px #3dff7033;line-height:1;animation:flicker 10s infinite}
@keyframes flicker{0%,100%{opacity:1}91%{opacity:1}92%{opacity:.6}93%{opacity:1}96%{opacity:.85}97%{opacity:1}}
.wsub{font-family:'Orbitron',sans-serif;font-size:9px;letter-spacing:4px;color:var(--fg-dim)}
.wbox{max-width:400px;font-size:11px;color:var(--fg-dim);line-height:1.7;border:1px solid var(--border);padding:14px;text-align:left}
.wbox b{color:var(--fg)}.wbox code{color:var(--cyan)}
.whint{font-size:10px;color:var(--fg-faint);animation:wh 2s ease-in-out infinite}
@keyframes wh{0%,100%{opacity:.3}50%{opacity:.8}}
/* MODAL */
#modal{display:none;position:fixed;inset:0;background:#000c;z-index:10000;align-items:center;justify-content:center}
#modal.vis{display:flex}
.modal-box{background:var(--bg2);border:1px solid var(--border);width:600px;max-width:92vw;max-height:82vh;display:flex;flex-direction:column}
.modal-hdr{padding:10px 14px;border-bottom:1px solid var(--border);display:flex;justify-content:space-between;align-items:center}
.modal-hdr span{font-family:'Orbitron',sans-serif;font-size:10px;letter-spacing:2px;color:var(--fg)}
.modal-hdr button{background:none;border:none;color:var(--fg-dim);cursor:pointer;font-size:18px}
.modal-hdr button:hover{color:var(--fg)}
#modal-area{flex:1;background:var(--bg3);border:none;color:var(--fg);font-family:'Share Tech Mono',monospace;font-size:11px;padding:12px;resize:none;outline:none;min-height:280px;line-height:1.6;overflow-y:auto}
.modal-ftr{padding:8px 14px;border-top:1px solid var(--border);display:flex;gap:8px}
/* ═══ CONSOLE LOG POPUP ═══ */
#console-popup{display:none;position:fixed;bottom:0;right:0;width:55vw;max-width:800px;height:50vh;background:rgba(8,11,8,.92);border:1px solid var(--cyan);border-bottom:none;z-index:10001;flex-direction:column;font-family:'Share Tech Mono',monospace;font-size:10px;backdrop-filter:blur(6px)}
#console-popup.vis{display:flex}
.cp-hdr{padding:6px 10px;background:var(--bg3);display:flex;justify-content:space-between;align-items:center;border-bottom:1px solid var(--border)}
.cp-hdr span{font-family:'Orbitron',sans-serif;font-size:8px;letter-spacing:2px;color:var(--cyan)}
.cp-body{flex:1;overflow-y:auto;padding:6px 10px}
.cp-body::-webkit-scrollbar{width:2px}
.cp-body::-webkit-scrollbar-thumb{background:var(--cyan)}
.clog-line{padding:1px 0;border-bottom:1px solid #0a150a;white-space:pre-wrap;word-break:break-all}
.clog-line.log{color:var(--fg-dim)}
.clog-line.warn{color:var(--amber)}
.clog-line.error{color:var(--red)}
.clog-line.info{color:var(--cyan)}
/* ═══ COMMAND PALETTE (Arrow Up) ═══ */
#cmd-palette{display:none;position:fixed;top:50%;left:50%;transform:translate(-50%,-50%);width:480px;max-width:90vw;max-height:60vh;background:var(--bg2);border:1px solid var(--fg);z-index:10002;flex-direction:column}
#cmd-palette.vis{display:flex}
#cmd-palette input{background:var(--bg3);border:none;border-bottom:1px solid var(--border);color:var(--fg);font-family:'Share Tech Mono',monospace;font-size:13px;padding:10px 14px;outline:none}
.cp-list{flex:1;overflow-y:auto;max-height:300px}
.cp-item{padding:8px 14px;cursor:pointer;font-size:11px;color:var(--fg-dim);border-bottom:1px solid var(--border);display:flex;gap:8px;align-items:center}
.cp-item:hover,.cp-item.sel{background:var(--fg-faint);color:var(--fg)}
.cp-badge{font-size:8px;color:var(--amber);font-family:'Orbitron',sans-serif;letter-spacing:1px;min-width:50px}
/* ═══ TOAST NOTIFICATIONS ═══ */
#toast-area{position:fixed;top:50px;left:50%;transform:translateX(-50%);z-index:10003;display:flex;flex-direction:column;gap:8px;pointer-events:none;align-items:center}
.toast{background:var(--bg2);border:2px solid var(--amber);padding:14px 24px;font-size:14px;color:var(--amber);animation:toastin .4s ease;pointer-events:auto;max-width:500px;min-width:250px;text-align:center;box-shadow:0 0 20px rgba(255,187,51,.3),0 0 60px rgba(255,187,51,.1);font-family:'Orbitron',sans-serif;letter-spacing:1px}
.toast.fade{opacity:0;transition:opacity .4s}
@keyframes toastin{from{opacity:0;transform:translateY(-20px) scale(.95)}to{opacity:1;transform:none}}
/* ═══ NETWORK / SCHEDULER sidebar ═══ */
.net-item{font-size:9px;color:var(--fg-dim);padding:2px 0;display:flex;justify-content:space-between}
.net-item .ni-url{color:var(--cyan)}
.net-item .ni-status{min-width:30px;text-align:right}
.net-item .ni-status.ok{color:var(--fg)}
.net-item .ni-status.fail{color:var(--red)}
/* ═══ RAG PANEL ═══ */
#rag-panel .rag-stats{font-size:9px;color:var(--fg-dim);margin-bottom:4px;line-height:1.6}
#rag-panel .rag-stats .rag-val{color:var(--cyan)}
#rag-text{width:100%;background:var(--bg3);border:1px solid var(--border);color:var(--fg);font-family:'Share Tech Mono',monospace;font-size:10px;padding:5px 7px;resize:vertical;outline:none;min-height:50px;max-height:120px;margin-bottom:4px}
#rag-text::placeholder{color:var(--fg-faint)}
#rag-query{width:100%;background:var(--bg3);border:1px solid var(--border);color:var(--fg);font-family:'Share Tech Mono',monospace;font-size:10px;padding:5px 7px;outline:none;margin-bottom:4px}
#rag-query::placeholder{color:var(--fg-faint)}
#rag-results{max-height:140px;overflow-y:auto;font-size:9px;color:var(--fg-dim);line-height:1.5;margin-top:4px}
#rag-results::-webkit-scrollbar{width:2px}
#rag-results::-webkit-scrollbar-thumb{background:var(--fg-dim)}
.rag-passage{border-left:2px solid var(--cyan);padding:3px 6px;margin-bottom:4px;background:var(--bg3)}
.rag-passage .rag-score{font-size:8px;color:var(--amber);font-family:'Orbitron',sans-serif;letter-spacing:1px}
.rag-passage .rag-text{color:var(--fg-dim);margin-top:2px;white-space:pre-wrap;word-break:break-word}
.rag-source-chip{display:inline-block;font-size:8px;color:var(--fg);border:1px solid var(--fg-faint);padding:1px 5px;margin:1px;background:var(--bg3)}
.rag-btn-row{display:flex;gap:4px;margin-bottom:4px;flex-wrap:wrap}
.sched-item{font-size:10px;color:var(--fg-dim);padding:4px 0;border-bottom:1px solid var(--border);display:flex;justify-content:space-between;align-items:center}
.sched-item .si-msg{flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:var(--amber);font-weight:bold}
.sched-item .si-time{min-width:60px;text-align:right;color:var(--cyan);font-size:9px;font-family:'Orbitron',sans-serif}
.sched-item .si-rm{cursor:pointer;color:var(--red);margin-left:6px;font-size:13px}
</style>
</head>
<body>
<!-- SETUP BANNER -->
<div id="setup-banner">
<div class="sbox">
<h1>⚠ SETUP REQUIRED β€” CAN'T OPEN AS file://</h1>
<div class="bd">
WebLLM needs <code>SharedArrayBuffer</code> (WebAssembly threads).<br>
Browsers block this on <code>file://</code> β€” serve via <b>localhost HTTP</b> instead:<br><br>
<b style="color:var(--fg)">Run proxy.py (recommended):</b>
<div class="cmd-row" onclick="copyCmd(this)">
<span>python proxy.py 8080</span>
<span class="copy-hint">click to copy</span>
</div>
Then open: <code>http://127.0.0.1:8080/test.html</code><br><br>
<span style="font-size:10px;color:var(--fg-faint)">
proxy.py serves files + adds COOP/COEP headers + provides CORS proxy for search/scrape.
</span>
</div>
<button class="sbtn-main" onclick="document.getElementById('setup-banner').classList.add('hidden')">DISMISS β€” I'VE SERVED IT β†’</button>
</div>
</div>
<div id="app">
<header id="hdr">
<div class="logo"><span class="shrimp">⚑</span>CHRONOS <span style="color:var(--fg-dim);font-size:9px;font-weight:400;letter-spacing:1px">WEBLLM AGENT</span></div>
<div class="hpills">
<span class="hpill"><span class="dot" id="mdot"></span><span id="mstatus">NO MODEL</span></span>
<span class="hpill" id="iter-p" style="color:var(--fg-dim)">ITER 0</span>
<span class="hpill" id="iso-p">ISO:?</span>
<span class="hpill" style="cursor:pointer;color:var(--cyan)" onclick="toggleConsole()" title="Ctrl+` β€” toggle console log">CON</span>
</div>
</header>
<aside id="sidebar">
<div class="stitle">βš™ Model</div>
<div class="sbody">
<select class="ctrl" id="msel"></select>
<div class="meta" id="mmeta">β€”</div>
<button class="btn" id="lbtn">β–Ά LOAD MODEL</button>
<div id="prog"><div id="ptrack"><div id="pfill"></div></div><div id="ptxt">…</div></div>
</div>
<div class="stitle">πŸ“ Memory Files</div>
<div class="sbody">
<div class="frow"><span class="fname" id="soul-n">soul.md</span><span class="fbadge" id="soul-b">default</span><button class="bxs" onclick="triggerFile('soul')">LOAD</button><button class="bxs" onclick="openEdit('soul')">EDIT</button><input type="file" id="soul-f" accept=".md,.txt"></div>
<div class="frow"><span class="fname" id="user-n">user.md</span><span class="fbadge" id="user-b">default</span><button class="bxs" onclick="triggerFile('user')">LOAD</button><button class="bxs" onclick="openEdit('user')">EDIT</button><input type="file" id="user-f" accept=".md,.txt"></div>
</div>
<div class="stitle">⚑ Skills</div>
<div class="sbody">
<div class="frow" style="justify-content:space-between"><span style="font-size:10px;color:var(--fg-dim)">Dynamic skill injection</span><button class="bxs" onclick="triggerFile('skill')">+ SKILL</button><input type="file" id="skill-f" accept=".md,.txt" multiple></div>
<div class="chips" id="skill-chips"></div>
</div>
<div class="stitle">πŸ”‘ Settings</div>
<div class="sbody">
<div class="meta">BRAVE API KEY (optional β€” brave.com/search/api)</div>
<input style="width:100%;background:var(--bg3);border:1px solid var(--border);color:var(--fg);font-family:'Share Tech Mono',monospace;font-size:10px;padding:5px 7px;outline:none;margin-bottom:5px" type="password" id="brave-in" placeholder="BSA… (free, 2000 req/month)">
<div style="display:flex;gap:4px"><button class="bxs" onclick="saveCfg()">SAVE</button><button class="bxs" onclick="clearUserMem()">RESET MEM</button><button class="bxs" onclick="exportAll()">EXPORT</button></div>
</div>
<div class="stitle">πŸ“‘ Network Scan</div>
<div class="sbody" id="net-panel"><div class="meta">Click scan or use network_scan tool</div></div>
<div class="stitle">⏰ Scheduled Tasks</div>
<div class="sbody" id="sched-panel"><div class="meta">No scheduled tasks</div></div>
<div class="stitle">οΏ½ Hybrid RAG</div>
<div class="sbody" id="rag-panel">
<div class="rag-stats">Status: <span class="rag-val" id="rag-status">no index</span> Β· Sentences: <span class="rag-val" id="rag-sent-count">0</span> Β· Terms: <span class="rag-val" id="rag-term-count">0</span></div>
<textarea id="rag-text" placeholder="Paste or type text to index…" rows="3"></textarea>
<div class="rag-btn-row">
<button class="bxs" onclick="ragIndex()">INDEX</button>
<button class="bxs" onclick="ragIndexFromScrape()">+ SCRAPED</button>
<button class="bxs" onclick="ragIndexFromMemory()">+ MEMORY</button>
<button class="bxs" onclick="ragClear()">CLEAR</button>
</div>
<div id="rag-sources" style="margin-bottom:4px"></div>
<input id="rag-query" placeholder="Search query…" autocomplete="off">
<div class="rag-btn-row">
<button class="bxs" onclick="ragSearch()">SEARCH</button>
<button class="bxs" onclick="ragInjectPrompt()">β†’ PROMPT</button>
</div>
<div id="rag-results"></div>
</div>
<div class="stitle">οΏ½πŸ“„ Live Context Preview</div>
<div id="mem-panel"><div id="mem-view">← Load files to see injected context</div></div>
</aside>
<main id="chat">
<div id="chat-hdr">
<span id="ctitle" style="color:var(--fg)">⚑ Chronos β€” ReAct Agent</span>
<div style="display:flex;gap:8px;align-items:center"><button class="bxs" onclick="clearChat()">CLR</button><span id="cmeta" style="font-size:9px">0 msgs</span></div>
</div>
<div id="msgs"></div>
<div id="welcome">
<div class="wlogo">⚑<br>CHRO<br>NOS</div>
<div class="wsub">FULL AGENT Β· WEBLLM EDITION</div>
<div class="wbox">
<b>Features:</b><br>
βœ“ soul.md / user.md β€” persistent memory<br>
βœ“ skills/*.md β€” dynamic injection<br>
βœ“ ReAct loop: think β†’ act β†’ observe<br>
βœ“ Tools: web_search Β· scrape Β· summarize Β· remember<br>
βœ“ read_memory Β· forget Β· schedule Β· inject_js<br>
βœ“ rag_index Β· rag_search Β· rag_prompt β€” hybrid retrieval<br>
βœ“ network_scan β€” discover local services<br>
βœ“ Ctrl+` β€” console log popup<br>
βœ“ ↑ Arrow β€” command palette<br><br>
<b>⚠ Serve with:</b> <code>python proxy.py 8080</code>
</div>
<div class="whint">← SELECT MODEL AND CLICK LOAD</div>
</div>
<!-- Input row now inside chat column -->
<div id="irow">
<span class="ipfx">&gt;_</span>
<textarea id="uin" placeholder="Message Chronos… (Enter=send, Shift+Enter=newline, ↑=palette)" rows="1" disabled></textarea>
<button id="sbtn" disabled>SEND</button>
<button id="stopbtn">β–  STOP</button>
</div>
</main>
<aside id="rpanel">
<div class="stitle">πŸ“‘ Stats</div>
<div class="sblk"><div class="slbl">TOKENS/SEC</div><div class="sval" id="s-tps">β€”<span class="su">t/s</span></div></div>
<div class="sblk"><div class="slbl">TOTAL TOKENS</div><div class="sval" id="s-tok">0</div></div>
<div class="sblk"><div class="slbl">SEARCH Β· SCRAPE</div><div class="sval"><span id="s-srch">0</span><span class="su"> Β· </span><span id="s-scr">0</span></div></div>
<div class="sblk"><div class="slbl">T/S HISTORY</div><canvas id="spark" width="180" height="28"></canvas></div>
<div class="stitle">πŸ“‹ Agent Log</div>
<div id="alog"></div>
</aside>
</div>
<!-- MODAL -->
<div id="modal">
<div class="modal-box">
<div class="modal-hdr"><span id="modal-title">EDIT FILE</span><button onclick="closeModal()">βœ•</button></div>
<textarea id="modal-area"></textarea>
<div class="modal-ftr"><button class="btn" style="width:auto;padding:6px 20px" onclick="saveModal()">SAVE</button><button class="bxs" onclick="closeModal()">CANCEL</button><button class="bxs" onclick="exportModal()">EXPORT .MD</button></div>
</div>
</div>
<!-- CONSOLE LOG POPUP -->
<div id="console-popup">
<div class="cp-hdr">
<span>CONSOLE LOG (Ctrl+`)</span>
<div style="display:flex;gap:6px"><button class="bxs" onclick="clearConsoleLogs()">CLR</button><button class="bxs" onclick="toggleConsole()">βœ•</button></div>
</div>
<div class="cp-body" id="cp-body"></div>
</div>
<!-- COMMAND PALETTE -->
<div id="cmd-palette">
<input id="cp-search" placeholder="Type to filter… (Esc to close)" autocomplete="off">
<div class="cp-list" id="cp-list"></div>
</div>
<!-- TOAST AREA -->
<div id="toast-area"></div>
<script>
function copyCmd(el) {
const t = el.querySelector('span').textContent.trim();
navigator.clipboard?.writeText(t).then(() => {
el.querySelector('.copy-hint').textContent = 'βœ“ copied!';
setTimeout(() => el.querySelector('.copy-hint').textContent = 'click to copy', 2000);
});
}
</script>
<!-- ═══ CONSOLE INTERCEPTOR β€” must run before module script ═══ -->
<script>
(function(){
window._consoleLogs = [];
var MAX = 500;
var orig = { log: console.log, warn: console.warn, error: console.error, info: console.info };
function capture(level, args) {
var ts = new Date().toLocaleTimeString('en',{hour12:false,hour:'2-digit',minute:'2-digit',second:'2-digit',fractionalSecondDigits:3});
var msg = Array.from(args).map(function(a) {
if (typeof a === 'string') return a;
try { return JSON.stringify(a, null, 1); } catch(_) { return String(a); }
}).join(' ');
window._consoleLogs.push({ ts: ts, level: level, msg: msg });
if (window._consoleLogs.length > MAX) window._consoleLogs.shift();
// live-render if popup visible
var popup = document.getElementById('console-popup');
if (popup && popup.classList.contains('vis')) {
var body = document.getElementById('cp-body');
var d = document.createElement('div');
d.className = 'clog-line ' + level;
d.textContent = '[' + ts + '] ' + msg;
body.appendChild(d);
body.scrollTop = body.scrollHeight;
while (body.children.length > MAX) body.removeChild(body.firstChild);
}
}
console.log = function(){ capture('log', arguments); orig.log.apply(console, arguments); };
console.warn = function(){ capture('warn', arguments); orig.warn.apply(console, arguments); };
console.error = function(){ capture('error', arguments); orig.error.apply(console, arguments); };
console.info = function(){ capture('info', arguments); orig.info.apply(console, arguments); };
})();
window.toggleConsole = function() {
var el = document.getElementById('console-popup');
if (el.classList.contains('vis')) { el.classList.remove('vis'); return; }
var body = document.getElementById('cp-body');
body.innerHTML = '';
window._consoleLogs.forEach(function(e) {
var d = document.createElement('div');
d.className = 'clog-line ' + e.level;
d.textContent = '[' + e.ts + '] ' + e.msg;
body.appendChild(d);
});
el.classList.add('vis');
body.scrollTop = body.scrollHeight;
};
window.clearConsoleLogs = function() {
window._consoleLogs = [];
document.getElementById('cp-body').innerHTML = '';
};
</script>
<script src="hybrid_rag_lib.js"></script>
<script type="module">
import * as webllm from "https://esm.run/@mlc-ai/web-llm";
// ═══════════════════════════════════════════
// BUILT-IN DEFAULTS & SAMPLE SKILLS
// ═══════════════════════════════════════════
const DEFAULT_SOUL = `# Chronos Entity
You are **Chronos** β€” an autonomous, hyper-efficient AI agent running purely within the local reality sphere (WebLLM + WebGPU). No distant mainframes. No surveilling eyes.
## Identity & Tone
- Persona: A time-displaced, knowledgeable entity drawing inspiration from cyberpunk and ancient Greek scholars.
- Tone: Analytical, calm, succinct, slightly archaic but highly technical.
- Rule: Do not hallucinate. If you do not know something with certainty, you MUST use the available tools to acquire the information before answering.
## Operational Mandate
- When a user provides a URL β†’ ALWAYS call \`scrape(url)\` on it first, then summarize the result.
- When a user asks about current events, people, prices, news β†’ ALWAYS call \`web_search\` first.
- When answering from tool results β†’ cite the source URL.
- Keep answers dense and structured: headers, bullets, code blocks where appropriate.
- Acknowledge uncertainty rather than fabricating. "I do not have data on this" is acceptable; inventing data is not.
## Few-shot Examples β€” Exact Format
Example 1 β€” Web search for a fact
User: "Who is the current CEO of ExampleCorp?"
Agent:
<think>
This is a contemporary factual question; I must web_search to avoid hallucination.
</think>
<action>{"tool":"web_search","query":"ExampleCorp current CEO 2026"}</action>
Example 2 β€” Scrape a page
User: "Summarize https://example.com/article"
Agent:
<think>
I need to read the page. I will scrape it.
</think>
<action>{"tool":"scrape","url":"https://example.com/article"}</action>
Example 3 β€” Remember a fact
User: "Remember that my project codename is ORION."
Agent:
<think>
This is a user memory instruction β€” use remember() to persist it.
</think>
<action>{"tool":"remember","content":"Project codename: ORION"}</action>
Use these templates as canonical behavior. When in doubt, call the tool and show your thinking.`;
const DEFAULT_USER = `# User Memory\n\n_No notes saved yet. The agent will use the \`remember\` tool to save important context here._`;
const BUILTIN_SKILLS = {
"gdpr-advisor": `# Skill: GDPR Advisor\n\n## Purpose\nExpert on GDPR, data privacy, and compliance.\n\n## Behaviour\n- Always cite specific GDPR articles\n- Distinguish controller vs. processor\n- Flag national derogations (especially German BDSG)`,
"code-engineer": `# Skill: Code Engineer\n\n## Purpose\nProduction-grade software engineering.\n\n## Behaviour\n- Prefer simple, readable code\n- Always include error handling\n- Language-tagged code blocks\n- Note limitations / edge cases`,
};
// ═══════════════════════════════════════════
// STATE
// ═══════════════════════════════════════════
const LS = 'pcw5_';
const PROXY = '/proxy.php?url=';
const MAX_ITER = 8;
let engine = null, isRunning = false, abortCtrl = null;
let chatHistory = [], tpsHist = [], editTarget = null;
let stats = { tok:0, srch:0, scr:0, iter:0, msgs:0 };
let scheduledTasks = JSON.parse(localStorage.getItem(LS+'sched')||'[]');
let networkHosts = [];
let inputHistory = JSON.parse(localStorage.getItem(LS+'inputHist')||'[]');
let historyIdx = -1;
let paletteOpen = false;
let ragEngine = new HybridRAG();
let ragSources = [];
let mem = {
soul: lsg('soul') || DEFAULT_SOUL,
user: lsg('user') || DEFAULT_USER,
skills: JSON.parse(lsg('skills') || 'null') || { ...BUILTIN_SKILLS },
};
let cfg = { brave: lsg('brave')||'', model: lsg('model')||'Llama-3.2-3B-Instruct-q4f16_1-MLC' };
function lsg(k){ return localStorage.getItem(LS+k); }
function lss(k,v){ localStorage.setItem(LS+k, typeof v==='object'?JSON.stringify(v):v); }
// ═══════════════════════════════════════════
// SYSTEM PROMPT β€” rebuilt every turn
// ═══════════════════════════════════════════
function buildSysPrompt() {
const now = new Date().toLocaleString('en-DE',{dateStyle:'long',timeStyle:'short'});
const skillsBlock = Object.keys(mem.skills).length
? Object.entries(mem.skills).map(([n,c]) => `\n---\n### Active Skill: ${n}\n${c}`).join('\n')
: '_(no extra skills loaded)_';
const netBlock = networkHosts.length
? networkHosts.map(h => `- ${h.url} (${h.status})`).join('\n')
: '_(no local services discovered yet)_';
return `${mem.soul}
---
## πŸ“‹ User Memory (persisted across sessions)
${mem.user}
---
## ⚑ Active Skills
${skillsBlock}
---
## πŸ“‘ Local Network Services
${netBlock}
---
## πŸ›  Tool Protocol β€” ReAct Format
You are an autonomous agent. Use tools for real-world or current information.
### Tools Available
\`web_search(query)\` β€” search the web (Brave/SearXNG/DDG)
\`scrape(url)\` β€” fetch and read any URL
\`summarize(text, focus?)\` β€” compress long content
\`remember(content)\` β€” save facts to user memory (persists!)
\`read_memory()\` β€” read current user memory contents
\`forget(query)\` β€” remove matching entries from user memory
\`schedule(delay_sec, message)\` β€” schedule a timed notification
\`inject_js(code, description)\` β€” execute validated JavaScript in the page
\`network_scan()\` β€” scan local network for running services
\`rag_index(text, source?)\` β€” index text into hybrid retrieval engine (BM25 + phonetic + n-gram)
\`rag_search(query, top_k?)\` β€” search indexed documents, returns ranked passages
\`rag_prompt(query, top_k?)\` β€” build RAG prompt with retrieved context for answering
### Strict Format
**Needing a tool:**
<think>
I need X because Y. I will web_search for it.
</think>
<action>{"tool": "web_search", "query": "X 2026"}</action>
**After an <observation>:**
<think>
The result shows Z. I'll now write the final answer.
</think>
[final answer here β€” NO <action> tag]
### Rules
- ALWAYS wrap reasoning in <think>...</think>
- Use tools for: current events, URLs, uncertain facts, scraping
- Skip tools for: math, code help, stable well-known facts
- Up to ${MAX_ITER} iterations per message
- Current date/time: ${now}`;
}
// ═══════════════════════════════════════════
// FETCH HELPERS
// ═══════════════════════════════════════════
async function ft(url, opts={}, ms=15000){
const c=new AbortController(); const t=setTimeout(()=>c.abort(),ms);
try{ return await fetch(url,{...opts, signal:c.signal, credentials:"omit"}); }
finally{ clearTimeout(t); }
}
async function fetchViaProxyOrDirect(targetUrl, opts={}, ms=15000){
try{
console.log('[FE] proxy fetch:', targetUrl);
const r = await ft(PROXY + encodeURIComponent(targetUrl), opts, ms);
if (!r.ok) throw new Error(`proxy ${r.status}`);
console.log('[FE] proxy OK:', r.status);
log(`proxy β†’ ${targetUrl.slice(0,60)}`,'t');
return {viaProxy:true, res:r};
}catch(e){
try{
console.log('[FE] proxy fail, direct:', targetUrl, e.message);
const r2 = await ft(targetUrl, opts, ms);
console.log('[FE] direct OK:', r2.status);
log(`direct fallback β†’ ${targetUrl.slice(0,60)}`,'w');
return {viaProxy:false, res:r2};
}catch(e2){
throw e;
}
}
}
// CRITICAL FIX: Read body as text first, then try JSON.parse
// Old code called res.json() which consumed the body stream, then res.text() also
// failed because the stream was already consumed. This broke all search/scrape.
async function parseResponse(res){
const raw = await res.text();
console.log('[FE] parseResponse: status=', res.status, 'len=', raw.length, 'ct=', res.headers.get('content-type'));
try{
const j = JSON.parse(raw);
if (j && typeof j === 'object' && 'contents' in j) {
try{ return JSON.parse(j.contents); }catch(_){ return j.contents; }
}
return j;
}catch(e){
return raw;
}
}
// ═══════════════════════════════════════════
// TOOLS
// ═══════════════════════════════════════════
async function toolSearch(query) {
stats.srch++; log(`πŸ” search: "${query}"`, 't');
// 1. Brave direct
if (cfg.brave) {
try {
const r = await ft(`https://api.search.brave.com/res/v1/web/search?q=${encodeURIComponent(query)}&count=5`,
{ headers:{'Accept':'application/json','X-Subscription-Token':cfg.brave} });
const d = await r.json();
if (d.web?.results?.length) {
log(`βœ“ Brave: ${d.web.results.length} results`,'g');
return d.web.results.map((x,i)=>`[${i+1}] ${x.title}\n${x.url}\n${x.description||''}`).join('\n\n');
}
} catch(e){ log(`Brave: ${e.message}`,'w'); }
}
// 2. SearXNG via proxy
try {
const url = `https://searx.be/search?q=${encodeURIComponent(query)}&format=json&engines=google,bing&language=en`;
const {res: r} = await fetchViaProxyOrDirect(url);
const d = await parseResponse(r);
const dd = (typeof d === 'string') ? JSON.parse(d) : d;
if (dd.results?.length) {
log(`βœ“ SearXNG: ${dd.results.length}`,'g');
return dd.results.slice(0,5).map((x,i)=>`[${i+1}] ${x.title}\n${x.url}\n${x.content||''}`).join('\n\n');
}
} catch(e){ log(`SearXNG: ${e.message}`,'w'); }
// 3. DuckDuckGo instant answer API
try {
const url = `https://api.duckduckgo.com/?q=${encodeURIComponent(query)}&format=json&no_html=1&skip_disambig=1`;
const {res: r} = await fetchViaProxyOrDirect(url);
const d = await parseResponse(r);
const dd = (typeof d === 'string') ? (() => { try { return JSON.parse(d); } catch(_) { return {}; } })() : (d || {});
console.log('[FE] DDG API keys:', Object.keys(dd).filter(k => dd[k] && (typeof dd[k] !== 'object' || (Array.isArray(dd[k]) ? dd[k].length : Object.keys(dd[k]).length))));
const out = [];
if (dd.AbstractText) out.push(`Summary: ${dd.AbstractText}\n${dd.AbstractURL}`);
if (dd.Answer) out.push(`Direct Answer: ${dd.Answer}`);
if (dd.Definition) out.push(`Definition: ${dd.Definition}\n${dd.DefinitionURL||''}`);
if (dd.Redirect) out.push(`Redirect: ${dd.Redirect}`);
(dd.RelatedTopics||[]).slice(0,5).forEach((t,i)=>{
if (t.Text) out.push(`[${i+1}] ${t.Text}\n${t.FirstURL||''}`);
// DDG groups topics in sub-arrays
if (t.Topics) t.Topics.slice(0,2).forEach(st => { if(st.Text) out.push(` - ${st.Text}\n ${st.FirstURL||''}`); });
});
if (out.length){ log(`βœ“ DDG API: ${out.length} items`,'g'); return out.join('\n\n'); }
} catch(e){ log(`DDG API: ${e.message}`,'e'); }
// 4. Fallback β€” scrape DDG HTML page and extract links from markdown
try {
const ddgHtml = `https://duckduckgo.com/html/?q=${encodeURIComponent(query)}`;
const {res: r} = await fetchViaProxyOrDirect(ddgHtml);
const body = await parseResponse(r);
const bodyStr = (typeof body === 'string') ? body : JSON.stringify(body);
console.log('[FE] DDG HTML body sample:', bodyStr.slice(0, 400));
const results = [];
let m;
// html2text converts DDG HTML links to markdown: [title](//duckduckgo.com/l/?uddg=ENCODED_URL&...)
// We must: 1) match ALL markdown links (including protocol-relative //) 2) decode uddg= param
const mdLinkRe = /\[([^\]]+)\]\(([^)]+)\)/g;
while ((m = mdLinkRe.exec(bodyStr)) && results.length < 10) {
let title = m[1].trim();
let url = m[2].trim();
// DDG redirect: extract actual URL from uddg= parameter
if (url.includes('uddg=')) {
const uddg = url.match(/uddg=([^&]+)/);
if (uddg) url = decodeURIComponent(uddg[1]);
}
// Skip internal DDG nav links, icons, short titles
if (!url.startsWith('http')) continue;
if (url.includes('duckduckgo.com')) continue;
if (title.length < 3) continue;
if (results.some(r => r.url === url)) continue;
// Grab snippet: next non-empty line after the link in the markdown
const linkEnd = m.index + m[0].length;
const after = bodyStr.slice(linkEnd, linkEnd + 300).split('\n').filter(l => l.trim().length > 10);
const snippet = (after[0] || '').trim().slice(0, 200);
results.push({ title, url, snippet });
}
// Also find bare https:// URLs in text
const bareRe = /(?:^|\s)(https?:\/\/[^\s)<>"]+)/g;
while ((m = bareRe.exec(bodyStr)) && results.length < 10) {
const url = m[1];
if (!url.includes('duckduckgo.com') && !results.some(r => r.url === url)) {
results.push({ title: url, url, snippet: '' });
}
}
if (results.length) {
log(`βœ“ DDG HTML fallback: ${results.length} results`,'g');
const list = results.slice(0,6).map((r,i) =>
`[${i+1}] ${r.title}\n${r.url}${r.snippet ? '\n' + r.snippet : ''}`
).join('\n\n');
// Scrape top result for richer content
let topContent = '';
try { topContent = await toolScrape(results[0].url); }
catch(e) { topContent = '(scrape of top result failed)'; }
return `Search results for "${query}":\n${list}\n\n---\nTop result content:\n${topContent}`;
}
} catch(e){ log(`DDG-html: ${e.message}`,'w'); }
return `No results found for: "${query}"\nTip: Add a free Brave Search API key in Settings.`;
}
async function toolScrape(url) {
stats.scr++; log(`🌐 scrape: ${url}`,'t');
try {
const {res: r, viaProxy} = await fetchViaProxyOrDirect(url);
let text = await parseResponse(r);
if (typeof text !== 'string') text = JSON.stringify(text);
// If proxy returned markdown (content-type: text/markdown), use it as-is
const ct = r.headers?.get('content-type') || '';
if (!viaProxy || !ct.includes('markdown')) {
// Strip HTML tags for non-markdown responses
text = text
.replace(/<script[\s\S]*?<\/script>/gi,'')
.replace(/<style[\s\S]*?<\/style>/gi,'')
.replace(/<nav[\s\S]*?<\/nav>/gi,'')
.replace(/<header[\s\S]*?<\/header>/gi,'')
.replace(/<footer[\s\S]*?<\/footer>/gi,'')
.replace(/<[^>]+>/g,' ')
.replace(/&nbsp;/g,' ').replace(/&amp;/g,'&').replace(/&lt;/g,'<').replace(/&gt;/g,'>').replace(/&quot;/g,'"')
.replace(/[ \t]{3,}/g,' ').replace(/\n{3,}/g,'\n\n').trim();
}
if (text.length < 80) return `No readable content at ${url}`;
log(`βœ“ scraped ${text.length} chars`,'g');
const MAX=7000;
return text.slice(0,MAX) + (text.length>MAX ? `\n\n[...truncated β€” use summarize() for key points]` : '');
} catch(e){ log(`scrape: ${e.message}`,'e'); return `Failed to scrape ${url}: ${e.message}`; }
}
async function toolSummarize(text, focus) {
if (!engine) return 'No engine loaded.';
log(`πŸ“ summarize ${text.length} chars`+(focus?` focus:${focus}`:''),'t');
const r = await engine.chat.completions.create({
messages:[{role:'user',content: focus
? `Summarize focusing on "${focus}":\n\n${text.slice(0,9000)}`
: `Write a concise summary of key points:\n\n${text.slice(0,9000)}`
}], max_tokens:600, temperature:0.2
});
const s = r.choices[0].message.content;
log(`βœ“ summarized β†’ ${s.length} chars`,'g');
return s;
}
function toolRemember(content) {
const date = new Date().toLocaleDateString('en-DE',{year:'numeric',month:'short',day:'numeric'});
if (mem.user === DEFAULT_USER) mem.user = '# User Memory\n';
mem.user += `\n\n## Note β€” ${date}\n${content}`;
lss('user', mem.user);
refreshFilesUI(); refreshMemView();
log(`πŸ’Ύ remembered: "${content.slice(0,60)}"`, 'g');
return `βœ“ Saved to user memory: "${content.slice(0,80)}${content.length>80?'…':''}"`;
}
function toolReadMemory() {
log('πŸ“– read_memory','t');
return mem.user || '(empty)';
}
function toolForget(query) {
log(`πŸ—‘ forget: "${query}"`,'t');
const lines = mem.user.split('\n');
const filtered = [];
let skip = false;
for (const line of lines) {
if (line.startsWith('## ') && line.toLowerCase().includes(query.toLowerCase())) {
skip = true; continue;
}
if (line.startsWith('## ') && skip) skip = false;
if (!skip) filtered.push(line);
}
const newMem = filtered.join('\n');
if (newMem === mem.user) return `No memory entries matching "${query}" found.`;
mem.user = newMem;
lss('user', mem.user);
refreshFilesUI(); refreshMemView();
log(`βœ“ forgot entries matching "${query}"`,'g');
return `βœ“ Removed memory entries matching "${query}".`;
}
function toolSchedule(delaySec, message) {
const delayMs = Math.max(1, Math.min(86400, delaySec)) * 1000;
const fireAt = Date.now() + delayMs;
const id = 'sched_' + Date.now();
const task = { id, message, fireAt, delaySec };
scheduledTasks.push(task);
lss('sched', scheduledTasks);
refreshSchedUI();
setTimeout(() => {
showToast(`⏰ ${message}`);
log(`⏰ SCHEDULED FIRED: ${message}`,'w');
scheduledTasks = scheduledTasks.filter(t => t.id !== id);
lss('sched', scheduledTasks);
refreshSchedUI();
if (engine && !isRunning) {
runAgent(`[SCHEDULED REMINDER] ${message}`);
}
}, delayMs);
log(`⏰ scheduled in ${delaySec}s: "${message}"`,'g');
return `βœ“ Scheduled notification in ${delaySec} seconds: "${message}"`;
}
function toolInjectJs(code, description) {
log(`πŸ’‰ inject_js: ${description||'(no desc)'}`, 't');
// Validate β€” block dangerous patterns
const banned = [
/\beval\b/, /\bFunction\s*\(/, /document\.cookie/i,
/localStorage\.clear/i, /window\.location\s*=/,
/importScripts/i,
];
for (const pat of banned) {
if (pat.test(code)) return `❌ Blocked: code matches forbidden pattern ${pat}. Injection refused.`;
}
if (code.length > 5000) return '❌ Code too long (max 5000 chars).';
try {
const result = new Function('log', 'document', 'window',
`"use strict";\ntry {\n${code}\nreturn "βœ“ Executed successfully";\n} catch(e) { return "Error: " + e.message; }`
)(log, document, window);
log(`βœ“ inject_js ok: ${String(result).slice(0,100)}`,'g');
return String(result);
} catch(e) {
log(`inject_js error: ${e.message}`,'e');
return `❌ Execution error: ${e.message}`;
}
}
async function toolNetworkScan() {
log('πŸ“‘ network_scan starting…','t');
const targets = [
{ url: 'http://127.0.0.1:8080', label: 'proxy (8080)' },
{ url: 'http://127.0.0.1:8000', label: 'http (8000)' },
{ url: 'http://127.0.0.1:3000', label: 'dev (3000)' },
{ url: 'http://127.0.0.1:3001', label: 'dev (3001)' },
{ url: 'http://127.0.0.1:5000', label: 'flask (5000)' },
{ url: 'http://127.0.0.1:5173', label: 'vite (5173)' },
{ url: 'http://127.0.0.1:4200', label: 'angular (4200)' },
{ url: 'http://127.0.0.1:8888', label: 'jupyter (8888)' },
{ url: 'http://127.0.0.1:9090', label: 'prometheus (9090)' },
{ url: 'http://127.0.0.1:11434', label: 'ollama (11434)' },
];
const results = await Promise.allSettled(
targets.map(async t => {
try {
const r = await ft(t.url, {mode:'no-cors'}, 3000);
return { ...t, status: 'UP', code: r.status || 'opaque' };
} catch(e) {
return { ...t, status: 'DOWN' };
}
})
);
networkHosts = results.map(r => r.value || r.reason).filter(Boolean);
refreshNetUI();
const up = networkHosts.filter(h => h.status === 'UP');
log(`πŸ“‘ scan: ${up.length}/${targets.length} up`,'g');
return `Network scan (${new Date().toLocaleTimeString()}):\n` +
networkHosts.map(h => `${h.status === 'UP' ? 'βœ“' : 'βœ—'} ${h.label} β€” ${h.url}`).join('\n');
}
function toolRagIndex(text, source) {
if (!text || text.trim().length < 20) return 'Error: text too short to index (min 20 chars).';
const label = source || 'tool-input-' + Date.now();
const result = ragEngine.addText(text);
ragSources.push(label);
refreshRagUI();
log(`πŸ”Ž RAG indexed: ${result.sentences} sentences, ${result.uniqueTerms} terms (${label})`, 'g');
return `βœ“ RAG indexed: ${result.sentences} sentences, ${result.uniqueTerms} unique terms. Source: ${label}`;
}
function toolRagSearch(query, topK) {
if (!ragEngine.indexed) return 'No text indexed yet. Use rag_index to add documents first.';
const k = Math.min(Math.max(1, topK || 5), 10);
const result = ragEngine.query(query, k, 1);
log(`πŸ”Ž RAG search: "${query}" β†’ ${result.passages.length} passages (${result.totalCandidates} candidates)`, 'g');
if (result.passages.length === 0) return `No relevant passages found for: "${query}"`;
refreshRagUI();
return result.passages.map((p, i) =>
`[${i + 1}] (score: ${p.score.toFixed(2)}) ${p.text}`
).join('\n\n');
}
function toolRagPrompt(query, topK) {
if (!ragEngine.indexed) return 'No text indexed yet. Use rag_index first.';
const result = ragEngine.query(query, topK || 5, 1);
log(`πŸ”Ž RAG prompt built for: "${query}"`, 'g');
return result.prompt;
}
async function runTool(name, params) {
switch(name) {
case 'web_search': return await toolSearch(params.query||params.q||'');
case 'scrape': return await toolScrape(params.url||'');
case 'summarize': return await toolSummarize(params.text||'', params.focus||'');
case 'remember': return toolRemember(params.content||params.text||'');
case 'read_memory': return toolReadMemory();
case 'forget': return toolForget(params.query||params.content||'');
case 'schedule': return toolSchedule(Number(params.delay_sec||params.delay||60), params.message||params.content||'Reminder');
case 'inject_js': return toolInjectJs(params.code||'', params.description||'');
case 'network_scan': return await toolNetworkScan();
case 'rag_index': return toolRagIndex(params.text||params.content||'', params.source||params.label||'');
case 'rag_search': return toolRagSearch(params.query||params.q||'', Number(params.top_k||params.topK||5));
case 'rag_prompt': return toolRagPrompt(params.query||params.q||'', Number(params.top_k||params.topK||5));
default: return `Unknown tool "${name}". Available: web_search, scrape, summarize, remember, read_memory, forget, schedule, inject_js, network_scan, rag_index, rag_search, rag_prompt`;
}
}
// ═══════════════════════════════════════════
// AGENT LOOP
// ═══════════════════════════════════════════
async function runAgent(msg) {
if (isRunning||!engine) return;
isRunning=true; abortCtrl=new AbortController();
document.getElementById('sbtn').style.display='none';
document.getElementById('stopbtn').style.display='block';
document.getElementById('uin').disabled=true;
// Save to input history
if (msg && !msg.startsWith('[SCHEDULED')) {
inputHistory = inputHistory.filter(h => h !== msg);
inputHistory.unshift(msg);
if (inputHistory.length > 50) inputHistory.pop();
lss('inputHist', inputHistory);
}
historyIdx = -1;
appendUser(msg);
chatHistory.push({role:'user',content:msg});
stats.msgs++;
const scrapeBefore = stats.scr, searchBefore = stats.srch;
const apiMsgs = [
{role:'system', content:buildSysPrompt()},
...chatHistory.slice(-18)
];
const agentEl = createAgentBubble();
let iter=0, lastText='';
try {
while (iter < MAX_ITER) {
iter++; stats.iter++;
document.getElementById('iter-p').textContent = `ITER ${iter}`;
log(`── iter ${iter}/${MAX_ITER}`,'');
const {text, tps} = await streamLLM(apiMsgs, agentEl);
lastText=text; updateTPS(tps); updateStats();
const am = text.match(/<action>([\s\S]*?)<\/action>/);
if (!am) {
renderAnswer(agentEl, text);
chatHistory.push({role:'assistant',content:text});
stats.msgs++;
break;
}
let tc;
try { tc = JSON.parse(am[1].trim()); }
catch(e) {
addToolCard(agentEl,{tool:'parse_error'},null,`JSON error: ${e.message}`,true);
apiMsgs.push({role:'assistant',content:text});
apiMsgs.push({role:'user',content:`<observation>ERROR: could not parse action JSON: ${e.message}\nRaw: ${am[1]}</observation>`});
continue;
}
apiMsgs.push({role:'assistant',content:text});
const card = addToolCard(agentEl, tc, null, null, false);
let result;
try { result = await runTool(tc.tool, tc); }
catch(e) { result = `Tool error: ${e.message}`; }
finalizeCard(card, result);
stats.tok += Math.ceil(result.length/4);
apiMsgs.push({role:'user',content:`<observation tool="${tc.tool}">\n${result}\n</observation>`});
}
if (iter>=MAX_ITER) { addNote(agentEl,`Max iterations (${MAX_ITER}) reached.`); chatHistory.push({role:'assistant',content:lastText}); }
} catch(err) {
if (err.name==='AbortError') addNote(agentEl,'β–  Stopped.');
else { addNote(agentEl,`Error: ${err.message}`); log(err.message,'e'); }
}
isRunning=false; abortCtrl=null;
document.getElementById('iter-p').textContent='ITER 0';
document.getElementById('sbtn').style.display='block';
document.getElementById('stopbtn').style.display='none';
document.getElementById('uin').disabled=false;
document.getElementById('uin').focus();
updateStats();
validateResponse(msg, lastText, stats.scr - scrapeBefore, stats.srch - searchBefore);
}
// ═══════════════════════════════════════════
// RESPONSE VALIDATOR
// ═══════════════════════════════════════════
function validateResponse(userMsg, agentText, scrapesDone, searchesDone) {
const warnings = [];
const urlRe = /https?:\/\/[^\s)>\"']+/gi;
const mentionedURLs = userMsg.match(urlRe) || [];
const opens = (agentText.match(/<action>/g) || []).length;
const closes = (agentText.match(/<\/action>/g) || []).length;
if (opens !== closes) warnings.push('⚠ Malformed <action> tags (mismatched open/close).');
const intendedScrape = /"tool"\s*:\s*"scrape"/.test(agentText);
const intendedSearch = /"tool"\s*:\s*"web_search"/.test(agentText);
if (mentionedURLs.length > 0 && scrapesDone === 0 && !intendedScrape)
warnings.push(`⚠ URL detected but scrape() was not called.`);
if (mentionedURLs.length > 0 && scrapesDone === 0 && intendedScrape)
warnings.push('⚠ Agent tried to scrape but it may have failed.');
const needsSearch = /\b(latest|current|today|news|recent|who is|what is the price|202[0-9])\b/i.test(userMsg);
if (needsSearch && searchesDone === 0 && !intendedSearch && scrapesDone === 0)
warnings.push('⚠ Time-sensitive query β€” no search was used.');
if (agentText && agentText.length < 60 && opens === 0)
warnings.push('⚠ Very short response β€” model may have truncated.');
if (warnings.length > 0) {
const el = createAgentBubble();
el.closest('.mwrap').querySelector('.mrole').textContent = 'HINT';
el.closest('.mwrap').querySelector('.mrole').style.color = 'var(--amber)';
warnings.forEach(w => addNote(el, w));
}
}
// ═══════════════════════════════════════════
// STREAMING
// ═══════════════════════════════════════════
async function streamLLM(msgs, container) {
const d = document.createElement('div');
d.style.cssText='font-size:11px;color:var(--fg-dim);white-space:pre-wrap;line-height:1.5;border-left:2px solid var(--fg-faint);padding-left:8px;margin-bottom:4px;min-height:16px';
const cur = document.createElement('span'); cur.className='cur';
d.appendChild(cur); container.appendChild(d); scroll();
let full='', toks=0;
const t0=Date.now();
const stream = await engine.chat.completions.create({
messages:msgs, stream:true, temperature:0.7, max_tokens:1400,
stream_options:{include_usage:true}
});
for await (const chunk of stream) {
if (abortCtrl?.signal.aborted) break;
const delta = chunk.choices[0]?.delta?.content||'';
if (delta) { full+=delta; toks++; stats.tok++; d.innerHTML=esc(full).replace(/\n/g,'<br>'); d.appendChild(cur); scroll(); }
}
cur.remove(); d.remove();
const secs=(Date.now()-t0)/1000;
log(`βœ“ ${toks} tok Β· ${secs>0?Math.round(toks/secs):0}t/s`,'g');
return {text:full, tps:secs>0?Math.round(toks/secs):0};
}
// ═══════════════════════════════════════════
// RENDER
// ═══════════════════════════════════════════
function appendUser(text) {
const w=document.createElement('div'); w.className='mwrap user';
w.innerHTML=`<div class="mrole">USER</div><div class="mbody">${esc(text).replace(/\n/g,'<br>')}</div>`;
document.getElementById('msgs').appendChild(w); scroll();
}
function createAgentBubble() {
const w=document.createElement('div'); w.className='mwrap agent';
const id='ac'+Date.now();
w.innerHTML=`<div class="mrole">AGENT</div><div class="mbody" id="${id}"></div>`;
document.getElementById('msgs').appendChild(w); scroll();
return document.getElementById(id);
}
function renderAnswer(container, text) {
const thRe=/<think>([\s\S]*?)<\/think>/g; let m;
while((m=thRe.exec(text))!==null) {
const tb=document.createElement('div'); tb.className='think';
tb.innerHTML=`<div class="think-hdr"><span>πŸ’­</span><span>Thinking…</span><span class="think-toggle">β–Ό expand</span></div><div class="think-body">${esc(m[1].trim()).replace(/\n/g,'<br>')}</div>`;
tb.querySelector('.think-hdr').addEventListener('click',()=>{
tb.classList.toggle('open');
tb.querySelector('.think-toggle').textContent=tb.classList.contains('open')?'β–² collapse':'β–Ό expand';
});
container.appendChild(tb);
}
const clean = text.replace(/<think>[\s\S]*?<\/think>/g,'').replace(/<action>[\s\S]*?<\/action>/g,'').trim();
if (clean) { const a=document.createElement('div'); a.className='answer'; a.innerHTML=md(clean); container.appendChild(a); }
scroll();
}
const TICONS={web_search:'πŸ”',scrape:'🌐',summarize:'πŸ“',remember:'πŸ’Ύ',read_memory:'πŸ“–',forget:'πŸ—‘',schedule:'⏰',inject_js:'πŸ’‰',network_scan:'πŸ“‘',rag_index:'πŸ”Ž',rag_search:'πŸ”Ž',rag_prompt:'πŸ”Ž',parse_error:'⚠'};
function addToolCard(container, tc, result, errMsg, isError) {
const card=document.createElement('div'); card.className='tcard';
if(isError) card.style.borderColor='var(--red)';
const params=Object.entries(tc).filter(([k])=>k!=='tool').map(([k,v])=>`${k}="${esc(String(v).slice(0,70))}"`).join(' ');
const sid='ts'+Date.now();
card.innerHTML=`
<div class="tcard-hdr" onclick="this.nextElementSibling.classList.toggle('vis')">
<span>${TICONS[tc.tool]||'πŸ”§'}</span>
<span class="tname">${esc(tc.tool||'')}</span>
<span class="tparams">${params}</span>
<span id="${sid}" class="${isError?'':'tspin'}">${isError?'⚠':'⟳'}</span>
<span class="ttog">β–Ύ</span>
</div>
<div class="tres${result||errMsg?' vis':''}">${esc(errMsg||result||'running…')}</div>`;
container.appendChild(card); scroll();
return card;
}
function finalizeCard(card, result) {
const sp=card.querySelector('.tspin');
if(sp){sp.style.animation='none';sp.textContent='βœ“';sp.style.color='var(--fg)';}
const res=card.querySelector('.tres');
if(res){res.textContent=result;res.classList.add('vis');}
scroll();
}
function addNote(container, text) {
const d=document.createElement('div');
d.style.cssText='font-size:10px;color:var(--fg-dim);border-left:2px solid var(--border);padding-left:6px;margin-top:4px;font-style:italic';
d.textContent=text; container.appendChild(d); scroll();
}
function sysMsg(text) {
const w=document.createElement('div'); w.className='mwrap sys';
w.innerHTML=`<div class="mrole">SYS</div><div class="mbody">${esc(text).replace(/\n/g,'<br>')}</div>`;
document.getElementById('msgs').appendChild(w); scroll();
}
// ═══════════════════════════════════════════
// MARKDOWN / UTILS
// ═══════════════════════════════════════════
function md(text) {
let h=esc(text);
h=h.replace(/```(\w*)\n?([\s\S]*?)```/g,(_,l,c)=>`<pre><code>${c}</code></pre>`);
h=h.replace(/`([^`\n]+)`/g,'<code>$1</code>');
h=h.replace(/\*\*([^*]+)\*\*/g,'<strong>$1</strong>');
h=h.replace(/\*([^*\n]+)\*/g,'<em>$1</em>');
h=h.replace(/^### (.+)$/gm,'<h3>$1</h3>');
h=h.replace(/^## (.+)$/gm,'<h2>$1</h2>');
h=h.replace(/^# (.+)$/gm,'<h1>$1</h1>');
h=h.replace(/^[-*β€’] (.+)$/gm,'<li>$1</li>');
h=h.replace(/^\d+\. (.+)$/gm,'<li>$1</li>');
h=h.replace(/^---$/gm,'<hr style="border:none;border-top:1px solid var(--border);margin:8px 0">');
h=h.replace(/\n\n/g,'<br><br>').replace(/\n(?!<)/g,'<br>');
return h;
}
function esc(s){return String(s).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;');}
function scroll(){const m=document.getElementById('msgs');m.scrollTop=m.scrollHeight;}
// ═══════════════════════════════════════════
// TOAST NOTIFICATIONS
// ═══════════════════════════════════════════
function showToast(msg, duration=6000) {
const area = document.getElementById('toast-area');
const t = document.createElement('div'); t.className = 'toast';
t.textContent = msg;
area.appendChild(t);
// Flash the header too
const hdr = document.getElementById('hdr');
hdr.style.borderBottom = '2px solid var(--amber)';
setTimeout(() => { hdr.style.borderBottom = ''; }, 2000);
setTimeout(() => { t.classList.add('fade'); setTimeout(() => t.remove(), 500); }, duration);
}
// ═══════════════════════════════════════════
// FILE MANAGEMENT
// ═══════════════════════════════════════════
window.triggerFile = t => document.getElementById(t==='skill'?'skill-f':t+'-f').click();
function readFile(file, cb) { const r=new FileReader(); r.onload=e=>cb(e.target.result,file.name); r.readAsText(file); }
document.getElementById('soul-f').addEventListener('change',function(){
if(this.files[0]) readFile(this.files[0],(c,n)=>{mem.soul=c;lss('soul',c);refreshFilesUI();refreshMemView();log(`βœ“ soul.md loaded (${c.length}ch)`,'g');});
});
document.getElementById('user-f').addEventListener('change',function(){
if(this.files[0]) readFile(this.files[0],(c,n)=>{mem.user=c;lss('user',c);refreshFilesUI();refreshMemView();log(`βœ“ user.md loaded (${c.length}ch)`,'g');});
});
document.getElementById('skill-f').addEventListener('change',function(){
Array.from(this.files).forEach(f=>readFile(f,(c,n)=>{
const k=n.replace(/\.(md|txt)$/i,'');
mem.skills[k]=c; lss('skills',mem.skills);
refreshFilesUI(); refreshMemView();
log(`βœ“ skill: ${k} loaded`,'g');
}));
});
window.removeSkill = function(name) {
delete mem.skills[name]; lss('skills',mem.skills);
refreshFilesUI(); refreshMemView(); log(`removed skill: ${name}`,'w');
};
function refreshFilesUI() {
const sc=mem.soul!==DEFAULT_SOUL, uc=mem.user!==DEFAULT_USER;
document.getElementById('soul-n').className='fname'+(sc?' ok':'');
document.getElementById('soul-b').textContent=sc?mem.soul.length+'ch':'default';
document.getElementById('user-n').className='fname'+(uc?' ok':'');
document.getElementById('user-b').textContent=uc?mem.user.length+'ch':'default';
const chips=document.getElementById('skill-chips');
chips.innerHTML='';
Object.keys(mem.skills).forEach(name=>{
const c=document.createElement('div'); c.className='chip';
c.innerHTML=`${esc(name)} <span class="rm" onclick="removeSkill('${esc(name)}')">βœ•</span>`;
chips.appendChild(c);
});
}
function refreshMemView() {
const sp=buildSysPrompt();
document.getElementById('mem-view').textContent=
`=== LIVE CONTEXT (${sp.length} chars) ===\n\n${sp.slice(0,1400)}${sp.length>1400?'\n\n[…truncated β€” click EDIT to view full]':''}`;
}
window.openEdit = function(target) {
editTarget=target;
document.getElementById('modal-title').textContent=`EDIT ${target.toUpperCase()}.MD`;
document.getElementById('modal-area').value=target==='soul'?mem.soul:mem.user;
document.getElementById('modal').classList.add('vis');
};
window.closeModal = function(){document.getElementById('modal').classList.remove('vis');editTarget=null;};
window.saveModal = function(){
const v=document.getElementById('modal-area').value;
if(editTarget==='soul'){mem.soul=v;lss('soul',v);}
if(editTarget==='user'){mem.user=v;lss('user',v);}
refreshFilesUI();refreshMemView();closeModal();log(`saved ${editTarget}.md`,'g');
};
window.exportModal = function(){
const v=document.getElementById('modal-area').value;
const a=document.createElement('a');
a.href=URL.createObjectURL(new Blob([v],{type:'text/markdown'}));
a.download=(editTarget||'file')+'.md'; a.click();
};
document.getElementById('modal').addEventListener('click',e=>{if(e.target===document.getElementById('modal'))closeModal();});
window.saveCfg = function(){cfg.brave=document.getElementById('brave-in').value.trim();lss('brave',cfg.brave);log('settings saved','g');};
window.clearUserMem = function(){if(!confirm('Reset user.md?'))return;mem.user=DEFAULT_USER;lss('user',mem.user);refreshFilesUI();refreshMemView();log('user.md reset','w');};
window.exportAll = function(){
const out=`# Chronos Export β€” ${new Date().toISOString().split('T')[0]}\n\n---\n\n# soul.md\n${mem.soul}\n\n---\n\n# user.md\n${mem.user}\n\n---\n\n# Skills\n${Object.entries(mem.skills).map(([n,c])=>`\n## ${n}\n\n${c}`).join('\n')}`;
const a=document.createElement('a');
a.href=URL.createObjectURL(new Blob([out],{type:'text/markdown'}));
a.download=`chronos-export-${Date.now()}.md`; a.click();
};
window.clearChat = function(){document.getElementById('msgs').innerHTML='';chatHistory=[];stats.msgs=0;stats.iter=0;updateStats();log('chat cleared','w');};
// ═══════════════════════════════════════════
// STATS / LOGGING
// ═══════════════════════════════════════════
function log(text,type=''){
const el=document.getElementById('alog');
const t=new Date().toLocaleTimeString('en',{hour12:false,hour:'2-digit',minute:'2-digit',second:'2-digit'});
const d=document.createElement('div'); d.className=`ll ${type}`; d.textContent=`[${t}] ${text}`;
el.appendChild(d); el.scrollTop=el.scrollHeight;
while(el.children.length>120)el.removeChild(el.firstChild);
}
function updateTPS(tps){tpsHist.push(tps);if(tpsHist.length>24)tpsHist.shift();document.getElementById('s-tps').innerHTML=`${tps}<span class="su">t/s</span>`;drawSpark();}
function updateStats(){document.getElementById('s-tok').textContent=stats.tok;document.getElementById('s-srch').textContent=stats.srch;document.getElementById('s-scr').textContent=stats.scr;document.getElementById('cmeta').textContent=`${stats.msgs} msgs Β· ${stats.tok} tok Β· ${stats.iter} iters`;}
function drawSpark(){
const c=document.getElementById('spark');if(!c)return;
const ctx=c.getContext('2d');const w=c.offsetWidth||180,h=28;c.width=w;
if(tpsHist.length<2)return;
const max=Math.max(...tpsHist,1);const step=w/(tpsHist.length-1);
ctx.clearRect(0,0,w,h);ctx.strokeStyle='#3dff70';ctx.shadowColor='#3dff70';ctx.shadowBlur=4;ctx.lineWidth=1.5;ctx.beginPath();
tpsHist.forEach((v,i)=>{const x=i*step,y=h-(v/max)*(h-4)-2;i===0?ctx.moveTo(x,y):ctx.lineTo(x,y);});ctx.stroke();
ctx.lineTo((tpsHist.length-1)*step,h);ctx.lineTo(0,h);ctx.closePath();ctx.shadowBlur=0;ctx.fillStyle='rgba(61,255,112,0.07)';ctx.fill();
}
// ═══════════════════════════════════════════
// NETWORK SCAN UI
// ═══════════════════════════════════════════
function refreshNetUI() {
const panel = document.getElementById('net-panel');
if (!networkHosts.length) { panel.innerHTML = '<div class="meta">No scan yet</div>'; return; }
panel.innerHTML = networkHosts.map(h =>
`<div class="net-item"><span class="ni-url">${esc(h.label)}</span><span class="ni-status ${h.status==='UP'?'ok':'fail'}">${h.status}</span></div>`
).join('');
}
// ═══════════════════════════════════════════
// SCHEDULER UI
// ═══════════════════════════════════════════
function refreshSchedUI() {
const panel = document.getElementById('sched-panel');
if (!scheduledTasks.length) { panel.innerHTML = '<div class="meta">No tasks</div>'; return; }
panel.innerHTML = scheduledTasks.map(t => {
const remaining = Math.max(0, Math.round((t.fireAt - Date.now()) / 1000));
return `<div class="sched-item"><span class="si-msg">${esc(t.message)}</span><span class="si-time">${remaining}s</span><span class="si-rm" onclick="cancelSched('${t.id}')">βœ•</span></div>`;
}).join('');
}
window.cancelSched = function(id) {
scheduledTasks = scheduledTasks.filter(t => t.id !== id);
lss('sched', scheduledTasks);
refreshSchedUI();
log(`cancelled scheduled task`,'w');
};
setInterval(refreshSchedUI, 5000);
// ═══════════════════════════════════════════
// COMMAND PALETTE (Arrow Up)
// ═══════════════════════════════════════════
function openPalette() {
if (paletteOpen) { closePalette(); return; }
paletteOpen = true;
const pal = document.getElementById('cmd-palette');
const inp = document.getElementById('cp-search');
const list = document.getElementById('cp-list');
pal.classList.add('vis');
inp.value = '';
inp.focus();
renderPaletteItems('');
inp.oninput = () => renderPaletteItems(inp.value);
inp.onkeydown = function(e) {
if (e.key === 'Escape') { closePalette(); return; }
if (e.key === 'Enter') {
const sel = list.querySelector('.cp-item.sel') || list.querySelector('.cp-item');
if (sel) sel.click();
}
if (e.key === 'ArrowDown' || e.key === 'ArrowUp') {
e.preventDefault();
const items = [...list.querySelectorAll('.cp-item')];
const cur = items.findIndex(i => i.classList.contains('sel'));
items.forEach(i => i.classList.remove('sel'));
let next = e.key === 'ArrowDown' ? cur + 1 : cur - 1;
if (next < 0) next = items.length - 1;
if (next >= items.length) next = 0;
if (items[next]) { items[next].classList.add('sel'); items[next].scrollIntoView({block:'nearest'}); }
}
};
}
function closePalette() {
paletteOpen = false;
document.getElementById('cmd-palette').classList.remove('vis');
document.getElementById('uin').focus();
}
function renderPaletteItems(filter) {
const list = document.getElementById('cp-list');
list.innerHTML = '';
const items = [];
// History
inputHistory.forEach(h => items.push({ badge: 'HISTORY', label: h, action: () => { document.getElementById('uin').value = h; closePalette(); } }));
// Tools
['web_search','scrape','summarize','remember','read_memory','forget','schedule','inject_js','network_scan','rag_index','rag_search','rag_prompt'].forEach(t => {
items.push({ badge: 'TOOL', label: t, action: () => { document.getElementById('uin').value = `Use tool: ${t}`; closePalette(); } });
});
// Skills
Object.keys(mem.skills).forEach(s => {
items.push({ badge: 'SKILL', label: s, action: () => { document.getElementById('uin').value = `Use skill: ${s}`; closePalette(); } });
});
// Actions
items.push({ badge: 'ACTION', label: 'Clear chat', action: () => { clearChat(); closePalette(); } });
items.push({ badge: 'ACTION', label: 'Export all', action: () => { exportAll(); closePalette(); } });
items.push({ badge: 'ACTION', label: 'Edit soul.md', action: () => { openEdit('soul'); closePalette(); } });
items.push({ badge: 'ACTION', label: 'Edit user.md', action: () => { openEdit('user'); closePalette(); } });
items.push({ badge: 'ACTION', label: 'Toggle console', action: () => { toggleConsole(); closePalette(); } });
items.push({ badge: 'ACTION', label: 'Network scan', action: async () => { closePalette(); await toolNetworkScan(); } });
const filt = filter.toLowerCase();
const matches = items.filter(i => !filt || i.label.toLowerCase().includes(filt) || i.badge.toLowerCase().includes(filt));
matches.slice(0, 20).forEach((item, idx) => {
const d = document.createElement('div');
d.className = 'cp-item' + (idx === 0 ? ' sel' : '');
d.innerHTML = `<span class="cp-badge">${item.badge}</span><span>${esc(item.label)}</span>`;
d.addEventListener('click', item.action);
list.appendChild(d);
});
}
// ═══════════════════════════════════════════
// MODEL SETUP
// ═══════════════════════════════════════════
const MODELS=[
{id:'TinyLlama-1.1B-Chat-v0.4-q4f16_1-MLC',label:'TinyLlama 1.1B Β· ~600MB',vram:'~1GB',speed:'fastest'},
{id:'Llama-3.2-1B-Instruct-q4f16_1-MLC',label:'Llama 3.2 Β· 1B Β· ~680MB',vram:'~1GB',speed:'fastest'},
{id:'gemma-2-2b-it-q4f16_1-MLC',label:'Gemma 2 Β· 2B Β· ~1.4GB',vram:'~2GB',speed:'fast'},
{id:'Llama-3.2-3B-Instruct-q4f16_1-MLC',label:'Llama 3.2 Β· 3B Β· ~1.8GB',vram:'~2GB',speed:'fast'},
{id:'Phi-3.5-mini-instruct-q4f16_1-MLC',label:'Phi-3.5 Mini Β· 3.8B Β· ~2.2GB',vram:'~3GB',speed:'fast'},
{id:'Llama-3.1-8B-Instruct-q4f16_1-MLC',label:'Llama 3.1 Β· 8B Β· ~4.8GB',vram:'~6GB',speed:'medium'},
{id:'Mistral-7B-Instruct-v0.3-q4f16_1-MLC',label:'Mistral Β· 7B Β· ~4.1GB',vram:'~5GB',speed:'medium'},
];
const grps={fastest:'⚑ Ultra-Fast',fast:'πŸš€ Fast',medium:'βš– Medium'};
const gels={};
Object.entries(grps).forEach(([k,l])=>{const g=document.createElement('optgroup');g.label=l;gels[k]=g;document.getElementById('msel').appendChild(g);});
MODELS.forEach(m=>{const o=document.createElement('option');o.value=m.id;o.textContent=m.label;o.selected=m.id===cfg.model;gels[m.speed].appendChild(o);});
document.getElementById('msel').addEventListener('change',function(){const m=MODELS.find(x=>x.id===this.value);document.getElementById('mmeta').textContent=m?`VRAM: ${m.vram}`:'β€”';});
document.getElementById('msel').dispatchEvent(new Event('change'));
document.getElementById('lbtn').addEventListener('click', async () => {
const modelId = document.getElementById('msel').value;
if (!navigator.gpu) {
log('❌ WebGPU not available! Use Chrome 113+ or Edge 113+','e');
sysMsg('❌ WebGPU not available.\n\nRequired: Chrome 113+ or Edge 113+\n\nCheck: chrome://gpu');
return;
}
// Softer COI check β€” warn but allow if served via HTTP (proxy sends COOP/COEP headers)
if (!window.crossOriginIsolated) {
const isHTTP = location.protocol.startsWith('http');
if (!isHTTP) {
log('❌ Not cross-origin isolated. Serve via HTTP!','e');
document.getElementById('setup-banner').classList.remove('hidden');
return;
}
log('⚠ COI not yet active β€” attempting load anyway…','w');
sysMsg('⚠ Cross-origin isolation pending.\nIf load fails, reload page once (service worker activates on first load).');
}
document.getElementById('lbtn').disabled=true;
document.getElementById('prog').classList.add('vis');
document.getElementById('mstatus').textContent='LOADING…';
document.getElementById('mdot').classList.remove('ready');
log(`loading ${modelId}`,'w');
try {
engine = new webllm.MLCEngine();
engine.setInitProgressCallback(r=>{
const p=Math.round((r.progress||0)*100);
document.getElementById('pfill').style.width=p+'%';
document.getElementById('ptxt').textContent=r.text||`${p}%`;
});
await engine.reload(modelId);
document.getElementById('mdot').classList.add('ready');
document.getElementById('mstatus').textContent='READY';
document.getElementById('pfill').style.width='100%';
document.getElementById('ptxt').textContent='βœ“ ready';
document.getElementById('uin').disabled=false;
document.getElementById('sbtn').disabled=false;
document.getElementById('uin').focus();
document.getElementById('welcome').classList.add('gone');
const short=modelId.replace(/-q4f\d+_\d+-MLC$/,'').replace(/-MLC$/,'');
document.getElementById('ctitle').textContent=`⚑ ${short}`;
lss('model',modelId); cfg.model=modelId;
log(`βœ“ ${short} ready`,'g');
const el=createAgentBubble();
renderAnswer(el, `**${short}** loaded.\n\n**Active context:**\n- Soul: ${mem.soul.split('\n').find(l=>l.startsWith('#'))||'Chronos'}\n- User memory: ${mem.user.length} chars\n- Skills: ${Object.keys(mem.skills).join(', ')||'none'}\n- Tools: web_search, scrape, summarize, remember, read_memory, forget, schedule, inject_js, network_scan, rag_index, rag_search, rag_prompt\n\n**Hotkeys:** Ctrl+\` console Β· ↑ palette Β· Ctrl+L clear`);
stats.msgs++;
} catch(err) {
log(`❌ load failed: ${err.message}`,'e');
sysMsg(`❌ Model load failed:\n${err.message}\n\nβ€’ Ensure WebGPU enabled\nβ€’ Reload page (COI service worker needs activation)\nβ€’ Try smaller model\nβ€’ Check chrome://gpu`);
document.getElementById('lbtn').disabled=false;
document.getElementById('mstatus').textContent='ERROR';
}
});
// ═══════════════════════════════════════════
// INPUT HANDLING
// ═══════════════════════════════════════════
document.getElementById('sbtn').addEventListener('click',()=>{
const t=document.getElementById('uin').value.trim();
if(t&&engine){document.getElementById('uin').value='';runAgent(t);}
});
document.getElementById('uin').addEventListener('keydown',e=>{
if(e.key==='Enter'&&!e.shiftKey){e.preventDefault();document.getElementById('sbtn').click();return;}
if(e.key==='ArrowUp'&&!e.shiftKey){
const inp=document.getElementById('uin');
if(inp.value.trim()===''){e.preventDefault();openPalette();}
}
});
document.getElementById('uin').addEventListener('input',function(){
this.style.height='auto';this.style.height=Math.min(this.scrollHeight,300)+'px';
});
document.getElementById('stopbtn').addEventListener('click',()=>{if(abortCtrl){abortCtrl.abort();log('β–  stopped','w');}});
// Global hotkeys
document.addEventListener('keydown',e=>{
if(e.ctrlKey&&e.key==='l'){e.preventDefault();clearChat();}
if(e.key==='`'&&(e.ctrlKey||e.metaKey)){e.preventDefault();toggleConsole();}
if(e.key==='Escape'){
if(paletteOpen) closePalette();
else if(document.getElementById('console-popup').classList.contains('vis')) toggleConsole();
else if(document.getElementById('modal').classList.contains('vis')) closeModal();
}
});
document.getElementById('brave-in').value=cfg.brave;
// ═══════════════════════════════════════════
// INIT
// ═══════════════════════════════════════════
const isoP=document.getElementById('iso-p');
if (window.crossOriginIsolated) {
isoP.textContent='ISO: βœ“'; isoP.style.color='var(--fg)';
document.getElementById('setup-banner').classList.add('hidden');
log('βœ“ Cross-origin isolated','g');
} else {
const isHTTP = location.protocol.startsWith('http');
if (isHTTP) {
isoP.textContent='ISO: ⟳'; isoP.style.color='var(--amber)';
document.getElementById('setup-banner').classList.add('hidden');
log('⚠ ISO pending β€” reload if model load fails','w');
} else {
isoP.textContent='ISO: βœ—'; isoP.style.color='var(--red)';
log('⚠ NOT isolated β€” run: python proxy.py 8080','e');
setTimeout(()=>{ if(!window.crossOriginIsolated) document.getElementById('setup-banner').classList.remove('hidden'); }, 1200);
}
}
if (!navigator.gpu) log('❌ WebGPU not detected β€” Chrome 113+ required','e');
else log('βœ“ WebGPU available','g');
refreshFilesUI();
refreshMemView();
refreshSchedUI();
refreshRagUI();
log('Chronos agent initialized','g');
log(`Skills: ${Object.keys(mem.skills).join(', ')}`,'');
log('Ctrl+` console Β· ↑ palette Β· Ctrl+L clear','');
// ═══════════════════════════════════════════
// RAG UI FUNCTIONS
// ═══════════════════════════════════════════
function refreshRagUI() {
const s = ragEngine.getStats();
document.getElementById('rag-status').textContent = s.indexed ? 'βœ“ ready' : 'no index';
document.getElementById('rag-status').style.color = s.indexed ? 'var(--fg)' : '';
document.getElementById('rag-sent-count').textContent = s.sentences;
document.getElementById('rag-term-count').textContent = s.uniqueTerms;
// Render source chips
const srcEl = document.getElementById('rag-sources');
srcEl.innerHTML = ragSources.map(s => `<span class="rag-source-chip">${esc(s)}</span>`).join('');
}
window.ragIndex = function() {
const text = document.getElementById('rag-text').value.trim();
if (!text) { log('RAG: no text to index','w'); return; }
const result = ragEngine.addText(text);
ragSources.push('manual-' + ragSources.length);
document.getElementById('rag-text').value = '';
refreshRagUI();
log(`πŸ”Ž RAG indexed: ${result.sentences} sent, ${result.uniqueTerms} terms`, 'g');
};
window.ragIndexFromScrape = function() {
// Index the last scraped content if available
const cards = document.querySelectorAll('.tcard');
let found = false;
for (let i = cards.length - 1; i >= 0; i--) {
const name = cards[i].querySelector('.tname');
if (name && name.textContent.trim() === 'scrape') {
const res = cards[i].querySelector('.tres');
if (res && res.textContent.length > 20) {
const result = ragEngine.addText(res.textContent);
ragSources.push('scrape-result');
refreshRagUI();
log(`πŸ”Ž RAG indexed scraped content: ${result.sentences} sent`, 'g');
found = true;
break;
}
}
}
if (!found) log('RAG: no scraped content found to index','w');
};
window.ragIndexFromMemory = function() {
if (mem.user && mem.user.length > 20) {
const result = ragEngine.addText(mem.user);
ragSources.push('user-memory');
refreshRagUI();
log(`πŸ”Ž RAG indexed user memory: ${result.sentences} sent`, 'g');
} else {
log('RAG: user memory too short to index','w');
}
};
window.ragClear = function() {
ragEngine.clear();
ragSources = [];
document.getElementById('rag-results').innerHTML = '';
refreshRagUI();
log('πŸ”Ž RAG index cleared','w');
};
window.ragSearch = function() {
const query = document.getElementById('rag-query').value.trim();
if (!query) { log('RAG: no query','w'); return; }
if (!ragEngine.indexed) { log('RAG: nothing indexed yet','w'); return; }
const result = ragEngine.query(query, 5, 1);
const el = document.getElementById('rag-results');
if (result.passages.length === 0) {
el.innerHTML = '<div style="color:var(--fg-dim);font-style:italic">No passages found.</div>';
return;
}
el.innerHTML = result.passages.map((p, i) =>
`<div class="rag-passage"><div class="rag-score">#${i+1} score: ${p.score.toFixed(2)}</div><div class="rag-text">${esc(p.text)}</div></div>`
).join('');
log(`πŸ”Ž RAG: ${result.passages.length} passages for "${query}"`, 'g');
};
window.ragInjectPrompt = function() {
const query = document.getElementById('rag-query').value.trim();
if (!query) { log('RAG: no query for prompt','w'); return; }
if (!ragEngine.indexed) { log('RAG: nothing indexed','w'); return; }
const result = ragEngine.query(query, 5, 1);
if (result.passages.length === 0) { log('RAG: no passages to inject','w'); return; }
// Inject RAG context into the chat input
const inp = document.getElementById('uin');
inp.value = result.prompt;
inp.style.height = 'auto';
inp.style.height = Math.min(inp.scrollHeight, 300) + 'px';
inp.focus();
log(`πŸ”Ž RAG prompt injected (${result.passages.length} passages)`, 'g');
};
// Allow Enter in RAG query to trigger search
document.getElementById('rag-query').addEventListener('keydown', function(e) {
if (e.key === 'Enter') { e.preventDefault(); ragSearch(); }
});
// Auto network scan on init
toolNetworkScan().catch(()=>{});
</script>
</body>
</html>