| | <!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>
|
| |
|
| |
|
| | <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>
|
| |
|
| | <div id="irow">
|
| | <span class="ipfx">>_</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>
|
| |
|
| |
|
| | <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>
|
| |
|
| |
|
| | <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>
|
| |
|
| |
|
| | <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>
|
| |
|
| |
|
| | <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>
|
| |
|
| |
|
| | <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();
|
| |
|
| | 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";
|
| |
|
| |
|
| |
|
| |
|
| | 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`,
|
| | };
|
| |
|
| |
|
| |
|
| |
|
| | 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); }
|
| |
|
| |
|
| |
|
| |
|
| | 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}`;
|
| | }
|
| |
|
| |
|
| |
|
| |
|
| | 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;
|
| | }
|
| | }
|
| | }
|
| |
|
| |
|
| |
|
| |
|
| | 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;
|
| | }
|
| | }
|
| |
|
| |
|
| |
|
| |
|
| | async function toolSearch(query) {
|
| | stats.srch++; log(`π search: "${query}"`, 't');
|
| |
|
| |
|
| | 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'); }
|
| | }
|
| |
|
| |
|
| | 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'); }
|
| |
|
| |
|
| | 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||''}`);
|
| |
|
| | 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'); }
|
| |
|
| |
|
| | 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;
|
| |
|
| |
|
| |
|
| | const mdLinkRe = /\[([^\]]+)\]\(([^)]+)\)/g;
|
| | while ((m = mdLinkRe.exec(bodyStr)) && results.length < 10) {
|
| | let title = m[1].trim();
|
| | let url = m[2].trim();
|
| |
|
| |
|
| | if (url.includes('uddg=')) {
|
| | const uddg = url.match(/uddg=([^&]+)/);
|
| | if (uddg) url = decodeURIComponent(uddg[1]);
|
| | }
|
| |
|
| |
|
| | if (!url.startsWith('http')) continue;
|
| | if (url.includes('duckduckgo.com')) continue;
|
| | if (title.length < 3) continue;
|
| | if (results.some(r => r.url === url)) continue;
|
| |
|
| |
|
| | 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 });
|
| | }
|
| |
|
| |
|
| | 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');
|
| |
|
| | 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);
|
| |
|
| |
|
| | const ct = r.headers?.get('content-type') || '';
|
| | if (!viaProxy || !ct.includes('markdown')) {
|
| |
|
| | 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(/ /g,' ').replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/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');
|
| |
|
| | 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`;
|
| | }
|
| | }
|
| |
|
| |
|
| |
|
| |
|
| | 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;
|
| |
|
| |
|
| | 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);
|
| | }
|
| |
|
| |
|
| |
|
| |
|
| | 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));
|
| | }
|
| | }
|
| |
|
| |
|
| |
|
| |
|
| | 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};
|
| | }
|
| |
|
| |
|
| |
|
| |
|
| | 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();
|
| | }
|
| |
|
| |
|
| |
|
| |
|
| | 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,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"');}
|
| | function scroll(){const m=document.getElementById('msgs');m.scrollTop=m.scrollHeight;}
|
| |
|
| |
|
| |
|
| |
|
| | 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);
|
| |
|
| | 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);
|
| | }
|
| |
|
| |
|
| |
|
| |
|
| | 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');};
|
| |
|
| |
|
| |
|
| |
|
| | 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();
|
| | }
|
| |
|
| |
|
| |
|
| |
|
| | 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('');
|
| | }
|
| |
|
| |
|
| |
|
| |
|
| | 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);
|
| |
|
| |
|
| |
|
| |
|
| | 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 = [];
|
| |
|
| | inputHistory.forEach(h => items.push({ badge: 'HISTORY', label: h, action: () => { document.getElementById('uin').value = h; closePalette(); } }));
|
| |
|
| | ['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(); } });
|
| | });
|
| |
|
| | Object.keys(mem.skills).forEach(s => {
|
| | items.push({ badge: 'SKILL', label: s, action: () => { document.getElementById('uin').value = `Use skill: ${s}`; closePalette(); } });
|
| | });
|
| |
|
| | 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);
|
| | });
|
| | }
|
| |
|
| |
|
| |
|
| |
|
| | 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;
|
| | }
|
| |
|
| |
|
| | 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';
|
| | }
|
| | });
|
| |
|
| |
|
| |
|
| |
|
| | 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');}});
|
| |
|
| |
|
| | 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;
|
| |
|
| |
|
| |
|
| |
|
| | 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','');
|
| |
|
| |
|
| |
|
| |
|
| | 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;
|
| |
|
| | 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() {
|
| |
|
| | 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; }
|
| |
|
| | 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');
|
| | };
|
| |
|
| |
|
| | document.getElementById('rag-query').addEventListener('keydown', function(e) {
|
| | if (e.key === 'Enter') { e.preventDefault(); ragSearch(); }
|
| | });
|
| |
|
| |
|
| | toolNetworkScan().catch(()=>{});
|
| | </script>
|
| | </body>
|
| | </html>
|
| |
|