| <!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>
|
|
|