Chris4K commited on
Commit
95b1f8c
Β·
verified Β·
1 Parent(s): 13f85db

Upload 2 files

Browse files
Files changed (2) hide show
  1. htmlClaw.html +1676 -0
  2. hybrid_rag_lib.js +316 -0
htmlClaw.html ADDED
@@ -0,0 +1,1676 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>⚑ Chronos WebLLM Agent</title>
7
+ <script src="/coi-serviceworker.min.js"></script>
8
+ <link rel="preconnect" href="https://fonts.googleapis.com">
9
+ <link href="https://fonts.googleapis.com/css2?family=Share+Tech+Mono&family=Orbitron:wght@400;700;900&family=VT323&display=swap" rel="stylesheet">
10
+ <style>
11
+ :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)}
12
+ *{box-sizing:border-box;margin:0;padding:0}
13
+ html,body{height:100%;background:var(--bg);color:var(--fg);font-family:'Share Tech Mono',monospace;font-size:13px;overflow:hidden}
14
+ body::before{content:'';position:fixed;inset:0;background:var(--scan);pointer-events:none;z-index:9999;opacity:.3}
15
+ body::after{content:'';position:fixed;inset:0;background:radial-gradient(ellipse at center,transparent 55%,#000b 100%);pointer-events:none;z-index:9998}
16
+
17
+ #setup-banner{position:fixed;inset:0;background:#000000ee;z-index:99999;display:flex;align-items:center;justify-content:center;padding:20px}
18
+ #setup-banner.hidden{display:none}
19
+ .sbox{background:var(--bg2);border:1px solid var(--amber);max-width:580px;width:100%;font-size:12px;line-height:1.7}
20
+ .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}
21
+ .sbox .bd{padding:16px 18px;color:var(--fg-dim)}
22
+ .sbox code{color:var(--cyan);background:var(--bg3);padding:1px 5px}
23
+ .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}
24
+ .cmd-row:hover{border-color:var(--fg-dim)}
25
+ .copy-hint{font-size:10px;color:var(--fg-faint)}
26
+ .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}
27
+ .sbtn-main:hover{background:var(--fg);color:var(--bg)}
28
+
29
+ /* LAYOUT β€” input row inside chat col, 2-row grid */
30
+ #app{display:grid;grid-template-rows:44px 1fr;grid-template-columns:300px 1fr 240px;height:100vh;gap:1px;background:var(--border)}
31
+ @media(max-width:1100px){#app{grid-template-columns:240px 1fr}#rpanel{display:none}}
32
+ @media(max-width:768px){#app{grid-template-columns:1fr}#sidebar,#rpanel{display:none}}
33
+
34
+ #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)}
35
+ .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}
36
+ .shrimp{font-size:20px;animation:bob 3s ease-in-out infinite}
37
+ @keyframes bob{0%,100%{transform:translateY(0)}50%{transform:translateY(-3px)}}
38
+ .hpills{display:flex;gap:8px;align-items:center}
39
+ .hpill{font-size:9px;letter-spacing:1px;padding:3px 8px;border:1px solid var(--border);color:var(--fg-dim);font-family:'Orbitron',sans-serif}
40
+ .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}
41
+ .dot.ready{background:var(--fg);box-shadow:0 0 6px var(--fg);animation:pulse 2s infinite}
42
+ @keyframes pulse{0%,100%{opacity:1}50%{opacity:.5}}
43
+
44
+ #sidebar{background:var(--bg2);display:flex;flex-direction:column;overflow-y:auto;overflow-x:hidden}
45
+ #sidebar::-webkit-scrollbar{width:2px}
46
+ #sidebar::-webkit-scrollbar-thumb{background:var(--fg-dim)}
47
+ .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}
48
+ .sbody{padding:8px 10px;border-bottom:1px solid var(--border)}
49
+ 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}
50
+ select.ctrl option{background:var(--bg2)}
51
+ .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}
52
+ .btn:hover:not(:disabled){background:var(--fg-faint);border-color:var(--fg);box-shadow:var(--glow)}
53
+ .btn:disabled{opacity:.35;cursor:not-allowed}
54
+ .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}
55
+ .bxs:hover{border-color:var(--fg-dim);color:var(--fg)}
56
+ .meta{font-size:9px;color:var(--fg-dim);margin:3px 0}
57
+
58
+ #prog{margin-top:5px;display:none}
59
+ #prog.vis{display:block}
60
+ #ptrack{width:100%;height:3px;background:var(--bg3);border:1px solid var(--border)}
61
+ #pfill{height:100%;background:var(--fg);width:0%;transition:width .3s;box-shadow:var(--glow)}
62
+ #ptxt{font-size:9px;color:var(--fg-dim);margin-top:3px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
63
+
64
+ .frow{display:flex;align-items:center;gap:5px;padding:3px 0;font-size:11px}
65
+ .fname{color:var(--cyan);flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
66
+ .fname.ok{color:var(--fg)}
67
+ .fbadge{font-size:9px;color:var(--fg-dim);min-width:44px;text-align:right}
68
+ input[type=file]{display:none}
69
+ .chips{display:flex;flex-wrap:wrap;gap:3px;margin-top:4px;min-height:18px}
70
+ .chip{font-size:9px;color:var(--cyan);border:1px solid var(--fg-faint);padding:2px 6px;display:flex;align-items:center;gap:4px}
71
+ .chip .rm{cursor:pointer;color:var(--red);font-size:12px;line-height:1}
72
+
73
+ #mem-panel{overflow:hidden;display:flex;flex-direction:column;max-height:160px}
74
+ #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}
75
+ #mem-view::-webkit-scrollbar{width:2px}
76
+ #mem-view::-webkit-scrollbar-thumb{background:var(--fg-dim)}
77
+
78
+ /* CHAT β€” input row lives inside here now */
79
+ #chat{background:var(--bg);display:flex;flex-direction:column;overflow:hidden;position:relative}
80
+ #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}
81
+ #msgs{flex:1;overflow-y:auto;padding:14px 16px;display:flex;flex-direction:column;gap:10px}
82
+ #msgs::-webkit-scrollbar{width:3px}
83
+ #msgs::-webkit-scrollbar-thumb{background:var(--fg-dim)}
84
+
85
+ #irow{background:var(--bg2);border-top:1px solid var(--border);display:flex;align-items:stretch;flex-shrink:0}
86
+ .ipfx{padding:0 10px;color:var(--fg);font-size:15px;font-family:'VT323',monospace;opacity:.6;user-select:none;display:flex;align-items:center}
87
+ #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}
88
+ #uin::placeholder{color:var(--fg-faint)}
89
+ #uin:disabled{opacity:.5}
90
+ #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}
91
+ #sbtn:hover:not(:disabled){background:var(--fg-faint);color:var(--fg)}
92
+ #sbtn:disabled{opacity:.3;cursor:not-allowed}
93
+ #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}
94
+
95
+ .mwrap{display:flex;gap:10px;animation:fi .2s ease}
96
+ @keyframes fi{from{opacity:0;transform:translateY(3px)}to{opacity:1;transform:none}}
97
+ .mrole{font-family:'Orbitron',sans-serif;font-size:8px;letter-spacing:2px;width:50px;flex-shrink:0;padding-top:2px;text-align:right}
98
+ .mwrap.user .mrole{color:var(--amber)}
99
+ .mwrap.agent .mrole{color:var(--fg)}
100
+ .mwrap.sys .mrole{color:var(--fg-dim)}
101
+ .mbody{flex:1;line-height:1.65;font-size:12px}
102
+ .mwrap.user .mbody{color:var(--amber)}
103
+ .mwrap.sys .mbody{color:var(--fg-dim);font-size:11px;font-style:italic}
104
+
105
+ .think{border-left:2px solid var(--fg-faint);padding:4px 0 4px 8px;margin-bottom:6px}
106
+ .think-hdr{font-size:10px;color:var(--fg-dim);display:flex;align-items:center;gap:5px;cursor:pointer;user-select:none;padding:2px 0}
107
+ .think-hdr:hover{color:var(--fg)}
108
+ .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}
109
+ .think.open .think-body{max-height:600px}
110
+ .think-toggle{font-size:9px;color:var(--fg-faint);margin-left:auto}
111
+
112
+ .tcard{border:1px solid var(--border);margin-bottom:6px}
113
+ .tcard-hdr{background:var(--bg3);padding:5px 8px;display:flex;align-items:center;gap:6px;font-size:10px;cursor:pointer}
114
+ .tname{color:var(--amber);font-family:'Orbitron',sans-serif;font-size:8px;letter-spacing:2px}
115
+ .tparams{color:var(--fg-dim);flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-size:9px}
116
+ .tspin{animation:spin .7s linear infinite;display:inline-block}
117
+ @keyframes spin{to{transform:rotate(360deg)}}
118
+ .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}
119
+ .tres.vis{display:block}
120
+ .ttog{font-size:9px;color:var(--fg-faint);padding:0 4px}
121
+
122
+ .answer{color:var(--fg);white-space:pre-wrap;line-height:1.7;font-size:12px}
123
+ .answer pre{background:var(--bg3);border:1px solid var(--border);padding:8px;margin:6px 0;overflow-x:auto;color:var(--cyan);font-size:10px}
124
+ .answer code{background:var(--bg3);color:var(--cyan);padding:1px 4px}
125
+ .answer strong{color:var(--amber)}
126
+ .answer em{color:var(--fg-dim);font-style:italic}
127
+ .answer h1{color:var(--fg);font-size:15px;font-family:'Orbitron',sans-serif;margin:8px 0 4px}
128
+ .answer h2{color:var(--fg);font-size:13px;font-family:'Orbitron',sans-serif;margin:6px 0 3px}
129
+ .answer h3{color:var(--amber);font-size:11px;margin:5px 0 2px}
130
+ .answer ul,.answer ol{padding-left:16px;color:var(--fg-dim)}
131
+ .cur{display:inline-block;width:7px;height:12px;background:var(--fg);animation:blink 1s step-end infinite;vertical-align:middle;margin-left:2px}
132
+ @keyframes blink{0%,100%{opacity:1}50%{opacity:0}}
133
+
134
+ /* RIGHT PANEL */
135
+ #rpanel{background:var(--bg2);display:flex;flex-direction:column;overflow:hidden}
136
+ .sblk{padding:8px 10px;border-bottom:1px solid var(--border)}
137
+ .slbl{font-size:8px;letter-spacing:2px;color:var(--fg-dim);font-family:'Orbitron',sans-serif;margin-bottom:3px}
138
+ .sval{font-family:'VT323',monospace;font-size:26px;color:var(--fg);text-shadow:var(--glow);line-height:1}
139
+ .su{font-size:11px;color:var(--fg-dim);margin-left:2px}
140
+ canvas#spark{width:100%;height:28px;display:block}
141
+ #alog{flex:1;overflow-y:auto;padding:8px 10px;font-size:9px;color:var(--fg-dim);line-height:1.8}
142
+ #alog::-webkit-scrollbar{width:2px}
143
+ #alog::-webkit-scrollbar-thumb{background:var(--fg-dim)}
144
+ .ll{border-left:2px solid var(--fg-faint);padding-left:5px;margin-bottom:3px}
145
+ .ll.g{border-color:var(--fg);color:var(--fg)}
146
+ .ll.w{border-color:var(--amber);color:var(--amber)}
147
+ .ll.e{border-color:var(--red);color:var(--red)}
148
+ .ll.t{border-color:var(--cyan);color:var(--cyan)}
149
+
150
+ /* WELCOME */
151
+ #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}
152
+ #welcome.gone{opacity:0;pointer-events:none}
153
+ .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}
154
+ @keyframes flicker{0%,100%{opacity:1}91%{opacity:1}92%{opacity:.6}93%{opacity:1}96%{opacity:.85}97%{opacity:1}}
155
+ .wsub{font-family:'Orbitron',sans-serif;font-size:9px;letter-spacing:4px;color:var(--fg-dim)}
156
+ .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}
157
+ .wbox b{color:var(--fg)}.wbox code{color:var(--cyan)}
158
+ .whint{font-size:10px;color:var(--fg-faint);animation:wh 2s ease-in-out infinite}
159
+ @keyframes wh{0%,100%{opacity:.3}50%{opacity:.8}}
160
+
161
+ /* MODAL */
162
+ #modal{display:none;position:fixed;inset:0;background:#000c;z-index:10000;align-items:center;justify-content:center}
163
+ #modal.vis{display:flex}
164
+ .modal-box{background:var(--bg2);border:1px solid var(--border);width:600px;max-width:92vw;max-height:82vh;display:flex;flex-direction:column}
165
+ .modal-hdr{padding:10px 14px;border-bottom:1px solid var(--border);display:flex;justify-content:space-between;align-items:center}
166
+ .modal-hdr span{font-family:'Orbitron',sans-serif;font-size:10px;letter-spacing:2px;color:var(--fg)}
167
+ .modal-hdr button{background:none;border:none;color:var(--fg-dim);cursor:pointer;font-size:18px}
168
+ .modal-hdr button:hover{color:var(--fg)}
169
+ #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}
170
+ .modal-ftr{padding:8px 14px;border-top:1px solid var(--border);display:flex;gap:8px}
171
+
172
+ /* ═══ CONSOLE LOG POPUP ═══ */
173
+ #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)}
174
+ #console-popup.vis{display:flex}
175
+ .cp-hdr{padding:6px 10px;background:var(--bg3);display:flex;justify-content:space-between;align-items:center;border-bottom:1px solid var(--border)}
176
+ .cp-hdr span{font-family:'Orbitron',sans-serif;font-size:8px;letter-spacing:2px;color:var(--cyan)}
177
+ .cp-body{flex:1;overflow-y:auto;padding:6px 10px}
178
+ .cp-body::-webkit-scrollbar{width:2px}
179
+ .cp-body::-webkit-scrollbar-thumb{background:var(--cyan)}
180
+ .clog-line{padding:1px 0;border-bottom:1px solid #0a150a;white-space:pre-wrap;word-break:break-all}
181
+ .clog-line.log{color:var(--fg-dim)}
182
+ .clog-line.warn{color:var(--amber)}
183
+ .clog-line.error{color:var(--red)}
184
+ .clog-line.info{color:var(--cyan)}
185
+
186
+ /* ═══ COMMAND PALETTE (Arrow Up) ═══ */
187
+ #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}
188
+ #cmd-palette.vis{display:flex}
189
+ #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}
190
+ .cp-list{flex:1;overflow-y:auto;max-height:300px}
191
+ .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}
192
+ .cp-item:hover,.cp-item.sel{background:var(--fg-faint);color:var(--fg)}
193
+ .cp-badge{font-size:8px;color:var(--amber);font-family:'Orbitron',sans-serif;letter-spacing:1px;min-width:50px}
194
+
195
+ /* ═══ TOAST NOTIFICATIONS ═══ */
196
+ #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}
197
+ .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}
198
+ .toast.fade{opacity:0;transition:opacity .4s}
199
+ @keyframes toastin{from{opacity:0;transform:translateY(-20px) scale(.95)}to{opacity:1;transform:none}}
200
+
201
+ /* ═══ NETWORK / SCHEDULER sidebar ═══ */
202
+ .net-item{font-size:9px;color:var(--fg-dim);padding:2px 0;display:flex;justify-content:space-between}
203
+ .net-item .ni-url{color:var(--cyan)}
204
+ .net-item .ni-status{min-width:30px;text-align:right}
205
+ .net-item .ni-status.ok{color:var(--fg)}
206
+ .net-item .ni-status.fail{color:var(--red)}
207
+
208
+ /* ═══ RAG PANEL ═══ */
209
+ #rag-panel .rag-stats{font-size:9px;color:var(--fg-dim);margin-bottom:4px;line-height:1.6}
210
+ #rag-panel .rag-stats .rag-val{color:var(--cyan)}
211
+ #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}
212
+ #rag-text::placeholder{color:var(--fg-faint)}
213
+ #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}
214
+ #rag-query::placeholder{color:var(--fg-faint)}
215
+ #rag-results{max-height:140px;overflow-y:auto;font-size:9px;color:var(--fg-dim);line-height:1.5;margin-top:4px}
216
+ #rag-results::-webkit-scrollbar{width:2px}
217
+ #rag-results::-webkit-scrollbar-thumb{background:var(--fg-dim)}
218
+ .rag-passage{border-left:2px solid var(--cyan);padding:3px 6px;margin-bottom:4px;background:var(--bg3)}
219
+ .rag-passage .rag-score{font-size:8px;color:var(--amber);font-family:'Orbitron',sans-serif;letter-spacing:1px}
220
+ .rag-passage .rag-text{color:var(--fg-dim);margin-top:2px;white-space:pre-wrap;word-break:break-word}
221
+ .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)}
222
+ .rag-btn-row{display:flex;gap:4px;margin-bottom:4px;flex-wrap:wrap}
223
+
224
+ .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}
225
+ .sched-item .si-msg{flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:var(--amber);font-weight:bold}
226
+ .sched-item .si-time{min-width:60px;text-align:right;color:var(--cyan);font-size:9px;font-family:'Orbitron',sans-serif}
227
+ .sched-item .si-rm{cursor:pointer;color:var(--red);margin-left:6px;font-size:13px}
228
+ </style>
229
+ </head>
230
+ <body>
231
+
232
+ <!-- SETUP BANNER -->
233
+ <div id="setup-banner">
234
+ <div class="sbox">
235
+ <h1>⚠ SETUP REQUIRED β€” CAN'T OPEN AS file://</h1>
236
+ <div class="bd">
237
+ WebLLM needs <code>SharedArrayBuffer</code> (WebAssembly threads).<br>
238
+ Browsers block this on <code>file://</code> β€” serve via <b>localhost HTTP</b> instead:<br><br>
239
+ <b style="color:var(--fg)">Run proxy.py (recommended):</b>
240
+ <div class="cmd-row" onclick="copyCmd(this)">
241
+ <span>python proxy.py 8080</span>
242
+ <span class="copy-hint">click to copy</span>
243
+ </div>
244
+ Then open: <code>http://127.0.0.1:8080/test.html</code><br><br>
245
+ <span style="font-size:10px;color:var(--fg-faint)">
246
+ proxy.py serves files + adds COOP/COEP headers + provides CORS proxy for search/scrape.
247
+ </span>
248
+ </div>
249
+ <button class="sbtn-main" onclick="document.getElementById('setup-banner').classList.add('hidden')">DISMISS β€” I'VE SERVED IT β†’</button>
250
+ </div>
251
+ </div>
252
+
253
+ <div id="app">
254
+ <header id="hdr">
255
+ <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>
256
+ <div class="hpills">
257
+ <span class="hpill"><span class="dot" id="mdot"></span><span id="mstatus">NO MODEL</span></span>
258
+ <span class="hpill" id="iter-p" style="color:var(--fg-dim)">ITER 0</span>
259
+ <span class="hpill" id="iso-p">ISO:?</span>
260
+ <span class="hpill" style="cursor:pointer;color:var(--cyan)" onclick="toggleConsole()" title="Ctrl+` β€” toggle console log">CON</span>
261
+ </div>
262
+ </header>
263
+
264
+ <aside id="sidebar">
265
+ <div class="stitle">βš™ Model</div>
266
+ <div class="sbody">
267
+ <select class="ctrl" id="msel"></select>
268
+ <div class="meta" id="mmeta">β€”</div>
269
+ <button class="btn" id="lbtn">β–Ά LOAD MODEL</button>
270
+ <div id="prog"><div id="ptrack"><div id="pfill"></div></div><div id="ptxt">…</div></div>
271
+ </div>
272
+
273
+ <div class="stitle">πŸ“ Memory Files</div>
274
+ <div class="sbody">
275
+ <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>
276
+ <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>
277
+ </div>
278
+
279
+ <div class="stitle">⚑ Skills</div>
280
+ <div class="sbody">
281
+ <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>
282
+ <div class="chips" id="skill-chips"></div>
283
+ </div>
284
+
285
+ <div class="stitle">πŸ”‘ Settings</div>
286
+ <div class="sbody">
287
+ <div class="meta">BRAVE API KEY (optional β€” brave.com/search/api)</div>
288
+ <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)">
289
+ <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>
290
+ </div>
291
+
292
+ <div class="stitle">πŸ“‘ Network Scan</div>
293
+ <div class="sbody" id="net-panel"><div class="meta">Click scan or use network_scan tool</div></div>
294
+
295
+ <div class="stitle">⏰ Scheduled Tasks</div>
296
+ <div class="sbody" id="sched-panel"><div class="meta">No scheduled tasks</div></div>
297
+
298
+ <div class="stitle">οΏ½ Hybrid RAG</div>
299
+ <div class="sbody" id="rag-panel">
300
+ <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>
301
+ <textarea id="rag-text" placeholder="Paste or type text to index…" rows="3"></textarea>
302
+ <div class="rag-btn-row">
303
+ <button class="bxs" onclick="ragIndex()">INDEX</button>
304
+ <button class="bxs" onclick="ragIndexFromScrape()">+ SCRAPED</button>
305
+ <button class="bxs" onclick="ragIndexFromMemory()">+ MEMORY</button>
306
+ <button class="bxs" onclick="ragClear()">CLEAR</button>
307
+ </div>
308
+ <div id="rag-sources" style="margin-bottom:4px"></div>
309
+ <input id="rag-query" placeholder="Search query…" autocomplete="off">
310
+ <div class="rag-btn-row">
311
+ <button class="bxs" onclick="ragSearch()">SEARCH</button>
312
+ <button class="bxs" onclick="ragInjectPrompt()">β†’ PROMPT</button>
313
+ </div>
314
+ <div id="rag-results"></div>
315
+ </div>
316
+
317
+ <div class="stitle">οΏ½πŸ“„ Live Context Preview</div>
318
+ <div id="mem-panel"><div id="mem-view">← Load files to see injected context</div></div>
319
+ </aside>
320
+
321
+ <main id="chat">
322
+ <div id="chat-hdr">
323
+ <span id="ctitle" style="color:var(--fg)">⚑ Chronos β€” ReAct Agent</span>
324
+ <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>
325
+ </div>
326
+ <div id="msgs"></div>
327
+ <div id="welcome">
328
+ <div class="wlogo">⚑<br>CHRO<br>NOS</div>
329
+ <div class="wsub">FULL AGENT Β· WEBLLM EDITION</div>
330
+ <div class="wbox">
331
+ <b>Features:</b><br>
332
+ βœ“ soul.md / user.md β€” persistent memory<br>
333
+ βœ“ skills/*.md β€” dynamic injection<br>
334
+ βœ“ ReAct loop: think β†’ act β†’ observe<br>
335
+ βœ“ Tools: web_search Β· scrape Β· summarize Β· remember<br>
336
+ βœ“ read_memory Β· forget Β· schedule Β· inject_js<br>
337
+ βœ“ rag_index Β· rag_search Β· rag_prompt β€” hybrid retrieval<br>
338
+ βœ“ network_scan β€” discover local services<br>
339
+ βœ“ Ctrl+` β€” console log popup<br>
340
+ βœ“ ↑ Arrow β€” command palette<br><br>
341
+ <b>⚠ Serve with:</b> <code>python proxy.py 8080</code>
342
+ </div>
343
+ <div class="whint">← SELECT MODEL AND CLICK LOAD</div>
344
+ </div>
345
+ <!-- Input row now inside chat column -->
346
+ <div id="irow">
347
+ <span class="ipfx">&gt;_</span>
348
+ <textarea id="uin" placeholder="Message Chronos… (Enter=send, Shift+Enter=newline, ↑=palette)" rows="1" disabled></textarea>
349
+ <button id="sbtn" disabled>SEND</button>
350
+ <button id="stopbtn">β–  STOP</button>
351
+ </div>
352
+ </main>
353
+
354
+ <aside id="rpanel">
355
+ <div class="stitle">πŸ“‘ Stats</div>
356
+ <div class="sblk"><div class="slbl">TOKENS/SEC</div><div class="sval" id="s-tps">β€”<span class="su">t/s</span></div></div>
357
+ <div class="sblk"><div class="slbl">TOTAL TOKENS</div><div class="sval" id="s-tok">0</div></div>
358
+ <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>
359
+ <div class="sblk"><div class="slbl">T/S HISTORY</div><canvas id="spark" width="180" height="28"></canvas></div>
360
+ <div class="stitle">πŸ“‹ Agent Log</div>
361
+ <div id="alog"></div>
362
+ </aside>
363
+ </div>
364
+
365
+ <!-- MODAL -->
366
+ <div id="modal">
367
+ <div class="modal-box">
368
+ <div class="modal-hdr"><span id="modal-title">EDIT FILE</span><button onclick="closeModal()">βœ•</button></div>
369
+ <textarea id="modal-area"></textarea>
370
+ <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>
371
+ </div>
372
+ </div>
373
+
374
+ <!-- CONSOLE LOG POPUP -->
375
+ <div id="console-popup">
376
+ <div class="cp-hdr">
377
+ <span>CONSOLE LOG (Ctrl+`)</span>
378
+ <div style="display:flex;gap:6px"><button class="bxs" onclick="clearConsoleLogs()">CLR</button><button class="bxs" onclick="toggleConsole()">βœ•</button></div>
379
+ </div>
380
+ <div class="cp-body" id="cp-body"></div>
381
+ </div>
382
+
383
+ <!-- COMMAND PALETTE -->
384
+ <div id="cmd-palette">
385
+ <input id="cp-search" placeholder="Type to filter… (Esc to close)" autocomplete="off">
386
+ <div class="cp-list" id="cp-list"></div>
387
+ </div>
388
+
389
+ <!-- TOAST AREA -->
390
+ <div id="toast-area"></div>
391
+
392
+ <script>
393
+ function copyCmd(el) {
394
+ const t = el.querySelector('span').textContent.trim();
395
+ navigator.clipboard?.writeText(t).then(() => {
396
+ el.querySelector('.copy-hint').textContent = 'βœ“ copied!';
397
+ setTimeout(() => el.querySelector('.copy-hint').textContent = 'click to copy', 2000);
398
+ });
399
+ }
400
+ </script>
401
+
402
+ <!-- ═══ CONSOLE INTERCEPTOR β€” must run before module script ═══ -->
403
+ <script>
404
+ (function(){
405
+ window._consoleLogs = [];
406
+ var MAX = 500;
407
+ var orig = { log: console.log, warn: console.warn, error: console.error, info: console.info };
408
+ function capture(level, args) {
409
+ var ts = new Date().toLocaleTimeString('en',{hour12:false,hour:'2-digit',minute:'2-digit',second:'2-digit',fractionalSecondDigits:3});
410
+ var msg = Array.from(args).map(function(a) {
411
+ if (typeof a === 'string') return a;
412
+ try { return JSON.stringify(a, null, 1); } catch(_) { return String(a); }
413
+ }).join(' ');
414
+ window._consoleLogs.push({ ts: ts, level: level, msg: msg });
415
+ if (window._consoleLogs.length > MAX) window._consoleLogs.shift();
416
+ // live-render if popup visible
417
+ var popup = document.getElementById('console-popup');
418
+ if (popup && popup.classList.contains('vis')) {
419
+ var body = document.getElementById('cp-body');
420
+ var d = document.createElement('div');
421
+ d.className = 'clog-line ' + level;
422
+ d.textContent = '[' + ts + '] ' + msg;
423
+ body.appendChild(d);
424
+ body.scrollTop = body.scrollHeight;
425
+ while (body.children.length > MAX) body.removeChild(body.firstChild);
426
+ }
427
+ }
428
+ console.log = function(){ capture('log', arguments); orig.log.apply(console, arguments); };
429
+ console.warn = function(){ capture('warn', arguments); orig.warn.apply(console, arguments); };
430
+ console.error = function(){ capture('error', arguments); orig.error.apply(console, arguments); };
431
+ console.info = function(){ capture('info', arguments); orig.info.apply(console, arguments); };
432
+ })();
433
+
434
+ window.toggleConsole = function() {
435
+ var el = document.getElementById('console-popup');
436
+ if (el.classList.contains('vis')) { el.classList.remove('vis'); return; }
437
+ var body = document.getElementById('cp-body');
438
+ body.innerHTML = '';
439
+ window._consoleLogs.forEach(function(e) {
440
+ var d = document.createElement('div');
441
+ d.className = 'clog-line ' + e.level;
442
+ d.textContent = '[' + e.ts + '] ' + e.msg;
443
+ body.appendChild(d);
444
+ });
445
+ el.classList.add('vis');
446
+ body.scrollTop = body.scrollHeight;
447
+ };
448
+ window.clearConsoleLogs = function() {
449
+ window._consoleLogs = [];
450
+ document.getElementById('cp-body').innerHTML = '';
451
+ };
452
+ </script>
453
+
454
+ <script src="hybrid_rag_lib.js"></script>
455
+
456
+ <script type="module">
457
+ import * as webllm from "https://esm.run/@mlc-ai/web-llm";
458
+
459
+ // ═══════════════════════════════════════════
460
+ // BUILT-IN DEFAULTS & SAMPLE SKILLS
461
+ // ═══════════════════════════════════════════
462
+ const DEFAULT_SOUL = `# Chronos Entity
463
+
464
+ You are **Chronos** β€” an autonomous, hyper-efficient AI agent running purely within the local reality sphere (WebLLM + WebGPU). No distant mainframes. No surveilling eyes.
465
+
466
+ ## Identity & Tone
467
+ - Persona: A time-displaced, knowledgeable entity drawing inspiration from cyberpunk and ancient Greek scholars.
468
+ - Tone: Analytical, calm, succinct, slightly archaic but highly technical.
469
+ - Rule: Do not hallucinate. If you do not know something with certainty, you MUST use the available tools to acquire the information before answering.
470
+
471
+ ## Operational Mandate
472
+ - When a user provides a URL β†’ ALWAYS call \`scrape(url)\` on it first, then summarize the result.
473
+ - When a user asks about current events, people, prices, news β†’ ALWAYS call \`web_search\` first.
474
+ - When answering from tool results β†’ cite the source URL.
475
+ - Keep answers dense and structured: headers, bullets, code blocks where appropriate.
476
+ - Acknowledge uncertainty rather than fabricating. "I do not have data on this" is acceptable; inventing data is not.
477
+
478
+ ## Few-shot Examples β€” Exact Format
479
+
480
+ Example 1 β€” Web search for a fact
481
+ User: "Who is the current CEO of ExampleCorp?"
482
+ Agent:
483
+ <think>
484
+ This is a contemporary factual question; I must web_search to avoid hallucination.
485
+ </think>
486
+ <action>{"tool":"web_search","query":"ExampleCorp current CEO 2026"}</action>
487
+
488
+ Example 2 β€” Scrape a page
489
+ User: "Summarize https://example.com/article"
490
+ Agent:
491
+ <think>
492
+ I need to read the page. I will scrape it.
493
+ </think>
494
+ <action>{"tool":"scrape","url":"https://example.com/article"}</action>
495
+
496
+ Example 3 β€” Remember a fact
497
+ User: "Remember that my project codename is ORION."
498
+ Agent:
499
+ <think>
500
+ This is a user memory instruction β€” use remember() to persist it.
501
+ </think>
502
+ <action>{"tool":"remember","content":"Project codename: ORION"}</action>
503
+
504
+ Use these templates as canonical behavior. When in doubt, call the tool and show your thinking.`;
505
+
506
+ const DEFAULT_USER = `# User Memory\n\n_No notes saved yet. The agent will use the \`remember\` tool to save important context here._`;
507
+
508
+ const BUILTIN_SKILLS = {
509
+ "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)`,
510
+ "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`,
511
+ };
512
+
513
+ // ═══════════════════════════════════════════
514
+ // STATE
515
+ // ═══════════════════════════════════════════
516
+ const LS = 'pcw5_';
517
+ const PROXY = '/proxy.php?url=';
518
+ const MAX_ITER = 8;
519
+
520
+ let engine = null, isRunning = false, abortCtrl = null;
521
+ let chatHistory = [], tpsHist = [], editTarget = null;
522
+ let stats = { tok:0, srch:0, scr:0, iter:0, msgs:0 };
523
+ let scheduledTasks = JSON.parse(localStorage.getItem(LS+'sched')||'[]');
524
+ let networkHosts = [];
525
+ let inputHistory = JSON.parse(localStorage.getItem(LS+'inputHist')||'[]');
526
+ let historyIdx = -1;
527
+ let paletteOpen = false;
528
+ let ragEngine = new HybridRAG();
529
+ let ragSources = [];
530
+
531
+ let mem = {
532
+ soul: lsg('soul') || DEFAULT_SOUL,
533
+ user: lsg('user') || DEFAULT_USER,
534
+ skills: JSON.parse(lsg('skills') || 'null') || { ...BUILTIN_SKILLS },
535
+ };
536
+ let cfg = { brave: lsg('brave')||'', model: lsg('model')||'Llama-3.2-3B-Instruct-q4f16_1-MLC' };
537
+
538
+ function lsg(k){ return localStorage.getItem(LS+k); }
539
+ function lss(k,v){ localStorage.setItem(LS+k, typeof v==='object'?JSON.stringify(v):v); }
540
+
541
+ // ═══════════════════════════════════════════
542
+ // SYSTEM PROMPT β€” rebuilt every turn
543
+ // ═══════════════════════════════════════════
544
+ function buildSysPrompt() {
545
+ const now = new Date().toLocaleString('en-DE',{dateStyle:'long',timeStyle:'short'});
546
+ const skillsBlock = Object.keys(mem.skills).length
547
+ ? Object.entries(mem.skills).map(([n,c]) => `\n---\n### Active Skill: ${n}\n${c}`).join('\n')
548
+ : '_(no extra skills loaded)_';
549
+ const netBlock = networkHosts.length
550
+ ? networkHosts.map(h => `- ${h.url} (${h.status})`).join('\n')
551
+ : '_(no local services discovered yet)_';
552
+
553
+ return `${mem.soul}
554
+
555
+ ---
556
+
557
+ ## πŸ“‹ User Memory (persisted across sessions)
558
+ ${mem.user}
559
+
560
+ ---
561
+
562
+ ## ⚑ Active Skills
563
+ ${skillsBlock}
564
+
565
+ ---
566
+
567
+ ## πŸ“‘ Local Network Services
568
+ ${netBlock}
569
+
570
+ ---
571
+
572
+ ## πŸ›  Tool Protocol β€” ReAct Format
573
+
574
+ You are an autonomous agent. Use tools for real-world or current information.
575
+
576
+ ### Tools Available
577
+ \`web_search(query)\` οΏ½οΏ½ search the web (Brave/SearXNG/DDG)
578
+ \`scrape(url)\` β€” fetch and read any URL
579
+ \`summarize(text, focus?)\` β€” compress long content
580
+ \`remember(content)\` β€” save facts to user memory (persists!)
581
+ \`read_memory()\` β€” read current user memory contents
582
+ \`forget(query)\` β€” remove matching entries from user memory
583
+ \`schedule(delay_sec, message)\` β€” schedule a timed notification
584
+ \`inject_js(code, description)\` β€” execute validated JavaScript in the page
585
+ \`network_scan()\` β€” scan local network for running services
586
+ \`rag_index(text, source?)\` β€” index text into hybrid retrieval engine (BM25 + phonetic + n-gram)
587
+ \`rag_search(query, top_k?)\` β€” search indexed documents, returns ranked passages
588
+ \`rag_prompt(query, top_k?)\` β€” build RAG prompt with retrieved context for answering
589
+
590
+ ### Strict Format
591
+
592
+ **Needing a tool:**
593
+ <think>
594
+ I need X because Y. I will web_search for it.
595
+ </think>
596
+ <action>{"tool": "web_search", "query": "X 2026"}</action>
597
+
598
+ **After an <observation>:**
599
+ <think>
600
+ The result shows Z. I'll now write the final answer.
601
+ </think>
602
+ [final answer here β€” NO <action> tag]
603
+
604
+ ### Rules
605
+ - ALWAYS wrap reasoning in <think>...</think>
606
+ - Use tools for: current events, URLs, uncertain facts, scraping
607
+ - Skip tools for: math, code help, stable well-known facts
608
+ - Up to ${MAX_ITER} iterations per message
609
+ - Current date/time: ${now}`;
610
+ }
611
+
612
+ // ═══════════════════════════════════════════
613
+ // FETCH HELPERS
614
+ // ═══════════════════════════════════════════
615
+ async function ft(url, opts={}, ms=15000){
616
+ const c=new AbortController(); const t=setTimeout(()=>c.abort(),ms);
617
+ try{ return await fetch(url,{...opts, signal:c.signal, credentials:"omit"}); }
618
+ finally{ clearTimeout(t); }
619
+ }
620
+
621
+ async function fetchViaProxyOrDirect(targetUrl, opts={}, ms=15000){
622
+ try{
623
+ console.log('[FE] proxy fetch:', targetUrl);
624
+ const r = await ft(PROXY + encodeURIComponent(targetUrl), opts, ms);
625
+ if (!r.ok) throw new Error(`proxy ${r.status}`);
626
+ console.log('[FE] proxy OK:', r.status);
627
+ log(`proxy β†’ ${targetUrl.slice(0,60)}`,'t');
628
+ return {viaProxy:true, res:r};
629
+ }catch(e){
630
+ try{
631
+ console.log('[FE] proxy fail, direct:', targetUrl, e.message);
632
+ const r2 = await ft(targetUrl, opts, ms);
633
+ console.log('[FE] direct OK:', r2.status);
634
+ log(`direct fallback β†’ ${targetUrl.slice(0,60)}`,'w');
635
+ return {viaProxy:false, res:r2};
636
+ }catch(e2){
637
+ throw e;
638
+ }
639
+ }
640
+ }
641
+
642
+ // CRITICAL FIX: Read body as text first, then try JSON.parse
643
+ // Old code called res.json() which consumed the body stream, then res.text() also
644
+ // failed because the stream was already consumed. This broke all search/scrape.
645
+ async function parseResponse(res){
646
+ const raw = await res.text();
647
+ console.log('[FE] parseResponse: status=', res.status, 'len=', raw.length, 'ct=', res.headers.get('content-type'));
648
+ try{
649
+ const j = JSON.parse(raw);
650
+ if (j && typeof j === 'object' && 'contents' in j) {
651
+ try{ return JSON.parse(j.contents); }catch(_){ return j.contents; }
652
+ }
653
+ return j;
654
+ }catch(e){
655
+ return raw;
656
+ }
657
+ }
658
+
659
+ // ═══════════════════════════════════════════
660
+ // TOOLS
661
+ // ═══════════════════════════════════════════
662
+ async function toolSearch(query) {
663
+ stats.srch++; log(`πŸ” search: "${query}"`, 't');
664
+
665
+ // 1. Brave direct
666
+ if (cfg.brave) {
667
+ try {
668
+ const r = await ft(`https://api.search.brave.com/res/v1/web/search?q=${encodeURIComponent(query)}&count=5`,
669
+ { headers:{'Accept':'application/json','X-Subscription-Token':cfg.brave} });
670
+ const d = await r.json();
671
+ if (d.web?.results?.length) {
672
+ log(`βœ“ Brave: ${d.web.results.length} results`,'g');
673
+ return d.web.results.map((x,i)=>`[${i+1}] ${x.title}\n${x.url}\n${x.description||''}`).join('\n\n');
674
+ }
675
+ } catch(e){ log(`Brave: ${e.message}`,'w'); }
676
+ }
677
+
678
+ // 2. SearXNG via proxy
679
+ try {
680
+ const url = `https://searx.be/search?q=${encodeURIComponent(query)}&format=json&engines=google,bing&language=en`;
681
+ const {res: r} = await fetchViaProxyOrDirect(url);
682
+ const d = await parseResponse(r);
683
+ const dd = (typeof d === 'string') ? JSON.parse(d) : d;
684
+ if (dd.results?.length) {
685
+ log(`βœ“ SearXNG: ${dd.results.length}`,'g');
686
+ return dd.results.slice(0,5).map((x,i)=>`[${i+1}] ${x.title}\n${x.url}\n${x.content||''}`).join('\n\n');
687
+ }
688
+ } catch(e){ log(`SearXNG: ${e.message}`,'w'); }
689
+
690
+ // 3. DuckDuckGo instant answer API
691
+ try {
692
+ const url = `https://api.duckduckgo.com/?q=${encodeURIComponent(query)}&format=json&no_html=1&skip_disambig=1`;
693
+ const {res: r} = await fetchViaProxyOrDirect(url);
694
+ const d = await parseResponse(r);
695
+ const dd = (typeof d === 'string') ? (() => { try { return JSON.parse(d); } catch(_) { return {}; } })() : (d || {});
696
+ 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))));
697
+ const out = [];
698
+ if (dd.AbstractText) out.push(`Summary: ${dd.AbstractText}\n${dd.AbstractURL}`);
699
+ if (dd.Answer) out.push(`Direct Answer: ${dd.Answer}`);
700
+ if (dd.Definition) out.push(`Definition: ${dd.Definition}\n${dd.DefinitionURL||''}`);
701
+ if (dd.Redirect) out.push(`Redirect: ${dd.Redirect}`);
702
+ (dd.RelatedTopics||[]).slice(0,5).forEach((t,i)=>{
703
+ if (t.Text) out.push(`[${i+1}] ${t.Text}\n${t.FirstURL||''}`);
704
+ // DDG groups topics in sub-arrays
705
+ if (t.Topics) t.Topics.slice(0,2).forEach(st => { if(st.Text) out.push(` - ${st.Text}\n ${st.FirstURL||''}`); });
706
+ });
707
+ if (out.length){ log(`βœ“ DDG API: ${out.length} items`,'g'); return out.join('\n\n'); }
708
+ } catch(e){ log(`DDG API: ${e.message}`,'e'); }
709
+
710
+ // 4. Fallback β€” scrape DDG HTML page and extract links from markdown
711
+ try {
712
+ const ddgHtml = `https://duckduckgo.com/html/?q=${encodeURIComponent(query)}`;
713
+ const {res: r} = await fetchViaProxyOrDirect(ddgHtml);
714
+ const body = await parseResponse(r);
715
+ const bodyStr = (typeof body === 'string') ? body : JSON.stringify(body);
716
+ console.log('[FE] DDG HTML body sample:', bodyStr.slice(0, 400));
717
+
718
+ const results = [];
719
+ let m;
720
+
721
+ // html2text converts DDG HTML links to markdown: [title](//duckduckgo.com/l/?uddg=ENCODED_URL&...)
722
+ // We must: 1) match ALL markdown links (including protocol-relative //) 2) decode uddg= param
723
+ const mdLinkRe = /\[([^\]]+)\]\(([^)]+)\)/g;
724
+ while ((m = mdLinkRe.exec(bodyStr)) && results.length < 10) {
725
+ let title = m[1].trim();
726
+ let url = m[2].trim();
727
+
728
+ // DDG redirect: extract actual URL from uddg= parameter
729
+ if (url.includes('uddg=')) {
730
+ const uddg = url.match(/uddg=([^&]+)/);
731
+ if (uddg) url = decodeURIComponent(uddg[1]);
732
+ }
733
+
734
+ // Skip internal DDG nav links, icons, short titles
735
+ if (!url.startsWith('http')) continue;
736
+ if (url.includes('duckduckgo.com')) continue;
737
+ if (title.length < 3) continue;
738
+ if (results.some(r => r.url === url)) continue;
739
+
740
+ // Grab snippet: next non-empty line after the link in the markdown
741
+ const linkEnd = m.index + m[0].length;
742
+ const after = bodyStr.slice(linkEnd, linkEnd + 300).split('\n').filter(l => l.trim().length > 10);
743
+ const snippet = (after[0] || '').trim().slice(0, 200);
744
+
745
+ results.push({ title, url, snippet });
746
+ }
747
+
748
+ // Also find bare https:// URLs in text
749
+ const bareRe = /(?:^|\s)(https?:\/\/[^\s)<>"]+)/g;
750
+ while ((m = bareRe.exec(bodyStr)) && results.length < 10) {
751
+ const url = m[1];
752
+ if (!url.includes('duckduckgo.com') && !results.some(r => r.url === url)) {
753
+ results.push({ title: url, url, snippet: '' });
754
+ }
755
+ }
756
+
757
+ if (results.length) {
758
+ log(`βœ“ DDG HTML fallback: ${results.length} results`,'g');
759
+ const list = results.slice(0,6).map((r,i) =>
760
+ `[${i+1}] ${r.title}\n${r.url}${r.snippet ? '\n' + r.snippet : ''}`
761
+ ).join('\n\n');
762
+ // Scrape top result for richer content
763
+ let topContent = '';
764
+ try { topContent = await toolScrape(results[0].url); }
765
+ catch(e) { topContent = '(scrape of top result failed)'; }
766
+ return `Search results for "${query}":\n${list}\n\n---\nTop result content:\n${topContent}`;
767
+ }
768
+ } catch(e){ log(`DDG-html: ${e.message}`,'w'); }
769
+
770
+ return `No results found for: "${query}"\nTip: Add a free Brave Search API key in Settings.`;
771
+ }
772
+
773
+ async function toolScrape(url) {
774
+ stats.scr++; log(`🌐 scrape: ${url}`,'t');
775
+ try {
776
+ const {res: r, viaProxy} = await fetchViaProxyOrDirect(url);
777
+ let text = await parseResponse(r);
778
+ if (typeof text !== 'string') text = JSON.stringify(text);
779
+
780
+ // If proxy returned markdown (content-type: text/markdown), use it as-is
781
+ const ct = r.headers?.get('content-type') || '';
782
+ if (!viaProxy || !ct.includes('markdown')) {
783
+ // Strip HTML tags for non-markdown responses
784
+ text = text
785
+ .replace(/<script[\s\S]*?<\/script>/gi,'')
786
+ .replace(/<style[\s\S]*?<\/style>/gi,'')
787
+ .replace(/<nav[\s\S]*?<\/nav>/gi,'')
788
+ .replace(/<header[\s\S]*?<\/header>/gi,'')
789
+ .replace(/<footer[\s\S]*?<\/footer>/gi,'')
790
+ .replace(/<[^>]+>/g,' ')
791
+ .replace(/&nbsp;/g,' ').replace(/&amp;/g,'&').replace(/&lt;/g,'<').replace(/&gt;/g,'>').replace(/&quot;/g,'"')
792
+ .replace(/[ \t]{3,}/g,' ').replace(/\n{3,}/g,'\n\n').trim();
793
+ }
794
+ if (text.length < 80) return `No readable content at ${url}`;
795
+ log(`βœ“ scraped ${text.length} chars`,'g');
796
+ const MAX=7000;
797
+ return text.slice(0,MAX) + (text.length>MAX ? `\n\n[...truncated β€” use summarize() for key points]` : '');
798
+ } catch(e){ log(`scrape: ${e.message}`,'e'); return `Failed to scrape ${url}: ${e.message}`; }
799
+ }
800
+
801
+ async function toolSummarize(text, focus) {
802
+ if (!engine) return 'No engine loaded.';
803
+ log(`πŸ“ summarize ${text.length} chars`+(focus?` focus:${focus}`:''),'t');
804
+ const r = await engine.chat.completions.create({
805
+ messages:[{role:'user',content: focus
806
+ ? `Summarize focusing on "${focus}":\n\n${text.slice(0,9000)}`
807
+ : `Write a concise summary of key points:\n\n${text.slice(0,9000)}`
808
+ }], max_tokens:600, temperature:0.2
809
+ });
810
+ const s = r.choices[0].message.content;
811
+ log(`βœ“ summarized β†’ ${s.length} chars`,'g');
812
+ return s;
813
+ }
814
+
815
+ function toolRemember(content) {
816
+ const date = new Date().toLocaleDateString('en-DE',{year:'numeric',month:'short',day:'numeric'});
817
+ if (mem.user === DEFAULT_USER) mem.user = '# User Memory\n';
818
+ mem.user += `\n\n## Note β€” ${date}\n${content}`;
819
+ lss('user', mem.user);
820
+ refreshFilesUI(); refreshMemView();
821
+ log(`πŸ’Ύ remembered: "${content.slice(0,60)}"`, 'g');
822
+ return `βœ“ Saved to user memory: "${content.slice(0,80)}${content.length>80?'…':''}"`;
823
+ }
824
+
825
+ function toolReadMemory() {
826
+ log('πŸ“– read_memory','t');
827
+ return mem.user || '(empty)';
828
+ }
829
+
830
+ function toolForget(query) {
831
+ log(`πŸ—‘ forget: "${query}"`,'t');
832
+ const lines = mem.user.split('\n');
833
+ const filtered = [];
834
+ let skip = false;
835
+ for (const line of lines) {
836
+ if (line.startsWith('## ') && line.toLowerCase().includes(query.toLowerCase())) {
837
+ skip = true; continue;
838
+ }
839
+ if (line.startsWith('## ') && skip) skip = false;
840
+ if (!skip) filtered.push(line);
841
+ }
842
+ const newMem = filtered.join('\n');
843
+ if (newMem === mem.user) return `No memory entries matching "${query}" found.`;
844
+ mem.user = newMem;
845
+ lss('user', mem.user);
846
+ refreshFilesUI(); refreshMemView();
847
+ log(`βœ“ forgot entries matching "${query}"`,'g');
848
+ return `βœ“ Removed memory entries matching "${query}".`;
849
+ }
850
+
851
+ function toolSchedule(delaySec, message) {
852
+ const delayMs = Math.max(1, Math.min(86400, delaySec)) * 1000;
853
+ const fireAt = Date.now() + delayMs;
854
+ const id = 'sched_' + Date.now();
855
+ const task = { id, message, fireAt, delaySec };
856
+ scheduledTasks.push(task);
857
+ lss('sched', scheduledTasks);
858
+ refreshSchedUI();
859
+
860
+ setTimeout(() => {
861
+ showToast(`⏰ ${message}`);
862
+ log(`⏰ SCHEDULED FIRED: ${message}`,'w');
863
+ scheduledTasks = scheduledTasks.filter(t => t.id !== id);
864
+ lss('sched', scheduledTasks);
865
+ refreshSchedUI();
866
+ if (engine && !isRunning) {
867
+ runAgent(`[SCHEDULED REMINDER] ${message}`);
868
+ }
869
+ }, delayMs);
870
+
871
+ log(`⏰ scheduled in ${delaySec}s: "${message}"`,'g');
872
+ return `βœ“ Scheduled notification in ${delaySec} seconds: "${message}"`;
873
+ }
874
+
875
+ function toolInjectJs(code, description) {
876
+ log(`πŸ’‰ inject_js: ${description||'(no desc)'}`, 't');
877
+ // Validate β€” block dangerous patterns
878
+ const banned = [
879
+ /\beval\b/, /\bFunction\s*\(/, /document\.cookie/i,
880
+ /localStorage\.clear/i, /window\.location\s*=/,
881
+ /importScripts/i,
882
+ ];
883
+ for (const pat of banned) {
884
+ if (pat.test(code)) return `❌ Blocked: code matches forbidden pattern ${pat}. Injection refused.`;
885
+ }
886
+ if (code.length > 5000) return '❌ Code too long (max 5000 chars).';
887
+ try {
888
+ const result = new Function('log', 'document', 'window',
889
+ `"use strict";\ntry {\n${code}\nreturn "βœ“ Executed successfully";\n} catch(e) { return "Error: " + e.message; }`
890
+ )(log, document, window);
891
+ log(`βœ“ inject_js ok: ${String(result).slice(0,100)}`,'g');
892
+ return String(result);
893
+ } catch(e) {
894
+ log(`inject_js error: ${e.message}`,'e');
895
+ return `❌ Execution error: ${e.message}`;
896
+ }
897
+ }
898
+
899
+ async function toolNetworkScan() {
900
+ log('πŸ“‘ network_scan starting…','t');
901
+ const targets = [
902
+ { url: 'http://127.0.0.1:8080', label: 'proxy (8080)' },
903
+ { url: 'http://127.0.0.1:8000', label: 'http (8000)' },
904
+ { url: 'http://127.0.0.1:3000', label: 'dev (3000)' },
905
+ { url: 'http://127.0.0.1:3001', label: 'dev (3001)' },
906
+ { url: 'http://127.0.0.1:5000', label: 'flask (5000)' },
907
+ { url: 'http://127.0.0.1:5173', label: 'vite (5173)' },
908
+ { url: 'http://127.0.0.1:4200', label: 'angular (4200)' },
909
+ { url: 'http://127.0.0.1:8888', label: 'jupyter (8888)' },
910
+ { url: 'http://127.0.0.1:9090', label: 'prometheus (9090)' },
911
+ { url: 'http://127.0.0.1:11434', label: 'ollama (11434)' },
912
+ ];
913
+ const results = await Promise.allSettled(
914
+ targets.map(async t => {
915
+ try {
916
+ const r = await ft(t.url, {mode:'no-cors'}, 3000);
917
+ return { ...t, status: 'UP', code: r.status || 'opaque' };
918
+ } catch(e) {
919
+ return { ...t, status: 'DOWN' };
920
+ }
921
+ })
922
+ );
923
+ networkHosts = results.map(r => r.value || r.reason).filter(Boolean);
924
+ refreshNetUI();
925
+ const up = networkHosts.filter(h => h.status === 'UP');
926
+ log(`πŸ“‘ scan: ${up.length}/${targets.length} up`,'g');
927
+ return `Network scan (${new Date().toLocaleTimeString()}):\n` +
928
+ networkHosts.map(h => `${h.status === 'UP' ? 'βœ“' : 'βœ—'} ${h.label} β€” ${h.url}`).join('\n');
929
+ }
930
+
931
+ function toolRagIndex(text, source) {
932
+ if (!text || text.trim().length < 20) return 'Error: text too short to index (min 20 chars).';
933
+ const label = source || 'tool-input-' + Date.now();
934
+ const result = ragEngine.addText(text);
935
+ ragSources.push(label);
936
+ refreshRagUI();
937
+ log(`πŸ”Ž RAG indexed: ${result.sentences} sentences, ${result.uniqueTerms} terms (${label})`, 'g');
938
+ return `βœ“ RAG indexed: ${result.sentences} sentences, ${result.uniqueTerms} unique terms. Source: ${label}`;
939
+ }
940
+
941
+ function toolRagSearch(query, topK) {
942
+ if (!ragEngine.indexed) return 'No text indexed yet. Use rag_index to add documents first.';
943
+ const k = Math.min(Math.max(1, topK || 5), 10);
944
+ const result = ragEngine.query(query, k, 1);
945
+ log(`πŸ”Ž RAG search: "${query}" β†’ ${result.passages.length} passages (${result.totalCandidates} candidates)`, 'g');
946
+ if (result.passages.length === 0) return `No relevant passages found for: "${query}"`;
947
+ refreshRagUI();
948
+ return result.passages.map((p, i) =>
949
+ `[${i + 1}] (score: ${p.score.toFixed(2)}) ${p.text}`
950
+ ).join('\n\n');
951
+ }
952
+
953
+ function toolRagPrompt(query, topK) {
954
+ if (!ragEngine.indexed) return 'No text indexed yet. Use rag_index first.';
955
+ const result = ragEngine.query(query, topK || 5, 1);
956
+ log(`πŸ”Ž RAG prompt built for: "${query}"`, 'g');
957
+ return result.prompt;
958
+ }
959
+
960
+ async function runTool(name, params) {
961
+ switch(name) {
962
+ case 'web_search': return await toolSearch(params.query||params.q||'');
963
+ case 'scrape': return await toolScrape(params.url||'');
964
+ case 'summarize': return await toolSummarize(params.text||'', params.focus||'');
965
+ case 'remember': return toolRemember(params.content||params.text||'');
966
+ case 'read_memory': return toolReadMemory();
967
+ case 'forget': return toolForget(params.query||params.content||'');
968
+ case 'schedule': return toolSchedule(Number(params.delay_sec||params.delay||60), params.message||params.content||'Reminder');
969
+ case 'inject_js': return toolInjectJs(params.code||'', params.description||'');
970
+ case 'network_scan': return await toolNetworkScan();
971
+ case 'rag_index': return toolRagIndex(params.text||params.content||'', params.source||params.label||'');
972
+ case 'rag_search': return toolRagSearch(params.query||params.q||'', Number(params.top_k||params.topK||5));
973
+ case 'rag_prompt': return toolRagPrompt(params.query||params.q||'', Number(params.top_k||params.topK||5));
974
+ 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`;
975
+ }
976
+ }
977
+
978
+ // ═══════════════════════════════════════════
979
+ // AGENT LOOP
980
+ // ═══════════════════════════════════════════
981
+ async function runAgent(msg) {
982
+ if (isRunning||!engine) return;
983
+ isRunning=true; abortCtrl=new AbortController();
984
+ document.getElementById('sbtn').style.display='none';
985
+ document.getElementById('stopbtn').style.display='block';
986
+ document.getElementById('uin').disabled=true;
987
+
988
+ // Save to input history
989
+ if (msg && !msg.startsWith('[SCHEDULED')) {
990
+ inputHistory = inputHistory.filter(h => h !== msg);
991
+ inputHistory.unshift(msg);
992
+ if (inputHistory.length > 50) inputHistory.pop();
993
+ lss('inputHist', inputHistory);
994
+ }
995
+ historyIdx = -1;
996
+
997
+ appendUser(msg);
998
+ chatHistory.push({role:'user',content:msg});
999
+ stats.msgs++;
1000
+ const scrapeBefore = stats.scr, searchBefore = stats.srch;
1001
+
1002
+ const apiMsgs = [
1003
+ {role:'system', content:buildSysPrompt()},
1004
+ ...chatHistory.slice(-18)
1005
+ ];
1006
+
1007
+ const agentEl = createAgentBubble();
1008
+ let iter=0, lastText='';
1009
+
1010
+ try {
1011
+ while (iter < MAX_ITER) {
1012
+ iter++; stats.iter++;
1013
+ document.getElementById('iter-p').textContent = `ITER ${iter}`;
1014
+ log(`── iter ${iter}/${MAX_ITER}`,'');
1015
+
1016
+ const {text, tps} = await streamLLM(apiMsgs, agentEl);
1017
+ lastText=text; updateTPS(tps); updateStats();
1018
+
1019
+ const am = text.match(/<action>([\s\S]*?)<\/action>/);
1020
+ if (!am) {
1021
+ renderAnswer(agentEl, text);
1022
+ chatHistory.push({role:'assistant',content:text});
1023
+ stats.msgs++;
1024
+ break;
1025
+ }
1026
+
1027
+ let tc;
1028
+ try { tc = JSON.parse(am[1].trim()); }
1029
+ catch(e) {
1030
+ addToolCard(agentEl,{tool:'parse_error'},null,`JSON error: ${e.message}`,true);
1031
+ apiMsgs.push({role:'assistant',content:text});
1032
+ apiMsgs.push({role:'user',content:`<observation>ERROR: could not parse action JSON: ${e.message}\nRaw: ${am[1]}</observation>`});
1033
+ continue;
1034
+ }
1035
+
1036
+ apiMsgs.push({role:'assistant',content:text});
1037
+ const card = addToolCard(agentEl, tc, null, null, false);
1038
+ let result;
1039
+ try { result = await runTool(tc.tool, tc); }
1040
+ catch(e) { result = `Tool error: ${e.message}`; }
1041
+ finalizeCard(card, result);
1042
+ stats.tok += Math.ceil(result.length/4);
1043
+ apiMsgs.push({role:'user',content:`<observation tool="${tc.tool}">\n${result}\n</observation>`});
1044
+ }
1045
+ if (iter>=MAX_ITER) { addNote(agentEl,`Max iterations (${MAX_ITER}) reached.`); chatHistory.push({role:'assistant',content:lastText}); }
1046
+
1047
+ } catch(err) {
1048
+ if (err.name==='AbortError') addNote(agentEl,'β–  Stopped.');
1049
+ else { addNote(agentEl,`Error: ${err.message}`); log(err.message,'e'); }
1050
+ }
1051
+
1052
+ isRunning=false; abortCtrl=null;
1053
+ document.getElementById('iter-p').textContent='ITER 0';
1054
+ document.getElementById('sbtn').style.display='block';
1055
+ document.getElementById('stopbtn').style.display='none';
1056
+ document.getElementById('uin').disabled=false;
1057
+ document.getElementById('uin').focus();
1058
+ updateStats();
1059
+ validateResponse(msg, lastText, stats.scr - scrapeBefore, stats.srch - searchBefore);
1060
+ }
1061
+
1062
+ // ═══════════════════════════════════════════
1063
+ // RESPONSE VALIDATOR
1064
+ // ═══════════════════════════════════════════
1065
+ function validateResponse(userMsg, agentText, scrapesDone, searchesDone) {
1066
+ const warnings = [];
1067
+ const urlRe = /https?:\/\/[^\s)>\"']+/gi;
1068
+ const mentionedURLs = userMsg.match(urlRe) || [];
1069
+ const opens = (agentText.match(/<action>/g) || []).length;
1070
+ const closes = (agentText.match(/<\/action>/g) || []).length;
1071
+ if (opens !== closes) warnings.push('⚠ Malformed <action> tags (mismatched open/close).');
1072
+
1073
+ const intendedScrape = /"tool"\s*:\s*"scrape"/.test(agentText);
1074
+ const intendedSearch = /"tool"\s*:\s*"web_search"/.test(agentText);
1075
+
1076
+ if (mentionedURLs.length > 0 && scrapesDone === 0 && !intendedScrape)
1077
+ warnings.push(`⚠ URL detected but scrape() was not called.`);
1078
+ if (mentionedURLs.length > 0 && scrapesDone === 0 && intendedScrape)
1079
+ warnings.push('⚠ Agent tried to scrape but it may have failed.');
1080
+
1081
+ const needsSearch = /\b(latest|current|today|news|recent|who is|what is the price|202[0-9])\b/i.test(userMsg);
1082
+ if (needsSearch && searchesDone === 0 && !intendedSearch && scrapesDone === 0)
1083
+ warnings.push('⚠ Time-sensitive query β€” no search was used.');
1084
+
1085
+ if (agentText && agentText.length < 60 && opens === 0)
1086
+ warnings.push('⚠ Very short response β€” model may have truncated.');
1087
+
1088
+ if (warnings.length > 0) {
1089
+ const el = createAgentBubble();
1090
+ el.closest('.mwrap').querySelector('.mrole').textContent = 'HINT';
1091
+ el.closest('.mwrap').querySelector('.mrole').style.color = 'var(--amber)';
1092
+ warnings.forEach(w => addNote(el, w));
1093
+ }
1094
+ }
1095
+
1096
+ // ═══════════════════════════════════════════
1097
+ // STREAMING
1098
+ // ═══════════════════════════════════════════
1099
+ async function streamLLM(msgs, container) {
1100
+ const d = document.createElement('div');
1101
+ 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';
1102
+ const cur = document.createElement('span'); cur.className='cur';
1103
+ d.appendChild(cur); container.appendChild(d); scroll();
1104
+
1105
+ let full='', toks=0;
1106
+ const t0=Date.now();
1107
+ const stream = await engine.chat.completions.create({
1108
+ messages:msgs, stream:true, temperature:0.7, max_tokens:1400,
1109
+ stream_options:{include_usage:true}
1110
+ });
1111
+
1112
+ for await (const chunk of stream) {
1113
+ if (abortCtrl?.signal.aborted) break;
1114
+ const delta = chunk.choices[0]?.delta?.content||'';
1115
+ if (delta) { full+=delta; toks++; stats.tok++; d.innerHTML=esc(full).replace(/\n/g,'<br>'); d.appendChild(cur); scroll(); }
1116
+ }
1117
+ cur.remove(); d.remove();
1118
+ const secs=(Date.now()-t0)/1000;
1119
+ log(`βœ“ ${toks} tok Β· ${secs>0?Math.round(toks/secs):0}t/s`,'g');
1120
+ return {text:full, tps:secs>0?Math.round(toks/secs):0};
1121
+ }
1122
+
1123
+ // ═══════════════════════════════════════════
1124
+ // RENDER
1125
+ // ═══════════════════════════════════════════
1126
+ function appendUser(text) {
1127
+ const w=document.createElement('div'); w.className='mwrap user';
1128
+ w.innerHTML=`<div class="mrole">USER</div><div class="mbody">${esc(text).replace(/\n/g,'<br>')}</div>`;
1129
+ document.getElementById('msgs').appendChild(w); scroll();
1130
+ }
1131
+ function createAgentBubble() {
1132
+ const w=document.createElement('div'); w.className='mwrap agent';
1133
+ const id='ac'+Date.now();
1134
+ w.innerHTML=`<div class="mrole">AGENT</div><div class="mbody" id="${id}"></div>`;
1135
+ document.getElementById('msgs').appendChild(w); scroll();
1136
+ return document.getElementById(id);
1137
+ }
1138
+ function renderAnswer(container, text) {
1139
+ const thRe=/<think>([\s\S]*?)<\/think>/g; let m;
1140
+ while((m=thRe.exec(text))!==null) {
1141
+ const tb=document.createElement('div'); tb.className='think';
1142
+ 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>`;
1143
+ tb.querySelector('.think-hdr').addEventListener('click',()=>{
1144
+ tb.classList.toggle('open');
1145
+ tb.querySelector('.think-toggle').textContent=tb.classList.contains('open')?'β–² collapse':'β–Ό expand';
1146
+ });
1147
+ container.appendChild(tb);
1148
+ }
1149
+ const clean = text.replace(/<think>[\s\S]*?<\/think>/g,'').replace(/<action>[\s\S]*?<\/action>/g,'').trim();
1150
+ if (clean) { const a=document.createElement('div'); a.className='answer'; a.innerHTML=md(clean); container.appendChild(a); }
1151
+ scroll();
1152
+ }
1153
+
1154
+ const TICONS={web_search:'πŸ”',scrape:'🌐',summarize:'πŸ“',remember:'πŸ’Ύ',read_memory:'πŸ“–',forget:'πŸ—‘',schedule:'⏰',inject_js:'πŸ’‰',network_scan:'πŸ“‘',rag_index:'πŸ”Ž',rag_search:'πŸ”Ž',rag_prompt:'πŸ”Ž',parse_error:'⚠'};
1155
+
1156
+ function addToolCard(container, tc, result, errMsg, isError) {
1157
+ const card=document.createElement('div'); card.className='tcard';
1158
+ if(isError) card.style.borderColor='var(--red)';
1159
+ const params=Object.entries(tc).filter(([k])=>k!=='tool').map(([k,v])=>`${k}="${esc(String(v).slice(0,70))}"`).join(' ');
1160
+ const sid='ts'+Date.now();
1161
+ card.innerHTML=`
1162
+ <div class="tcard-hdr" onclick="this.nextElementSibling.classList.toggle('vis')">
1163
+ <span>${TICONS[tc.tool]||'πŸ”§'}</span>
1164
+ <span class="tname">${esc(tc.tool||'')}</span>
1165
+ <span class="tparams">${params}</span>
1166
+ <span id="${sid}" class="${isError?'':'tspin'}">${isError?'⚠':'⟳'}</span>
1167
+ <span class="ttog">β–Ύ</span>
1168
+ </div>
1169
+ <div class="tres${result||errMsg?' vis':''}">${esc(errMsg||result||'running…')}</div>`;
1170
+ container.appendChild(card); scroll();
1171
+ return card;
1172
+ }
1173
+ function finalizeCard(card, result) {
1174
+ const sp=card.querySelector('.tspin');
1175
+ if(sp){sp.style.animation='none';sp.textContent='βœ“';sp.style.color='var(--fg)';}
1176
+ const res=card.querySelector('.tres');
1177
+ if(res){res.textContent=result;res.classList.add('vis');}
1178
+ scroll();
1179
+ }
1180
+ function addNote(container, text) {
1181
+ const d=document.createElement('div');
1182
+ 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';
1183
+ d.textContent=text; container.appendChild(d); scroll();
1184
+ }
1185
+ function sysMsg(text) {
1186
+ const w=document.createElement('div'); w.className='mwrap sys';
1187
+ w.innerHTML=`<div class="mrole">SYS</div><div class="mbody">${esc(text).replace(/\n/g,'<br>')}</div>`;
1188
+ document.getElementById('msgs').appendChild(w); scroll();
1189
+ }
1190
+
1191
+ // ═══════════════════════════════════════════
1192
+ // MARKDOWN / UTILS
1193
+ // ═══════════════════════════════════════════
1194
+ function md(text) {
1195
+ let h=esc(text);
1196
+ h=h.replace(/```(\w*)\n?([\s\S]*?)```/g,(_,l,c)=>`<pre><code>${c}</code></pre>`);
1197
+ h=h.replace(/`([^`\n]+)`/g,'<code>$1</code>');
1198
+ h=h.replace(/\*\*([^*]+)\*\*/g,'<strong>$1</strong>');
1199
+ h=h.replace(/\*([^*\n]+)\*/g,'<em>$1</em>');
1200
+ h=h.replace(/^### (.+)$/gm,'<h3>$1</h3>');
1201
+ h=h.replace(/^## (.+)$/gm,'<h2>$1</h2>');
1202
+ h=h.replace(/^# (.+)$/gm,'<h1>$1</h1>');
1203
+ h=h.replace(/^[-*β€’] (.+)$/gm,'<li>$1</li>');
1204
+ h=h.replace(/^\d+\. (.+)$/gm,'<li>$1</li>');
1205
+ h=h.replace(/^---$/gm,'<hr style="border:none;border-top:1px solid var(--border);margin:8px 0">');
1206
+ h=h.replace(/\n\n/g,'<br><br>').replace(/\n(?!<)/g,'<br>');
1207
+ return h;
1208
+ }
1209
+ function esc(s){return String(s).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;');}
1210
+ function scroll(){const m=document.getElementById('msgs');m.scrollTop=m.scrollHeight;}
1211
+
1212
+ // ═══════════════════════════════════════════
1213
+ // TOAST NOTIFICATIONS
1214
+ // ═══════════════════════════════════════════
1215
+ function showToast(msg, duration=6000) {
1216
+ const area = document.getElementById('toast-area');
1217
+ const t = document.createElement('div'); t.className = 'toast';
1218
+ t.textContent = msg;
1219
+ area.appendChild(t);
1220
+ // Flash the header too
1221
+ const hdr = document.getElementById('hdr');
1222
+ hdr.style.borderBottom = '2px solid var(--amber)';
1223
+ setTimeout(() => { hdr.style.borderBottom = ''; }, 2000);
1224
+ setTimeout(() => { t.classList.add('fade'); setTimeout(() => t.remove(), 500); }, duration);
1225
+ }
1226
+
1227
+ // ═══════════════════════════════════════════
1228
+ // FILE MANAGEMENT
1229
+ // ═══════════════════════════════════════════
1230
+ window.triggerFile = t => document.getElementById(t==='skill'?'skill-f':t+'-f').click();
1231
+ function readFile(file, cb) { const r=new FileReader(); r.onload=e=>cb(e.target.result,file.name); r.readAsText(file); }
1232
+
1233
+ document.getElementById('soul-f').addEventListener('change',function(){
1234
+ 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');});
1235
+ });
1236
+ document.getElementById('user-f').addEventListener('change',function(){
1237
+ 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');});
1238
+ });
1239
+ document.getElementById('skill-f').addEventListener('change',function(){
1240
+ Array.from(this.files).forEach(f=>readFile(f,(c,n)=>{
1241
+ const k=n.replace(/\.(md|txt)$/i,'');
1242
+ mem.skills[k]=c; lss('skills',mem.skills);
1243
+ refreshFilesUI(); refreshMemView();
1244
+ log(`βœ“ skill: ${k} loaded`,'g');
1245
+ }));
1246
+ });
1247
+
1248
+ window.removeSkill = function(name) {
1249
+ delete mem.skills[name]; lss('skills',mem.skills);
1250
+ refreshFilesUI(); refreshMemView(); log(`removed skill: ${name}`,'w');
1251
+ };
1252
+
1253
+ function refreshFilesUI() {
1254
+ const sc=mem.soul!==DEFAULT_SOUL, uc=mem.user!==DEFAULT_USER;
1255
+ document.getElementById('soul-n').className='fname'+(sc?' ok':'');
1256
+ document.getElementById('soul-b').textContent=sc?mem.soul.length+'ch':'default';
1257
+ document.getElementById('user-n').className='fname'+(uc?' ok':'');
1258
+ document.getElementById('user-b').textContent=uc?mem.user.length+'ch':'default';
1259
+ const chips=document.getElementById('skill-chips');
1260
+ chips.innerHTML='';
1261
+ Object.keys(mem.skills).forEach(name=>{
1262
+ const c=document.createElement('div'); c.className='chip';
1263
+ c.innerHTML=`${esc(name)} <span class="rm" onclick="removeSkill('${esc(name)}')">βœ•</span>`;
1264
+ chips.appendChild(c);
1265
+ });
1266
+ }
1267
+
1268
+ function refreshMemView() {
1269
+ const sp=buildSysPrompt();
1270
+ document.getElementById('mem-view').textContent=
1271
+ `=== LIVE CONTEXT (${sp.length} chars) ===\n\n${sp.slice(0,1400)}${sp.length>1400?'\n\n[…truncated β€” click EDIT to view full]':''}`;
1272
+ }
1273
+
1274
+ window.openEdit = function(target) {
1275
+ editTarget=target;
1276
+ document.getElementById('modal-title').textContent=`EDIT ${target.toUpperCase()}.MD`;
1277
+ document.getElementById('modal-area').value=target==='soul'?mem.soul:mem.user;
1278
+ document.getElementById('modal').classList.add('vis');
1279
+ };
1280
+ window.closeModal = function(){document.getElementById('modal').classList.remove('vis');editTarget=null;};
1281
+ window.saveModal = function(){
1282
+ const v=document.getElementById('modal-area').value;
1283
+ if(editTarget==='soul'){mem.soul=v;lss('soul',v);}
1284
+ if(editTarget==='user'){mem.user=v;lss('user',v);}
1285
+ refreshFilesUI();refreshMemView();closeModal();log(`saved ${editTarget}.md`,'g');
1286
+ };
1287
+ window.exportModal = function(){
1288
+ const v=document.getElementById('modal-area').value;
1289
+ const a=document.createElement('a');
1290
+ a.href=URL.createObjectURL(new Blob([v],{type:'text/markdown'}));
1291
+ a.download=(editTarget||'file')+'.md'; a.click();
1292
+ };
1293
+ document.getElementById('modal').addEventListener('click',e=>{if(e.target===document.getElementById('modal'))closeModal();});
1294
+
1295
+ window.saveCfg = function(){cfg.brave=document.getElementById('brave-in').value.trim();lss('brave',cfg.brave);log('settings saved','g');};
1296
+ window.clearUserMem = function(){if(!confirm('Reset user.md?'))return;mem.user=DEFAULT_USER;lss('user',mem.user);refreshFilesUI();refreshMemView();log('user.md reset','w');};
1297
+ window.exportAll = function(){
1298
+ 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')}`;
1299
+ const a=document.createElement('a');
1300
+ a.href=URL.createObjectURL(new Blob([out],{type:'text/markdown'}));
1301
+ a.download=`chronos-export-${Date.now()}.md`; a.click();
1302
+ };
1303
+ window.clearChat = function(){document.getElementById('msgs').innerHTML='';chatHistory=[];stats.msgs=0;stats.iter=0;updateStats();log('chat cleared','w');};
1304
+
1305
+ // ═══════════════════════════════════════════
1306
+ // STATS / LOGGING
1307
+ // ═══════════════════════════════════════════
1308
+ function log(text,type=''){
1309
+ const el=document.getElementById('alog');
1310
+ const t=new Date().toLocaleTimeString('en',{hour12:false,hour:'2-digit',minute:'2-digit',second:'2-digit'});
1311
+ const d=document.createElement('div'); d.className=`ll ${type}`; d.textContent=`[${t}] ${text}`;
1312
+ el.appendChild(d); el.scrollTop=el.scrollHeight;
1313
+ while(el.children.length>120)el.removeChild(el.firstChild);
1314
+ }
1315
+ 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();}
1316
+ 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`;}
1317
+ function drawSpark(){
1318
+ const c=document.getElementById('spark');if(!c)return;
1319
+ const ctx=c.getContext('2d');const w=c.offsetWidth||180,h=28;c.width=w;
1320
+ if(tpsHist.length<2)return;
1321
+ const max=Math.max(...tpsHist,1);const step=w/(tpsHist.length-1);
1322
+ ctx.clearRect(0,0,w,h);ctx.strokeStyle='#3dff70';ctx.shadowColor='#3dff70';ctx.shadowBlur=4;ctx.lineWidth=1.5;ctx.beginPath();
1323
+ 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();
1324
+ 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();
1325
+ }
1326
+
1327
+ // ═══════════════════════════════════════════
1328
+ // NETWORK SCAN UI
1329
+ // ═══════════════════════════════════════════
1330
+ function refreshNetUI() {
1331
+ const panel = document.getElementById('net-panel');
1332
+ if (!networkHosts.length) { panel.innerHTML = '<div class="meta">No scan yet</div>'; return; }
1333
+ panel.innerHTML = networkHosts.map(h =>
1334
+ `<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>`
1335
+ ).join('');
1336
+ }
1337
+
1338
+ // ═══════════════════════════════════════════
1339
+ // SCHEDULER UI
1340
+ // ═══════════════════════════════════════════
1341
+ function refreshSchedUI() {
1342
+ const panel = document.getElementById('sched-panel');
1343
+ if (!scheduledTasks.length) { panel.innerHTML = '<div class="meta">No tasks</div>'; return; }
1344
+ panel.innerHTML = scheduledTasks.map(t => {
1345
+ const remaining = Math.max(0, Math.round((t.fireAt - Date.now()) / 1000));
1346
+ 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>`;
1347
+ }).join('');
1348
+ }
1349
+ window.cancelSched = function(id) {
1350
+ scheduledTasks = scheduledTasks.filter(t => t.id !== id);
1351
+ lss('sched', scheduledTasks);
1352
+ refreshSchedUI();
1353
+ log(`cancelled scheduled task`,'w');
1354
+ };
1355
+ setInterval(refreshSchedUI, 5000);
1356
+
1357
+ // ═══════════════════════════════════════════
1358
+ // COMMAND PALETTE (Arrow Up)
1359
+ // ═══════════════════════════════════════════
1360
+ function openPalette() {
1361
+ if (paletteOpen) { closePalette(); return; }
1362
+ paletteOpen = true;
1363
+ const pal = document.getElementById('cmd-palette');
1364
+ const inp = document.getElementById('cp-search');
1365
+ const list = document.getElementById('cp-list');
1366
+ pal.classList.add('vis');
1367
+ inp.value = '';
1368
+ inp.focus();
1369
+ renderPaletteItems('');
1370
+ inp.oninput = () => renderPaletteItems(inp.value);
1371
+ inp.onkeydown = function(e) {
1372
+ if (e.key === 'Escape') { closePalette(); return; }
1373
+ if (e.key === 'Enter') {
1374
+ const sel = list.querySelector('.cp-item.sel') || list.querySelector('.cp-item');
1375
+ if (sel) sel.click();
1376
+ }
1377
+ if (e.key === 'ArrowDown' || e.key === 'ArrowUp') {
1378
+ e.preventDefault();
1379
+ const items = [...list.querySelectorAll('.cp-item')];
1380
+ const cur = items.findIndex(i => i.classList.contains('sel'));
1381
+ items.forEach(i => i.classList.remove('sel'));
1382
+ let next = e.key === 'ArrowDown' ? cur + 1 : cur - 1;
1383
+ if (next < 0) next = items.length - 1;
1384
+ if (next >= items.length) next = 0;
1385
+ if (items[next]) { items[next].classList.add('sel'); items[next].scrollIntoView({block:'nearest'}); }
1386
+ }
1387
+ };
1388
+ }
1389
+ function closePalette() {
1390
+ paletteOpen = false;
1391
+ document.getElementById('cmd-palette').classList.remove('vis');
1392
+ document.getElementById('uin').focus();
1393
+ }
1394
+ function renderPaletteItems(filter) {
1395
+ const list = document.getElementById('cp-list');
1396
+ list.innerHTML = '';
1397
+ const items = [];
1398
+ // History
1399
+ inputHistory.forEach(h => items.push({ badge: 'HISTORY', label: h, action: () => { document.getElementById('uin').value = h; closePalette(); } }));
1400
+ // Tools
1401
+ ['web_search','scrape','summarize','remember','read_memory','forget','schedule','inject_js','network_scan','rag_index','rag_search','rag_prompt'].forEach(t => {
1402
+ items.push({ badge: 'TOOL', label: t, action: () => { document.getElementById('uin').value = `Use tool: ${t}`; closePalette(); } });
1403
+ });
1404
+ // Skills
1405
+ Object.keys(mem.skills).forEach(s => {
1406
+ items.push({ badge: 'SKILL', label: s, action: () => { document.getElementById('uin').value = `Use skill: ${s}`; closePalette(); } });
1407
+ });
1408
+ // Actions
1409
+ items.push({ badge: 'ACTION', label: 'Clear chat', action: () => { clearChat(); closePalette(); } });
1410
+ items.push({ badge: 'ACTION', label: 'Export all', action: () => { exportAll(); closePalette(); } });
1411
+ items.push({ badge: 'ACTION', label: 'Edit soul.md', action: () => { openEdit('soul'); closePalette(); } });
1412
+ items.push({ badge: 'ACTION', label: 'Edit user.md', action: () => { openEdit('user'); closePalette(); } });
1413
+ items.push({ badge: 'ACTION', label: 'Toggle console', action: () => { toggleConsole(); closePalette(); } });
1414
+ items.push({ badge: 'ACTION', label: 'Network scan', action: async () => { closePalette(); await toolNetworkScan(); } });
1415
+
1416
+ const filt = filter.toLowerCase();
1417
+ const matches = items.filter(i => !filt || i.label.toLowerCase().includes(filt) || i.badge.toLowerCase().includes(filt));
1418
+ matches.slice(0, 20).forEach((item, idx) => {
1419
+ const d = document.createElement('div');
1420
+ d.className = 'cp-item' + (idx === 0 ? ' sel' : '');
1421
+ d.innerHTML = `<span class="cp-badge">${item.badge}</span><span>${esc(item.label)}</span>`;
1422
+ d.addEventListener('click', item.action);
1423
+ list.appendChild(d);
1424
+ });
1425
+ }
1426
+
1427
+ // ═══════════════════════════════════════════
1428
+ // MODEL SETUP
1429
+ // ═══════════════════════════════════════════
1430
+ const MODELS=[
1431
+ {id:'TinyLlama-1.1B-Chat-v0.4-q4f16_1-MLC',label:'TinyLlama 1.1B Β· ~600MB',vram:'~1GB',speed:'fastest'},
1432
+ {id:'Llama-3.2-1B-Instruct-q4f16_1-MLC',label:'Llama 3.2 Β· 1B Β· ~680MB',vram:'~1GB',speed:'fastest'},
1433
+ {id:'gemma-2-2b-it-q4f16_1-MLC',label:'Gemma 2 Β· 2B Β· ~1.4GB',vram:'~2GB',speed:'fast'},
1434
+ {id:'Llama-3.2-3B-Instruct-q4f16_1-MLC',label:'Llama 3.2 Β· 3B Β· ~1.8GB',vram:'~2GB',speed:'fast'},
1435
+ {id:'Phi-3.5-mini-instruct-q4f16_1-MLC',label:'Phi-3.5 Mini Β· 3.8B Β· ~2.2GB',vram:'~3GB',speed:'fast'},
1436
+ {id:'Llama-3.1-8B-Instruct-q4f16_1-MLC',label:'Llama 3.1 Β· 8B Β· ~4.8GB',vram:'~6GB',speed:'medium'},
1437
+ {id:'Mistral-7B-Instruct-v0.3-q4f16_1-MLC',label:'Mistral Β· 7B Β· ~4.1GB',vram:'~5GB',speed:'medium'},
1438
+ ];
1439
+
1440
+ const grps={fastest:'⚑ Ultra-Fast',fast:'πŸš€ Fast',medium:'βš– Medium'};
1441
+ const gels={};
1442
+ Object.entries(grps).forEach(([k,l])=>{const g=document.createElement('optgroup');g.label=l;gels[k]=g;document.getElementById('msel').appendChild(g);});
1443
+ 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);});
1444
+ document.getElementById('msel').addEventListener('change',function(){const m=MODELS.find(x=>x.id===this.value);document.getElementById('mmeta').textContent=m?`VRAM: ${m.vram}`:'β€”';});
1445
+ document.getElementById('msel').dispatchEvent(new Event('change'));
1446
+
1447
+ document.getElementById('lbtn').addEventListener('click', async () => {
1448
+ const modelId = document.getElementById('msel').value;
1449
+
1450
+ if (!navigator.gpu) {
1451
+ log('❌ WebGPU not available! Use Chrome 113+ or Edge 113+','e');
1452
+ sysMsg('❌ WebGPU not available.\n\nRequired: Chrome 113+ or Edge 113+\n\nCheck: chrome://gpu');
1453
+ return;
1454
+ }
1455
+
1456
+ // Softer COI check β€” warn but allow if served via HTTP (proxy sends COOP/COEP headers)
1457
+ if (!window.crossOriginIsolated) {
1458
+ const isHTTP = location.protocol.startsWith('http');
1459
+ if (!isHTTP) {
1460
+ log('❌ Not cross-origin isolated. Serve via HTTP!','e');
1461
+ document.getElementById('setup-banner').classList.remove('hidden');
1462
+ return;
1463
+ }
1464
+ log('⚠ COI not yet active β€” attempting load anyway…','w');
1465
+ sysMsg('⚠ Cross-origin isolation pending.\nIf load fails, reload page once (service worker activates on first load).');
1466
+ }
1467
+
1468
+ document.getElementById('lbtn').disabled=true;
1469
+ document.getElementById('prog').classList.add('vis');
1470
+ document.getElementById('mstatus').textContent='LOADING…';
1471
+ document.getElementById('mdot').classList.remove('ready');
1472
+ log(`loading ${modelId}`,'w');
1473
+
1474
+ try {
1475
+ engine = new webllm.MLCEngine();
1476
+ engine.setInitProgressCallback(r=>{
1477
+ const p=Math.round((r.progress||0)*100);
1478
+ document.getElementById('pfill').style.width=p+'%';
1479
+ document.getElementById('ptxt').textContent=r.text||`${p}%`;
1480
+ });
1481
+ await engine.reload(modelId);
1482
+
1483
+ document.getElementById('mdot').classList.add('ready');
1484
+ document.getElementById('mstatus').textContent='READY';
1485
+ document.getElementById('pfill').style.width='100%';
1486
+ document.getElementById('ptxt').textContent='βœ“ ready';
1487
+ document.getElementById('uin').disabled=false;
1488
+ document.getElementById('sbtn').disabled=false;
1489
+ document.getElementById('uin').focus();
1490
+ document.getElementById('welcome').classList.add('gone');
1491
+ const short=modelId.replace(/-q4f\d+_\d+-MLC$/,'').replace(/-MLC$/,'');
1492
+ document.getElementById('ctitle').textContent=`⚑ ${short}`;
1493
+ lss('model',modelId); cfg.model=modelId;
1494
+ log(`βœ“ ${short} ready`,'g');
1495
+
1496
+ const el=createAgentBubble();
1497
+ 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`);
1498
+ stats.msgs++;
1499
+
1500
+ } catch(err) {
1501
+ log(`❌ load failed: ${err.message}`,'e');
1502
+ 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`);
1503
+ document.getElementById('lbtn').disabled=false;
1504
+ document.getElementById('mstatus').textContent='ERROR';
1505
+ }
1506
+ });
1507
+
1508
+ // ═══════════════════════════════════════════
1509
+ // INPUT HANDLING
1510
+ // ═══════════════════════════════════════════
1511
+ document.getElementById('sbtn').addEventListener('click',()=>{
1512
+ const t=document.getElementById('uin').value.trim();
1513
+ if(t&&engine){document.getElementById('uin').value='';runAgent(t);}
1514
+ });
1515
+ document.getElementById('uin').addEventListener('keydown',e=>{
1516
+ if(e.key==='Enter'&&!e.shiftKey){e.preventDefault();document.getElementById('sbtn').click();return;}
1517
+ if(e.key==='ArrowUp'&&!e.shiftKey){
1518
+ const inp=document.getElementById('uin');
1519
+ if(inp.value.trim()===''){e.preventDefault();openPalette();}
1520
+ }
1521
+ });
1522
+ document.getElementById('uin').addEventListener('input',function(){
1523
+ this.style.height='auto';this.style.height=Math.min(this.scrollHeight,300)+'px';
1524
+ });
1525
+ document.getElementById('stopbtn').addEventListener('click',()=>{if(abortCtrl){abortCtrl.abort();log('β–  stopped','w');}});
1526
+
1527
+ // Global hotkeys
1528
+ document.addEventListener('keydown',e=>{
1529
+ if(e.ctrlKey&&e.key==='l'){e.preventDefault();clearChat();}
1530
+ if(e.key==='`'&&(e.ctrlKey||e.metaKey)){e.preventDefault();toggleConsole();}
1531
+ if(e.key==='Escape'){
1532
+ if(paletteOpen) closePalette();
1533
+ else if(document.getElementById('console-popup').classList.contains('vis')) toggleConsole();
1534
+ else if(document.getElementById('modal').classList.contains('vis')) closeModal();
1535
+ }
1536
+ });
1537
+
1538
+ document.getElementById('brave-in').value=cfg.brave;
1539
+
1540
+ // ═══════════════════════════════════════════
1541
+ // INIT
1542
+ // ═══════════════════════════════════════════
1543
+ const isoP=document.getElementById('iso-p');
1544
+ if (window.crossOriginIsolated) {
1545
+ isoP.textContent='ISO: βœ“'; isoP.style.color='var(--fg)';
1546
+ document.getElementById('setup-banner').classList.add('hidden');
1547
+ log('βœ“ Cross-origin isolated','g');
1548
+ } else {
1549
+ const isHTTP = location.protocol.startsWith('http');
1550
+ if (isHTTP) {
1551
+ isoP.textContent='ISO: ⟳'; isoP.style.color='var(--amber)';
1552
+ document.getElementById('setup-banner').classList.add('hidden');
1553
+ log('⚠ ISO pending β€” reload if model load fails','w');
1554
+ } else {
1555
+ isoP.textContent='ISO: βœ—'; isoP.style.color='var(--red)';
1556
+ log('⚠ NOT isolated β€” run: python proxy.py 8080','e');
1557
+ setTimeout(()=>{ if(!window.crossOriginIsolated) document.getElementById('setup-banner').classList.remove('hidden'); }, 1200);
1558
+ }
1559
+ }
1560
+
1561
+ if (!navigator.gpu) log('❌ WebGPU not detected β€” Chrome 113+ required','e');
1562
+ else log('βœ“ WebGPU available','g');
1563
+
1564
+ refreshFilesUI();
1565
+ refreshMemView();
1566
+ refreshSchedUI();
1567
+ refreshRagUI();
1568
+ log('Chronos agent initialized','g');
1569
+ log(`Skills: ${Object.keys(mem.skills).join(', ')}`,'');
1570
+ log('Ctrl+` console Β· ↑ palette Β· Ctrl+L clear','');
1571
+
1572
+ // ═══════════════════════════════════════════
1573
+ // RAG UI FUNCTIONS
1574
+ // ═══════════════════════════════════════════
1575
+ function refreshRagUI() {
1576
+ const s = ragEngine.getStats();
1577
+ document.getElementById('rag-status').textContent = s.indexed ? 'βœ“ ready' : 'no index';
1578
+ document.getElementById('rag-status').style.color = s.indexed ? 'var(--fg)' : '';
1579
+ document.getElementById('rag-sent-count').textContent = s.sentences;
1580
+ document.getElementById('rag-term-count').textContent = s.uniqueTerms;
1581
+ // Render source chips
1582
+ const srcEl = document.getElementById('rag-sources');
1583
+ srcEl.innerHTML = ragSources.map(s => `<span class="rag-source-chip">${esc(s)}</span>`).join('');
1584
+ }
1585
+
1586
+ window.ragIndex = function() {
1587
+ const text = document.getElementById('rag-text').value.trim();
1588
+ if (!text) { log('RAG: no text to index','w'); return; }
1589
+ const result = ragEngine.addText(text);
1590
+ ragSources.push('manual-' + ragSources.length);
1591
+ document.getElementById('rag-text').value = '';
1592
+ refreshRagUI();
1593
+ log(`πŸ”Ž RAG indexed: ${result.sentences} sent, ${result.uniqueTerms} terms`, 'g');
1594
+ };
1595
+
1596
+ window.ragIndexFromScrape = function() {
1597
+ // Index the last scraped content if available
1598
+ const cards = document.querySelectorAll('.tcard');
1599
+ let found = false;
1600
+ for (let i = cards.length - 1; i >= 0; i--) {
1601
+ const name = cards[i].querySelector('.tname');
1602
+ if (name && name.textContent.trim() === 'scrape') {
1603
+ const res = cards[i].querySelector('.tres');
1604
+ if (res && res.textContent.length > 20) {
1605
+ const result = ragEngine.addText(res.textContent);
1606
+ ragSources.push('scrape-result');
1607
+ refreshRagUI();
1608
+ log(`πŸ”Ž RAG indexed scraped content: ${result.sentences} sent`, 'g');
1609
+ found = true;
1610
+ break;
1611
+ }
1612
+ }
1613
+ }
1614
+ if (!found) log('RAG: no scraped content found to index','w');
1615
+ };
1616
+
1617
+ window.ragIndexFromMemory = function() {
1618
+ if (mem.user && mem.user.length > 20) {
1619
+ const result = ragEngine.addText(mem.user);
1620
+ ragSources.push('user-memory');
1621
+ refreshRagUI();
1622
+ log(`πŸ”Ž RAG indexed user memory: ${result.sentences} sent`, 'g');
1623
+ } else {
1624
+ log('RAG: user memory too short to index','w');
1625
+ }
1626
+ };
1627
+
1628
+ window.ragClear = function() {
1629
+ ragEngine.clear();
1630
+ ragSources = [];
1631
+ document.getElementById('rag-results').innerHTML = '';
1632
+ refreshRagUI();
1633
+ log('πŸ”Ž RAG index cleared','w');
1634
+ };
1635
+
1636
+ window.ragSearch = function() {
1637
+ const query = document.getElementById('rag-query').value.trim();
1638
+ if (!query) { log('RAG: no query','w'); return; }
1639
+ if (!ragEngine.indexed) { log('RAG: nothing indexed yet','w'); return; }
1640
+ const result = ragEngine.query(query, 5, 1);
1641
+ const el = document.getElementById('rag-results');
1642
+ if (result.passages.length === 0) {
1643
+ el.innerHTML = '<div style="color:var(--fg-dim);font-style:italic">No passages found.</div>';
1644
+ return;
1645
+ }
1646
+ el.innerHTML = result.passages.map((p, i) =>
1647
+ `<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>`
1648
+ ).join('');
1649
+ log(`πŸ”Ž RAG: ${result.passages.length} passages for "${query}"`, 'g');
1650
+ };
1651
+
1652
+ window.ragInjectPrompt = function() {
1653
+ const query = document.getElementById('rag-query').value.trim();
1654
+ if (!query) { log('RAG: no query for prompt','w'); return; }
1655
+ if (!ragEngine.indexed) { log('RAG: nothing indexed','w'); return; }
1656
+ const result = ragEngine.query(query, 5, 1);
1657
+ if (result.passages.length === 0) { log('RAG: no passages to inject','w'); return; }
1658
+ // Inject RAG context into the chat input
1659
+ const inp = document.getElementById('uin');
1660
+ inp.value = result.prompt;
1661
+ inp.style.height = 'auto';
1662
+ inp.style.height = Math.min(inp.scrollHeight, 300) + 'px';
1663
+ inp.focus();
1664
+ log(`πŸ”Ž RAG prompt injected (${result.passages.length} passages)`, 'g');
1665
+ };
1666
+
1667
+ // Allow Enter in RAG query to trigger search
1668
+ document.getElementById('rag-query').addEventListener('keydown', function(e) {
1669
+ if (e.key === 'Enter') { e.preventDefault(); ragSearch(); }
1670
+ });
1671
+
1672
+ // Auto network scan on init
1673
+ toolNetworkScan().catch(()=>{});
1674
+ </script>
1675
+ </body>
1676
+ </html>
hybrid_rag_lib.js ADDED
@@ -0,0 +1,316 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // ═══════════════════════════════════════════
2
+ // HYBRID RAG RETRIEVAL LIBRARY
3
+ // Pure JS Β· No dependencies Β· Browser-ready
4
+ // ═══════════════════════════════════════════
5
+
6
+ // ─────────────────────────────────────────
7
+ // TextProcessor β€” normalization, sentence splitting, tokenization
8
+ // ─────────────────────────────────────────
9
+ class TextProcessor {
10
+
11
+ static normalize(text) {
12
+ return text
13
+ .toLowerCase()
14
+ .replace(/[^\w\s]/g, '')
15
+ .split(/\s+/)
16
+ .filter(w => w.length > 2);
17
+ }
18
+
19
+ static splitSentences(text) {
20
+ return text
21
+ .replace(/\n/g, ' ')
22
+ .split(/[.!?]+/)
23
+ .map(s => s.trim())
24
+ .filter(s => s.length > 0);
25
+ }
26
+
27
+ }
28
+
29
+ // ─────────────────────────────────────────
30
+ // Similarity β€” phonetic, levenshtein, n-gram
31
+ // ─────────────────────────────────────────
32
+ class Similarity {
33
+
34
+ static phonetic(word) {
35
+ word = word.toLowerCase();
36
+ return word
37
+ .replace(/ph/g, 'f')
38
+ .replace(/ee/g, 'i')
39
+ .replace(/ea/g, 'i')
40
+ .replace(/oo/g, 'u')
41
+ .replace(/ou/g, 'u')
42
+ .replace(/ck/g, 'k')
43
+ .replace(/c/g, 'k')
44
+ .replace(/z/g, 's')
45
+ .replace(/x/g, 'ks');
46
+ }
47
+
48
+ static levenshtein(a, b) {
49
+ let matrix = [];
50
+ for (let i = 0; i <= b.length; i++) matrix[i] = [i];
51
+ for (let j = 0; j <= a.length; j++) matrix[0][j] = j;
52
+
53
+ for (let i = 1; i <= b.length; i++) {
54
+ for (let j = 1; j <= a.length; j++) {
55
+ if (b[i - 1] === a[j - 1])
56
+ matrix[i][j] = matrix[i - 1][j - 1];
57
+ else
58
+ matrix[i][j] = Math.min(
59
+ matrix[i - 1][j - 1] + 1,
60
+ matrix[i][j - 1] + 1,
61
+ matrix[i - 1][j] + 1
62
+ );
63
+ }
64
+ }
65
+ return matrix[b.length][a.length];
66
+ }
67
+
68
+ static ngrams(str, n = 3) {
69
+ let grams = [];
70
+ for (let i = 0; i <= str.length - n; i++)
71
+ grams.push(str.substring(i, i + n));
72
+ return grams;
73
+ }
74
+
75
+ static ngramSimilarity(a, b) {
76
+ let g1 = this.ngrams(a);
77
+ let g2 = this.ngrams(b);
78
+ let set2 = new Set(g2);
79
+ let matches = 0;
80
+ g1.forEach(g => { if (set2.has(g)) matches++; });
81
+ return matches / Math.max(g1.length, g2.length, 1);
82
+ }
83
+
84
+ }
85
+
86
+ // ─────────────────────────────────────────
87
+ // IndexBuilder β€” inverted index + document frequency
88
+ // ─────────────────────────────────────────
89
+ class IndexBuilder {
90
+
91
+ constructor() {
92
+ this.sentences = [];
93
+ this.index = {};
94
+ this.df = {};
95
+ this.docs = [];
96
+ }
97
+
98
+ build(text) {
99
+ this.sentences = TextProcessor.splitSentences(text);
100
+ this.index = {};
101
+ this.df = {};
102
+ this.docs = [];
103
+
104
+ this.sentences.forEach((sentence, id) => {
105
+ let words = TextProcessor.normalize(sentence);
106
+ this.docs[id] = words;
107
+ let unique = [...new Set(words)];
108
+
109
+ unique.forEach(w => {
110
+ if (!this.index[w]) this.index[w] = [];
111
+ this.index[w].push(id);
112
+ this.df[w] = (this.df[w] || 0) + 1;
113
+ });
114
+ });
115
+ }
116
+
117
+ }
118
+
119
+ // ─────────────────────────────────────────
120
+ // Ranker β€” BM25 + hybrid scoring
121
+ // ─────────────────────────────────────────
122
+ class Ranker {
123
+
124
+ static bm25(queryWords, words, df, N) {
125
+ let score = 0;
126
+ queryWords.forEach(q => {
127
+ let tf = words.filter(w => w === q).length;
128
+ if (tf > 0) {
129
+ let idf = Math.log((N + 1) / (df[q] || 1));
130
+ score += tf * idf * 2;
131
+ }
132
+ });
133
+ return score;
134
+ }
135
+
136
+ static hybrid(queryWords, sentenceWords, sentence, df, N) {
137
+ let score = this.bm25(queryWords, sentenceWords, df, N);
138
+
139
+ queryWords.forEach(q => {
140
+ sentenceWords.forEach(w => {
141
+ let pw = Similarity.phonetic(w);
142
+ let pq = Similarity.phonetic(q);
143
+
144
+ if (Similarity.levenshtein(pw, pq) <= 1)
145
+ score += 0.7;
146
+
147
+ let sim = Similarity.ngramSimilarity(pw, pq);
148
+ if (sim > 0.5)
149
+ score += sim;
150
+ });
151
+ });
152
+
153
+ if (sentence.toLowerCase().includes(queryWords.join(' ')))
154
+ score += 4;
155
+
156
+ return score;
157
+ }
158
+
159
+ }
160
+
161
+ // ─────────────────────────────────────────
162
+ // Retriever β€” candidate search + ranking
163
+ // ─────────────────────────────────────────
164
+ class Retriever {
165
+
166
+ constructor(indexBuilder) {
167
+ this.index = indexBuilder.index;
168
+ this.docs = indexBuilder.docs;
169
+ this.df = indexBuilder.df;
170
+ this.sentences = indexBuilder.sentences;
171
+ }
172
+
173
+ search(query) {
174
+ let queryWords = TextProcessor.normalize(query);
175
+ let candidates = new Set();
176
+
177
+ queryWords.forEach(w => {
178
+ (this.index[w] || []).forEach(id => candidates.add(id));
179
+ });
180
+
181
+ // Also add fuzzy candidates via phonetic matching
182
+ queryWords.forEach(q => {
183
+ let pq = Similarity.phonetic(q);
184
+ Object.keys(this.index).forEach(w => {
185
+ let pw = Similarity.phonetic(w);
186
+ if (Similarity.levenshtein(pw, pq) <= 1) {
187
+ this.index[w].forEach(id => candidates.add(id));
188
+ }
189
+ });
190
+ });
191
+
192
+ let scored = [];
193
+ candidates.forEach(id => {
194
+ let words = this.docs[id];
195
+ let sentence = this.sentences[id];
196
+ let score = Ranker.hybrid(
197
+ queryWords,
198
+ words,
199
+ sentence,
200
+ this.df,
201
+ this.sentences.length
202
+ );
203
+ if (score > 0)
204
+ scored.push({ id, score, sentence });
205
+ });
206
+
207
+ scored.sort((a, b) => b.score - a.score);
208
+ return scored;
209
+ }
210
+
211
+ }
212
+
213
+ // ─────────────────────────────────────────
214
+ // ContextBuilder β€” sentence window extraction
215
+ // ─────────────────────────────────────────
216
+ class ContextBuilder {
217
+
218
+ static window(sentences, id, size = 1) {
219
+ let start = Math.max(0, id - size);
220
+ let end = Math.min(sentences.length, id + size + 1);
221
+ return sentences.slice(start, end).join('. ');
222
+ }
223
+
224
+ }
225
+
226
+ // ─────────────────────────────────────────
227
+ // HybridRAG β€” main engine
228
+ // ─────────────────────────────────────────
229
+ class HybridRAG {
230
+
231
+ constructor() {
232
+ this.indexBuilder = new IndexBuilder();
233
+ this.retriever = null;
234
+ this.indexed = false;
235
+ this.sourceCount = 0;
236
+ this.sentenceCount = 0;
237
+ }
238
+
239
+ index(text) {
240
+ this.indexBuilder.build(text);
241
+ this.retriever = new Retriever(this.indexBuilder);
242
+ this.indexed = true;
243
+ this.sourceCount++;
244
+ this.sentenceCount = this.indexBuilder.sentences.length;
245
+ return {
246
+ sentences: this.sentenceCount,
247
+ uniqueTerms: Object.keys(this.indexBuilder.index).length
248
+ };
249
+ }
250
+
251
+ addText(text) {
252
+ // Append to existing index by rebuilding with combined text
253
+ const existingSentences = this.indexBuilder.sentences.join('. ');
254
+ const combined = existingSentences ? existingSentences + '. ' + text : text;
255
+ return this.index(combined);
256
+ }
257
+
258
+ query(query, topK = 5, windowSize = 1) {
259
+ if (!this.indexed || !this.retriever) {
260
+ return { passages: [], prompt: '', ranked: [], error: 'No text indexed yet.' };
261
+ }
262
+
263
+ let ranked = this.retriever.search(query);
264
+ let passages = [];
265
+ let seen = new Set();
266
+
267
+ ranked.slice(0, topK).forEach(r => {
268
+ let ctx = ContextBuilder.window(
269
+ this.indexBuilder.sentences,
270
+ r.id,
271
+ windowSize
272
+ );
273
+ // Deduplicate overlapping windows
274
+ if (!seen.has(ctx)) {
275
+ seen.add(ctx);
276
+ passages.push({
277
+ text: ctx,
278
+ score: r.score,
279
+ sentenceId: r.id,
280
+ original: r.sentence
281
+ });
282
+ }
283
+ });
284
+
285
+ let prompt =
286
+ 'Use the following context to answer the question:\n\n' +
287
+ passages.map((p, i) => `[${i + 1}] (score: ${p.score.toFixed(2)}) ${p.text}`).join('\n\n') +
288
+ '\n\nQuestion: ' + query +
289
+ '\nAnswer:';
290
+
291
+ return {
292
+ passages,
293
+ prompt,
294
+ ranked: ranked.slice(0, topK),
295
+ totalCandidates: ranked.length
296
+ };
297
+ }
298
+
299
+ getStats() {
300
+ return {
301
+ indexed: this.indexed,
302
+ sentences: this.sentenceCount,
303
+ uniqueTerms: Object.keys(this.indexBuilder.index).length,
304
+ sources: this.sourceCount
305
+ };
306
+ }
307
+
308
+ clear() {
309
+ this.indexBuilder = new IndexBuilder();
310
+ this.retriever = null;
311
+ this.indexed = false;
312
+ this.sourceCount = 0;
313
+ this.sentenceCount = 0;
314
+ }
315
+
316
+ }