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