quickgrid commited on
Commit
b523c91
·
verified ·
1 Parent(s): 5813afb
Files changed (1) hide show
  1. index.html +43 -1391
index.html CHANGED
@@ -1,1398 +1,50 @@
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>RAG Pipeline Visualizer</title>
7
- <link rel="preconnect" href="https://fonts.googleapis.com">
8
- <link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@300;400;500;600;700&family=DM+Sans:wght@300;400;500;600&display=swap" rel="stylesheet">
9
- <style>
10
- /* ═══════════════════════════════════════════════════════════ */
11
- /* TOKENS */
12
- /* ═══════════════════════════════════════════════════════════ */
13
- :root {
14
- --bg: #040912;
15
- --bg2: #07111f;
16
- --surface: #0b1a2e;
17
- --surface2: #102038;
18
- --surface3: #162845;
19
- --border: #1d3254;
20
- --border2: #26405e;
21
-
22
- --cyan: #00e5ff;
23
- --cyan-dim: rgba(0,229,255,.12);
24
- --purple: #a855f7;
25
- --purple-dim:rgba(168,85,247,.12);
26
- --green: #00ff88;
27
- --green-dim:rgba(0,255,136,.10);
28
- --amber: #ffaa00;
29
- --amber-dim:rgba(255,170,0,.12);
30
- --red: #ff4455;
31
- --pink: #f472b6;
32
-
33
- --text: #c8dff5;
34
- --text2: #7a9ab8;
35
- --text3: #3d5a7a;
36
-
37
- --mono: 'JetBrains Mono', monospace;
38
- --sans: 'DM Sans', system-ui, sans-serif;
39
- }
40
-
41
- /* ═══════════════════════════════════════════════════════════ */
42
- /* RESET & BASE */
43
- /* ═══════════════════════════════════════════════════════════ */
44
- *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
45
- html, body { height: 100%; overflow: hidden; }
46
- body {
47
- font-family: var(--sans);
48
- background: var(--bg);
49
- color: var(--text);
50
- display: grid;
51
- grid-template-rows: 44px 200px 1fr;
52
- height: 100vh;
53
- }
54
-
55
- /* scrollbar */
56
- ::-webkit-scrollbar { width: 4px; height: 4px; }
57
- ::-webkit-scrollbar-track { background: transparent; }
58
- ::-webkit-scrollbar-thumb { background: var(--border2); border-radius: 2px; }
59
-
60
- /* ═══════════════════════════════════════════════════════════ */
61
- /* HEADER */
62
- /* ═══════════════════════════════════════════════════════════ */
63
- #header {
64
- display: flex; align-items: center; justify-content: space-between;
65
- padding: 0 16px;
66
- background: var(--surface);
67
- border-bottom: 1px solid var(--border);
68
- position: relative; z-index: 50;
69
- }
70
- #header::after {
71
- content: ''; position: absolute; bottom: 0; left: 0; right: 0;
72
- height: 1px;
73
- background: linear-gradient(90deg,transparent,var(--cyan),var(--purple),transparent);
74
- opacity: .4;
75
- }
76
- .header-title {
77
- font-family: var(--mono); font-size: 13px; font-weight: 600; letter-spacing: .06em;
78
- background: linear-gradient(90deg, var(--cyan), var(--purple));
79
- -webkit-background-clip: text; -webkit-text-fill-color: transparent;
80
- }
81
- .header-title span { -webkit-text-fill-color: var(--text3); font-weight: 300; }
82
- .model-badges { display: flex; gap: 8px; }
83
- .badge {
84
- display: flex; align-items: center; gap: 5px;
85
- padding: 3px 9px; border-radius: 20px; border: 1px solid var(--border2);
86
- font-family: var(--mono); font-size: 10px; color: var(--text3);
87
- transition: all .3s;
88
- }
89
- .badge.loading { border-color: var(--amber); color: var(--amber); }
90
- .badge.ready { border-color: var(--green); color: var(--green); }
91
- .badge.error { border-color: var(--red); color: var(--red); }
92
- .badge-dot {
93
- width: 5px; height: 5px; border-radius: 50%; background: currentColor;
94
- }
95
- .badge.loading .badge-dot { animation: blink 1s infinite; }
96
- @keyframes blink { 0%,100%{opacity:1}50%{opacity:.2} }
97
-
98
- /* ═══════════════════════════════════════════════════════════ */
99
- /* NODE FLOW EDITOR */
100
- /* ═══════════════════════════════════════════════════════════ */
101
- #node-editor {
102
- position: relative;
103
- background: var(--bg2);
104
- border-bottom: 1px solid var(--border);
105
- overflow: hidden;
106
- }
107
- #flow-canvas { display: block; }
108
-
109
- /* Settings panel */
110
- #node-settings {
111
- position: absolute; right: 0; top: 0; bottom: 0; width: 240px;
112
- background: var(--surface);
113
- border-left: 1px solid var(--border);
114
- padding: 14px;
115
- overflow-y: auto;
116
- transform: translateX(100%);
117
- transition: transform .25s;
118
- z-index: 10;
119
- }
120
- #node-settings.open { transform: translateX(0); }
121
- .ns-title {
122
- font-family: var(--mono); font-size: 12px; font-weight: 600;
123
- margin-bottom: 14px; letter-spacing: .04em;
124
- }
125
- .ns-row { display: flex; flex-direction: column; gap: 4px; margin-bottom: 10px; }
126
- .ns-label {
127
- font-family: var(--mono); font-size: 9px; text-transform: uppercase;
128
- letter-spacing: .08em; color: var(--text3);
129
- }
130
- .ns-input {
131
- background: var(--surface3); border: 1px solid var(--border);
132
- color: var(--text); padding: 5px 8px; border-radius: 4px;
133
- font-family: var(--mono); font-size: 11px; width: 100%;
134
- outline: none; transition: border-color .2s;
135
- }
136
- .ns-input:focus { border-color: var(--cyan); }
137
- .ns-close {
138
- float: right; background: none; border: none; color: var(--text2);
139
- cursor: pointer; font-size: 16px; line-height: 1;
140
- }
141
- .ns-close:hover { color: var(--text); }
142
-
143
- /* ═══════════════════════════════════════════════════════════ */
144
- /* MAIN PANELS */
145
- /* ═══════════════════════════════════════════════════════════ */
146
- #main {
147
- display: grid;
148
- grid-template-columns: 1fr 1fr 1fr;
149
- overflow: hidden;
150
- min-height: 0;
151
- }
152
- .panel {
153
- display: flex; flex-direction: column; overflow: hidden;
154
- border-right: 1px solid var(--border);
155
- min-height: 0;
156
- }
157
- .panel:last-child { border-right: none; }
158
- .panel-hdr {
159
- display: flex; align-items: center; justify-content: space-between;
160
- padding: 7px 12px;
161
- background: var(--surface);
162
- border-bottom: 1px solid var(--border);
163
- flex-shrink: 0;
164
- }
165
- .panel-hdr-title {
166
- font-family: var(--mono); font-size: 10px; font-weight: 600;
167
- text-transform: uppercase; letter-spacing: .1em; color: var(--text2);
168
- }
169
- .panel-hdr-badge {
170
- font-family: var(--mono); font-size: 9px; color: var(--text3);
171
- padding: 2px 6px; border-radius: 4px; background: var(--surface2);
172
- }
173
-
174
- /* ═══════════════════════════════════════════════════════════ */
175
- /* CHAT PANEL */
176
- /* ═══════════════════════════════════════════════════════════ */
177
- #chat-messages {
178
- flex: 1; overflow-y: auto; padding: 10px;
179
- display: flex; flex-direction: column; gap: 8px;
180
- min-height: 0;
181
- }
182
- .msg { display: flex; flex-direction: column; gap: 3px; }
183
- .msg.user { align-items: flex-end; }
184
- .msg.assistant { align-items: flex-start; }
185
- .msg-bubble {
186
- max-width: 88%; padding: 8px 11px; border-radius: 8px;
187
- font-size: 12px; line-height: 1.55; white-space: pre-wrap;
188
- }
189
- .msg.user .msg-bubble {
190
- background: linear-gradient(135deg, var(--purple), #7c3aed);
191
- color: #fff; border-radius: 8px 8px 2px 8px;
192
- }
193
- .msg.assistant .msg-bubble {
194
- background: var(--surface2); border: 1px solid var(--border);
195
- color: var(--text); border-radius: 8px 8px 8px 2px;
196
- }
197
- .msg-time {
198
- font-family: var(--mono); font-size: 9px; color: var(--text3);
199
- padding: 0 4px;
200
- }
201
- .ctx-chips { display: flex; flex-wrap: wrap; gap: 3px; margin-top: 3px; }
202
- .ctx-chip {
203
- font-family: var(--mono); font-size: 9px;
204
- padding: 1px 6px; border-radius: 10px;
205
- background: var(--cyan-dim); border: 1px solid var(--cyan);
206
- color: var(--cyan);
207
- }
208
- /* typing dots */
209
- .typing { display: flex; gap: 4px; align-items: center; padding: 4px 2px; }
210
- .typing i {
211
- width: 5px; height: 5px; border-radius: 50%;
212
- background: var(--purple); display: block;
213
- animation: tdot 1s infinite;
214
- }
215
- .typing i:nth-child(2){animation-delay:.15s}
216
- .typing i:nth-child(3){animation-delay:.3s}
217
- @keyframes tdot{0%,100%{transform:translateY(0);opacity:.5}50%{transform:translateY(-5px);opacity:1}}
218
-
219
- /* chat input */
220
- #chat-input-wrap {
221
- flex-shrink: 0; display: flex; gap: 6px; padding: 9px 10px;
222
- border-top: 1px solid var(--border); background: var(--surface);
223
- }
224
- #chat-input {
225
- flex: 1; background: var(--surface3); border: 1px solid var(--border);
226
- color: var(--text); padding: 7px 11px; border-radius: 6px;
227
- font-family: var(--sans); font-size: 12px; outline: none;
228
- transition: border-color .2s;
229
- }
230
- #chat-input:focus { border-color: var(--purple); }
231
- #chat-input::placeholder { color: var(--text3); }
232
- #chat-input:disabled { opacity: .4; cursor: not-allowed; }
233
-
234
- /* ═══════════════════════════════════════════════════════════ */
235
- /* BUTTONS */
236
- /* ════════════════════════════════════════���══════════════════ */
237
- .btn {
238
- padding: 7px 13px; border-radius: 6px; border: none; cursor: pointer;
239
- font-family: var(--mono); font-size: 11px; font-weight: 600;
240
- transition: all .18s; letter-spacing: .03em;
241
- }
242
- .btn-cyan {
243
- background: var(--cyan); color: var(--bg);
244
- }
245
- .btn-cyan:hover { filter: brightness(1.15); transform: translateY(-1px); }
246
- .btn-cyan:active { transform: translateY(0); }
247
- .btn-cyan:disabled { opacity: .35; cursor: not-allowed; transform: none; filter: none; }
248
- .btn-ghost {
249
- background: var(--surface3); color: var(--text2);
250
- border: 1px solid var(--border);
251
- }
252
- .btn-ghost:hover { border-color: var(--border2); color: var(--text); }
253
- .btn-ghost:disabled { opacity: .35; cursor: not-allowed; }
254
- .btn-green {
255
- background: var(--green); color: var(--bg);
256
- }
257
- .btn-green:hover { filter: brightness(1.1); }
258
- .btn-green:disabled { opacity: .35; cursor: not-allowed; filter: none; }
259
-
260
- /* ═══════════════════════════════════════════════════════════ */
261
- /* RETRIEVAL PANEL */
262
- /* ═══════════════════════════════════════════════════════════ */
263
- #retrieval-panel { display: flex; flex-direction: column; min-height: 0; }
264
- .ret-section { flex: 1; display: flex; flex-direction: column; min-height: 0; overflow: hidden; }
265
- .ret-section + .ret-section { border-top: 1px solid var(--border); }
266
- .ret-section-hdr {
267
- flex-shrink: 0; padding: 5px 10px;
268
- font-family: var(--mono); font-size: 9px; font-weight: 600;
269
- text-transform: uppercase; letter-spacing: .1em;
270
- color: var(--text3); background: var(--surface2);
271
- border-bottom: 1px solid var(--border);
272
- display: flex; justify-content: space-between; align-items: center;
273
- }
274
- .ret-list { flex: 1; overflow-y: auto; padding: 6px; display: flex; flex-direction: column; gap: 5px; min-height: 0; }
275
- .ret-item {
276
- padding: 8px 10px; background: var(--surface2);
277
- border: 1px solid var(--border); border-radius: 5px;
278
- font-size: 11px; transition: all .35s; position: relative;
279
- cursor: default;
280
- }
281
- .ret-item.lit {
282
- border-color: var(--cyan);
283
- box-shadow: 0 0 0 1px var(--cyan-dim), inset 0 0 20px var(--cyan-dim);
284
- background: rgba(0,229,255,.05);
285
- animation: ret-pop .35s ease;
286
- }
287
- .ret-item.selected {
288
- border-color: var(--green);
289
- box-shadow: 0 0 0 1px var(--green-dim), inset 0 0 20px var(--green-dim);
290
- background: rgba(0,255,136,.04);
291
- animation: ret-pop .35s ease;
292
- }
293
- @keyframes ret-pop{0%{transform:scale(.96);opacity:.5}100%{transform:scale(1);opacity:1}}
294
- .ret-rank {
295
- position: absolute; top: -8px; left: 8px;
296
- font-family: var(--mono); font-size: 9px; font-weight: 700;
297
- padding: 1px 5px; border-radius: 4px;
298
- background: var(--amber); color: var(--bg);
299
- }
300
- .ret-item.selected .ret-rank { background: var(--green); }
301
- .ret-text {
302
- color: var(--text); font-size: 11px; line-height: 1.45;
303
- display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden;
304
- margin-bottom: 5px;
305
- }
306
- .ret-foot { display: flex; justify-content: space-between; align-items: center; gap: 6px; }
307
- .ret-meta { font-family: var(--mono); font-size: 9px; color: var(--text3); }
308
- .score-bar { flex: 1; height: 2px; background: var(--border); border-radius: 1px; overflow: hidden; }
309
- .score-fill {
310
- height: 100%; border-radius: 1px;
311
- transition: width .6s ease;
312
- background: linear-gradient(90deg, var(--purple), var(--cyan));
313
- }
314
- .ret-item.selected .score-fill {
315
- background: linear-gradient(90deg, var(--green), var(--cyan));
316
- }
317
- .score-pct { font-family: var(--mono); font-size: 9px; color: var(--text2); white-space: nowrap; }
318
-
319
- /* ═══════════════════════════════════════════════════════════ */
320
- /* VECTOR DB PANEL */
321
- /* ═══════════════════════════════════════════════════════════ */
322
- #add-form {
323
- flex-shrink: 0; padding: 9px; background: var(--surface2);
324
- border-bottom: 1px solid var(--border);
325
- display: flex; flex-direction: column; gap: 6px;
326
- }
327
- #add-form textarea {
328
- background: var(--surface3); border: 1px solid var(--border);
329
- color: var(--text); padding: 7px 9px; border-radius: 5px;
330
- font-family: var(--sans); font-size: 11px; resize: none; height: 54px;
331
- outline: none; width: 100%; line-height: 1.45; transition: border-color .2s;
332
- }
333
- #add-form textarea:focus { border-color: var(--green); }
334
- #add-form textarea::placeholder { color: var(--text3); }
335
- .form-row { display: grid; grid-template-columns: 1fr 1fr; gap: 5px; }
336
- .form-row input {
337
- background: var(--surface3); border: 1px solid var(--border);
338
- color: var(--text); padding: 5px 8px; border-radius: 4px;
339
- font-family: var(--mono); font-size: 10px; outline: none; width: 100%;
340
- transition: border-color .2s;
341
- }
342
- .form-row input:focus { border-color: var(--green); }
343
- .form-row input::placeholder { color: var(--text3); }
344
- .embed-progress { display: none; }
345
- .embed-progress.show { display: block; }
346
- .embed-progress-bar {
347
- height: 2px; background: var(--border); border-radius: 1px; overflow: hidden; margin-top: 2px;
348
- }
349
- .embed-progress-fill {
350
- height: 100%; background: linear-gradient(90deg, var(--green), var(--cyan));
351
- border-radius: 1px; width: 0; transition: width .3s;
352
- }
353
- .embed-progress-txt {
354
- font-family: var(--mono); font-size: 9px; color: var(--green);
355
- }
356
-
357
- /* table */
358
- #vdb-table-wrap { flex: 1; overflow: auto; min-height: 0; }
359
- #vdb-table {
360
- width: 100%; border-collapse: collapse;
361
- font-family: var(--mono); font-size: 10px;
362
- }
363
- #vdb-table th {
364
- padding: 5px 8px; background: var(--surface3);
365
- border-bottom: 1px solid var(--border);
366
- text-align: left; font-weight: 600; color: var(--text3);
367
- text-transform: uppercase; letter-spacing: .06em;
368
- position: sticky; top: 0; z-index: 1;
369
- }
370
- #vdb-table td {
371
- padding: 5px 8px; border-bottom: 1px solid rgba(29,50,84,.4);
372
- color: var(--text); vertical-align: top;
373
- }
374
- #vdb-table tr { transition: background .3s; }
375
- #vdb-table tr:hover td { background: var(--surface2); }
376
- #vdb-table tr.lit td { background: rgba(0,229,255,.05); }
377
- #vdb-table tr.lit { outline: 1px solid var(--cyan); }
378
- .vec-chip {
379
- display: inline-block;
380
- font-size: 9px; color: var(--cyan); white-space: nowrap;
381
- overflow: hidden; text-overflow: ellipsis; max-width: 80px;
382
- }
383
- .tag {
384
- display: inline-block; padding: 1px 5px; border-radius: 3px;
385
- font-size: 9px; font-weight: 600;
386
- background: var(--surface3); border: 1px solid var(--border); color: var(--text3);
387
- }
388
- .tag.new-tag { border-color: var(--green); color: var(--green); animation: tag-glow 2s ease; }
389
- @keyframes tag-glow{0%{box-shadow:0 0 10px var(--green)}100%{box-shadow:none}}
390
- .tag.selected-tag { border-color: var(--cyan); color: var(--cyan); }
391
-
392
- /* ═══════════════════════════════════════════════════════════ */
393
- /* EMPTY STATE */
394
- /* ═══════════════════════════════════════════════════════════ */
395
- .empty {
396
- flex: 1; display: flex; flex-direction: column; align-items: center; justify-content: center;
397
- color: var(--text3); font-size: 11px; gap: 7px; padding: 20px; text-align: center;
398
- }
399
- .empty-icon { font-size: 28px; opacity: .5; }
400
-
401
- /* ═══════════════════════════════════════════════════════════ */
402
- /* TOAST */
403
- /* ═══════════════════════════════════════════════════════════ */
404
- #toast {
405
- position: fixed; bottom: 16px; left: 50%; transform: translateX(-50%) translateY(60px);
406
- padding: 8px 16px; background: var(--surface3); border: 1px solid var(--border2);
407
- border-radius: 8px; font-family: var(--mono); font-size: 11px; color: var(--text);
408
- transition: transform .3s; z-index: 999; pointer-events: none; white-space: nowrap;
409
- }
410
- #toast.show { transform: translateX(-50%) translateY(0); }
411
-
412
- /* ═══════════════════════════════════════════════════════════ */
413
- /* LOADING OVERLAY */
414
- /* ═══════════════════════════════════════════════════════════ */
415
- #loading-overlay {
416
- position: fixed; inset: 0; background: var(--bg);
417
- display: flex; flex-direction: column; align-items: center; justify-content: center;
418
- gap: 20px; z-index: 200; transition: opacity .5s;
419
- }
420
- #loading-overlay.fade { opacity: 0; pointer-events: none; }
421
- .lo-title {
422
- font-family: var(--mono); font-size: 20px; font-weight: 700; letter-spacing: .08em;
423
- background: linear-gradient(90deg, var(--cyan), var(--purple));
424
- -webkit-background-clip: text; -webkit-text-fill-color: transparent;
425
- }
426
- .lo-sub {
427
- font-family: var(--sans); font-size: 12px; color: var(--text2); text-align: center;
428
- max-width: 340px; line-height: 1.6;
429
- }
430
- .lo-steps { display: flex; flex-direction: column; gap: 8px; width: 320px; }
431
- .lo-step {
432
- display: flex; align-items: center; gap: 10px;
433
- font-family: var(--mono); font-size: 11px; color: var(--text3);
434
- transition: color .3s;
435
- }
436
- .lo-step.active { color: var(--cyan); }
437
- .lo-step.done { color: var(--green); }
438
- .lo-step-icon { font-size: 14px; width: 20px; text-align: center; }
439
- .lo-bar-wrap { width: 320px; height: 2px; background: var(--border); border-radius: 1px; overflow: hidden; }
440
- .lo-bar-fill {
441
- height: 100%; background: linear-gradient(90deg, var(--cyan), var(--purple));
442
- border-radius: 1px; width: 0%; transition: width .5s;
443
- }
444
- </style>
445
- </head>
446
- <body>
447
-
448
- <!-- LOADING OVERLAY -->
449
- <div id="loading-overlay">
450
- <div class="lo-title">⬡ RAG PIPELINE VISUALIZER</div>
451
- <div class="lo-sub">Loading AI models into your browser. Models are cached after first download.</div>
452
- <div class="lo-steps">
453
- <div class="lo-step" id="lo-embed">
454
- <span class="lo-step-icon">🔢</span>
455
- <span>Embedding model — all-MiniLM-L6-v2 (22 MB)</span>
456
- </div>
457
- <div class="lo-step" id="lo-rerank">
458
- <span class="lo-step-icon">🔄</span>
459
- <span>Reranker — ms-marco-MiniLM-L-6 (~80 MB)</span>
460
- </div>
461
- <div class="lo-step" id="lo-llm">
462
- <span class="lo-step-icon">🤖</span>
463
- <span>LLM — LaMini-Flan-T5-248M (~900 MB)</span>
464
- </div>
465
- </div>
466
- <div class="lo-bar-wrap"><div class="lo-bar-fill" id="lo-bar"></div></div>
467
- <div class="lo-sub" id="lo-status">Initializing…</div>
468
- </div>
469
-
470
- <!-- HEADER -->
471
- <div id="header">
472
- <div class="header-title">⬡ RAG<span>/</span>VISUALIZER <span>· in-browser · <a href="https://quickgrid.github.io/">Made by Asif Ahmed</a></span></div>
473
- <div class="model-badges">
474
- <div class="badge" id="bd-embed"><div class="badge-dot"></div> Embed</div>
475
- <div class="badge" id="bd-rerank"><div class="badge-dot"></div> Rerank</div>
476
- <div class="badge" id="bd-llm"><div class="badge-dot"></div> LLM</div>
477
- </div>
478
- </div>
479
-
480
- <!-- NODE FLOW EDITOR -->
481
- <div id="node-editor">
482
- <canvas id="flow-canvas"></canvas>
483
- <div id="node-settings">
484
- <div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:12px">
485
- <div class="ns-title" id="ns-title">Node Settings</div>
486
- <button class="ns-close" onclick="closeSettings()">✕</button>
487
- </div>
488
- <div id="ns-body"></div>
489
- </div>
490
- </div>
491
-
492
- <!-- MAIN PANELS -->
493
- <div id="main">
494
- <!-- CHAT -->
495
- <div class="panel" id="chat-panel">
496
- <div class="panel-hdr">
497
- <span class="panel-hdr-title">💬 Chat</span>
498
- <span class="panel-hdr-badge" id="chat-status">idle</span>
499
- </div>
500
- <div id="chat-messages">
501
- <div class="empty" id="chat-empty">
502
- <div class="empty-icon">🤖</div>
503
- <span>Ask anything. Every step of the<br>RAG pipeline will be visualized live.</span>
504
- </div>
505
- </div>
506
- <div id="chat-input-wrap">
507
- <input id="chat-input" placeholder="Ask a question…" disabled />
508
- <button class="btn btn-cyan" id="send-btn" disabled>Send</button>
509
- </div>
510
- </div>
511
-
512
- <!-- RETRIEVAL + RERANKING -->
513
- <div class="panel" id="retrieval-panel">
514
- <div class="panel-hdr">
515
- <span class="panel-hdr-title">🔍 Retrieval &amp; Reranking</span>
516
- <span class="panel-hdr-badge" id="ret-status">idle</span>
517
- </div>
518
- <div class="ret-section">
519
- <div class="ret-section-hdr">
520
- <span>Retrieved (Top-K)</span>
521
- <span id="ret-k-badge" style="color:var(--cyan);font-size:9px"></span>
522
- </div>
523
- <div class="ret-list" id="ret-list">
524
- <div class="empty" id="ret-empty">
525
- <div class="empty-icon">📚</div>
526
- <span>Retrieved chunks appear here</span>
527
- </div>
528
- </div>
529
- </div>
530
- <div class="ret-section">
531
- <div class="ret-section-hdr">
532
- <span>After Reranking</span>
533
- <span id="rerank-k-badge" style="color:var(--green);font-size:9px"></span>
534
- </div>
535
- <div class="ret-list" id="rerank-list">
536
- <div class="empty" id="rerank-empty">
537
- <div class="empty-icon">🔄</div>
538
- <span>Reranked context appears here</span>
539
- </div>
540
- </div>
541
- </div>
542
- </div>
543
-
544
- <!-- VECTOR DB -->
545
- <div class="panel" id="vectordb-panel">
546
- <div class="panel-hdr">
547
- <span class="panel-hdr-title">🗄️ Vector DB (LanceDB-style)</span>
548
- <span class="panel-hdr-badge" id="db-count">0 vectors</span>
549
- </div>
550
- <div id="add-form">
551
- <textarea id="add-text" placeholder="Type or paste text to embed and store in the vector DB…"></textarea>
552
- <div class="form-row">
553
- <input id="add-source" placeholder="Source (e.g. doc1.pdf)" />
554
- <input id="add-category" placeholder="Category (e.g. science)" />
555
- </div>
556
- <div class="embed-progress" id="embed-prog">
557
- <div class="embed-progress-txt" id="embed-prog-txt">Embedding…</div>
558
- <div class="embed-progress-bar"><div class="embed-progress-fill" id="embed-prog-fill"></div></div>
559
- </div>
560
- <button class="btn btn-green" id="add-btn" style="width:100%" disabled>+ Embed &amp; Add to Vector DB</button>
561
- </div>
562
- <div id="vdb-table-wrap">
563
- <table id="vdb-table">
564
- <thead>
565
- <tr>
566
- <th>#</th>
567
- <th style="min-width:110px">Text Preview</th>
568
- <th>Vector Preview</th>
569
- <th>Source</th>
570
- <th>Category</th>
571
- <th>Date Added</th>
572
- </tr>
573
- </thead>
574
- <tbody id="vdb-tbody"></tbody>
575
- </table>
576
- </div>
577
- </div>
578
- </div>
579
-
580
- <!-- TOAST -->
581
- <div id="toast"></div>
582
-
583
- <!-- ═══════════════════════════════════════════════════════════
584
- SCRIPT
585
- ═══════════════════════════════════════════════════════════ -->
586
- <script type="module">
587
- import { pipeline, env }
588
- from 'https://cdn.jsdelivr.net/npm/@xenova/transformers@2.17.2';
589
-
590
- env.allowLocalModels = false;
591
- env.useBrowserCache = true; // cache models in browser
592
-
593
- /* ─────────────────────────────────────────────────────────── */
594
- /* CONFIG */
595
- /* ─────────────────────────────────────────────────────────── */
596
- const CFG = {
597
- embedModel: 'Xenova/all-MiniLM-L6-v2',
598
- rerankModel: 'Xenova/ms-marco-MiniLM-L-6-v2',
599
- llmModel: 'Xenova/LaMini-Flan-T5-248M',
600
- topK: 5,
601
- topKRerank: 3,
602
- maxTokens: 180,
603
- };
604
-
605
- /* ─────────────────────────────────────────────────────────── */
606
- /* STATE */
607
- /* ─────────────────────────────────────────────────────────── */
608
- const ST = {
609
- embedder: null, reranker: null, generator: null,
610
- db: [], // {id,text,vec,source,category,date}
611
- busy: false,
612
- };
613
-
614
- /* ─────────────────────────────────────────────────────────── */
615
- /* IndexedDB persistence (LanceDB-style in-browser) */
616
- /* ─────────────────────────────────────────────────────────── */
617
- let idb = null;
618
- async function openIDB() {
619
- return new Promise((res, rej) => {
620
- const req = indexedDB.open('rag-viz-db', 1);
621
- req.onupgradeneeded = e => {
622
- e.target.result.createObjectStore('vectors', { keyPath: 'id' });
623
- };
624
- req.onsuccess = e => { idb = e.target.result; res(); };
625
- req.onerror = e => rej(e);
626
- });
627
- }
628
- async function idbGetAll() {
629
- return new Promise((res, rej) => {
630
- const tx = idb.transaction('vectors','readonly');
631
- const req = tx.objectStore('vectors').getAll();
632
- req.onsuccess = e => res(e.target.result || []);
633
- req.onerror = e => rej(e);
634
- });
635
- }
636
- async function idbPut(entry) {
637
- return new Promise((res, rej) => {
638
- const tx = idb.transaction('vectors','readwrite');
639
- tx.objectStore('vectors').put(entry);
640
- tx.oncomplete = res; tx.onerror = rej;
641
- });
642
- }
643
-
644
- /* ─────────────────────────────────────────────────────────── */
645
- /* VECTOR MATH */
646
- /* ─────────────────────────────────────────────────────────── */
647
- function cosine(a, b) {
648
- let dot = 0, na = 0, nb = 0;
649
- for (let i = 0; i < a.length; i++) {
650
- dot += a[i]*b[i]; na += a[i]*a[i]; nb += b[i]*b[i];
651
- }
652
- return dot / (Math.sqrt(na)*Math.sqrt(nb) + 1e-9);
653
- }
654
-
655
- function vecSearch(queryVec, topK) {
656
- const scored = ST.db.map(e => ({
657
- ...e, score: cosine(queryVec, e.vec)
658
- }));
659
- scored.sort((a,b) => b.score - a.score);
660
- return scored.slice(0, topK);
661
- }
662
-
663
- /* ─────────────────────────────────────────────────────────── */
664
- /* TOAST */
665
- /* ─────────────────────────────────────────────────────────── */
666
- let toastTimer;
667
- function toast(msg, dur = 3200) {
668
- const el = document.getElementById('toast');
669
- el.textContent = msg;
670
- el.classList.add('show');
671
- clearTimeout(toastTimer);
672
- toastTimer = setTimeout(() => el.classList.remove('show'), dur);
673
- }
674
-
675
- const sleep = ms => new Promise(r => setTimeout(r, ms));
676
- const $ = id => document.getElementById(id);
677
-
678
- /* ─────────────────────────────────────────────────────────── */
679
- /* BADGE helpers */
680
- /* ─────────────────────────────────────────────────────────── */
681
- function badge(id, state) {
682
- const el = $(`bd-${id}`);
683
- el.className = `badge ${state}`;
684
- }
685
- function loStep(id, state) {
686
- const el = $(`lo-${id}`);
687
- if (state === 'active') { el.className = 'lo-step active'; el.querySelector('.lo-step-icon').textContent = '⏳'; }
688
- if (state === 'done') { el.className = 'lo-step done'; el.querySelector('.lo-step-icon').textContent = '✅'; }
689
- if (state === 'error') { el.className = 'lo-step'; el.querySelector('.lo-step-icon').textContent = '⚠️'; }
690
- }
691
- function loBar(pct) { $('lo-bar').style.width = pct + '%'; }
692
-
693
- /* ─────────────────────────────────────────────────────────── */
694
- /* MODEL LOADING */
695
- /* ─────────────────────────────────────────────────────────── */
696
- async function loadModels() {
697
- $('lo-status').textContent = 'Loading embedding model…';
698
- loStep('embed','active'); loBar(5); badge('embed','loading');
699
-
700
- try {
701
- ST.embedder = await pipeline('feature-extraction', CFG.embedModel, {
702
- progress_callback: p => {
703
- if (p.status === 'progress') {
704
- const pct = 5 + (p.progress || 0) * .2;
705
- loBar(Math.min(25, pct));
706
- }
707
- }
708
- });
709
- badge('embed','ready'); loStep('embed','done');
710
- toast('✅ Embedding model ready');
711
- } catch(e) {
712
- badge('embed','error'); loStep('embed','error');
713
- console.error(e); toast('⚠️ Embedding model failed: '+e.message, 5000);
714
- }
715
-
716
- loBar(25); $('lo-status').textContent = 'Loading reranking model…';
717
- loStep('rerank','active'); badge('rerank','loading');
718
-
719
- try {
720
- ST.reranker = await pipeline('text-classification', CFG.rerankModel, {
721
- progress_callback: p => {
722
- if (p.status === 'progress') loBar(25 + (p.progress||0)*.2);
723
- }
724
- });
725
- badge('rerank','ready'); loStep('rerank','done');
726
- toast('✅ Reranking model ready');
727
- } catch(e) {
728
- badge('rerank','error'); loStep('rerank','error');
729
- ST.reranker = null;
730
- console.warn('Reranker unavailable, fallback to cosine:', e.message);
731
- toast('⚠️ Reranker unavailable — cosine fallback active', 5000);
732
- }
733
-
734
- loBar(45); $('lo-status').textContent = 'Loading LLM (large — may take a minute)…';
735
- loStep('llm','active'); badge('llm','loading');
736
-
737
- try {
738
- ST.generator = await pipeline('text2text-generation', CFG.llmModel, {
739
- progress_callback: p => {
740
- if (p.status === 'progress') loBar(45 + (p.progress||0)*.5);
741
- }
742
- });
743
- badge('llm','ready'); loStep('llm','done');
744
- toast('✅ LLM ready!');
745
- } catch(e) {
746
- badge('llm','error'); loStep('llm','error');
747
- ST.generator = null;
748
- console.warn('LLM unavailable:', e.message);
749
- toast('⚠️ LLM not loaded — answers will be template-based', 5000);
750
- }
751
-
752
- loBar(100);
753
- $('lo-status').textContent = ST.embedder ? 'Ready! All models loaded.' : '⚠️ Some models failed.';
754
- await sleep(800);
755
- $('loading-overlay').classList.add('fade');
756
- setTimeout(() => { $('loading-overlay').style.display='none'; }, 500);
757
-
758
- if (ST.embedder) {
759
- $('chat-input').disabled = false;
760
- $('send-btn').disabled = false;
761
- $('add-btn').disabled = false;
762
- // Load sample data if DB empty
763
- if (ST.db.length === 0) await insertSamples();
764
- }
765
- FE.setAllIdle();
766
- }
767
-
768
- /* ─────────────────────────────────────────────────────────── */
769
- /* EMBEDDING & RERANKING & GENERATION */
770
- /* ─────────────────────────────────────────────────────────── */
771
- async function embed(text) {
772
- const out = await ST.embedder(text, { pooling: 'mean', normalize: true });
773
- return Array.from(out.data);
774
- }
775
-
776
- async function rerank(query, results) {
777
- const topK = CFG.topKRerank;
778
- if (!ST.reranker) {
779
- return [...results].sort((a,b)=>b.score-a.score).slice(0,topK)
780
- .map(r=>({...r, rerankScore: r.score}));
781
- }
782
- try {
783
- const pairs = results.map(r => [query, r.text]);
784
- const scores = await ST.reranker(pairs, { topk: 1 });
785
- const withScores = results.map((r, i) => ({
786
- ...r,
787
- rerankScore: Array.isArray(scores[i]) ? scores[i][0].score : (scores[i]?.score ?? r.score)
788
- }));
789
- withScores.sort((a,b) => b.rerankScore - a.rerankScore);
790
- return withScores.slice(0, topK);
791
- } catch(e) {
792
- console.warn('Rerank error, cosine fallback:', e.message);
793
- return [...results].sort((a,b)=>b.score-a.score).slice(0,topK)
794
- .map(r=>({...r, rerankScore: r.score}));
795
- }
796
- }
797
-
798
- async function generate(query, context) {
799
- const ctxTxt = context.map((c,i) => `[${i+1}] ${c.text}`).join('\n');
800
- const prompt = `Answer the question using the context.\n\nContext:\n${ctxTxt}\n\nQuestion: ${query}\nAnswer:`;
801
- if (!ST.generator) {
802
- return `Context summary:\n${context.slice(0,2).map(c=>'• '+c.text.slice(0,100)).join('\n')}`;
803
- }
804
- try {
805
- const out = await ST.generator(prompt, { max_new_tokens: CFG.maxTokens, do_sample: false });
806
- return (out[0]?.generated_text || '').trim() || 'No answer generated.';
807
- } catch(e) {
808
- console.error('Generate error:', e);
809
- return context[0]?.text || 'No context found.';
810
- }
811
- }
812
-
813
- /* ─────────────────────────────────────────────────────────── */
814
- /* VECTOR DB UI */
815
- /* ─────────────────────────────────────────────────────────── */
816
- function fmtDate(iso) {
817
- const d = new Date(iso);
818
- return `${d.getMonth()+1}/${d.getDate()} ${String(d.getHours()).padStart(2,'0')}:${String(d.getMinutes()).padStart(2,'0')}`;
819
- }
820
- function vecPreview(v) {
821
- return `[${v.slice(0,3).map(x=>x.toFixed(2)).join(', ')},…]`;
822
- }
823
- function addTableRow(entry, isNew=false) {
824
- const tbody = $('vdb-tbody');
825
- const rowNum = tbody.children.length + 1;
826
- const tr = document.createElement('tr');
827
- tr.dataset.id = entry.id;
828
- const textPrev = entry.text.length > 40 ? entry.text.slice(0,40)+'…' : entry.text;
829
- tr.innerHTML = `
830
- <td style="color:var(--text3)">${rowNum}</td>
831
- <td title="${entry.text}" style="font-family:var(--sans);font-size:11px">${textPrev}</td>
832
- <td><span class="vec-chip">${vecPreview(entry.vec)}</span></td>
833
- <td><span class="tag ${isNew?'new-tag':''}">${entry.source||'—'}</span></td>
834
- <td style="color:var(--text2)">${entry.category||'—'}</td>
835
- <td style="color:var(--text3)">${fmtDate(entry.date)}</td>
836
- `;
837
- tbody.appendChild(tr);
838
- if (isNew) tr.scrollIntoView({behavior:'smooth',block:'nearest'});
839
- $('db-count').textContent = `${tbody.children.length} vectors`;
840
- }
841
- function rebuildTable() {
842
- $('vdb-tbody').innerHTML = '';
843
- ST.db.forEach(e => addTableRow(e, false));
844
- $('db-count').textContent = `${ST.db.length} vectors`;
845
- }
846
- function highlightRows(ids) {
847
- document.querySelectorAll('#vdb-tbody tr').forEach(tr => {
848
- tr.classList.toggle('lit', ids.includes(tr.dataset.id));
849
- });
850
- }
851
- function clearHighlightRows() {
852
- document.querySelectorAll('#vdb-tbody tr').forEach(tr => tr.classList.remove('lit'));
853
- }
854
-
855
- /* ─────────────────────────────────────────────────────────── */
856
- /* ADD ENTRY */
857
- /* ─────────────────────────────────────────────────────────── */
858
- async function addEntry() {
859
- const text = $('add-text').value.trim();
860
- if (!text) { toast('Enter some text first'); return; }
861
- if (!ST.embedder) { toast('Embedding model not ready'); return; }
862
-
863
- $('add-btn').disabled = true;
864
- $('add-btn').textContent = 'Embedding…';
865
- $('embed-prog').classList.add('show');
866
- $('embed-prog-txt').textContent = 'Computing embedding…';
867
-
868
- // Animate embed node active
869
- FE.setStatus('embed', 'active');
870
-
871
- // Fake progress
872
- let pct = 0;
873
- const progTimer = setInterval(() => {
874
- pct = Math.min(pct + 8, 90);
875
- $('embed-prog-fill').style.width = pct + '%';
876
- }, 120);
877
-
878
- try {
879
- const vec = await embed(text);
880
- clearInterval(progTimer);
881
- $('embed-prog-fill').style.width = '100%';
882
-
883
- FE.setStatus('embed', 'done');
884
- FE.setStatus('search', 'active');
885
-
886
- const entry = {
887
- id: Date.now().toString(36) + Math.random().toString(36).slice(2,6),
888
- text, vec,
889
- source: $('add-source').value.trim() || 'manual',
890
- category: $('add-category').value.trim() || 'general',
891
- date: new Date().toISOString(),
892
- };
893
- ST.db.push(entry);
894
- await idbPut(entry);
895
- addTableRow(entry, true);
896
-
897
- // Animate edge
898
- await FE.animEdgePromise('embed','search');
899
- FE.setStatus('search','done');
900
- setTimeout(() => FE.setAllIdle(), 800);
901
-
902
- toast(`✅ Stored "${text.slice(0,35)}…" in vector DB`);
903
- $('add-text').value = '';
904
-
905
- } catch(e) {
906
- clearInterval(progTimer);
907
- toast('❌ Error: '+e.message, 5000);
908
- console.error(e);
909
- FE.setAllIdle();
910
- }
911
-
912
- $('embed-prog').classList.remove('show');
913
- $('embed-prog-fill').style.width = '0';
914
- $('add-btn').disabled = false;
915
- $('add-btn').textContent = '+ Embed & Add to Vector DB';
916
- }
917
-
918
- /* ─────────────────────────────────────────────────────────── */
919
- /* RETRIEVAL UI */
920
- /* ─────────────────────────────────────────────────────────── */
921
- function renderRetList(items, listId, isReranked=false) {
922
- const el = $(listId);
923
  el.innerHTML = '';
924
- if (!items || items.length === 0) {
925
- el.innerHTML = `<div class="empty"><div class="empty-icon">${isReranked?'🔄':'📚'}</div><span>No results</span></div>`;
926
  return;
927
  }
928
- items.forEach((item, i) => {
929
- const div = document.createElement('div');
930
- div.className = `ret-item${isReranked?' selected':''}`;
931
- div.dataset.id = item.id;
932
- const score = isReranked ? (item.rerankScore ?? item.score) : item.score;
933
- const scorePct = Math.min(100, Math.abs(score) * 100);
934
- div.innerHTML = `
935
- <div class="ret-rank">#${i+1}</div>
936
- <div class="ret-text">${item.text}</div>
937
- <div class="ret-foot">
938
- <span class="ret-meta">${item.source||'?'} · ${item.category||'?'}</span>
939
- <div class="score-bar"><div class="score-fill" style="width:0%"></div></div>
940
- <span class="score-pct">${(score*100).toFixed(1)}%</span>
941
- </div>
942
- `;
943
- setTimeout(() => {
944
- div.classList.add(isReranked ? 'selected' : 'lit');
945
- div.querySelector('.score-fill').style.width = scorePct + '%';
946
- }, i * 90);
947
- el.appendChild(div);
948
- });
949
- }
950
-
951
- /* ─────────────────────────────────────────────────────────── */
952
- /* CHAT UI */
953
- /* ─────────────────────────────────────────────────────────── */
954
- function chatMsg(role, text, ctxItems=[]) {
955
- const c = $('chat-messages');
956
- $('chat-empty')?.remove();
957
-
958
- const div = document.createElement('div');
959
- div.className = `msg ${role}`;
960
-
961
- let chips = '';
962
- if (ctxItems.length > 0) {
963
- chips = `<div class="ctx-chips">${ctxItems.map(c=>`<span class="ctx-chip">📄 ${c.source||'chunk'}</span>`).join('')}</div>`;
964
- }
965
-
966
- div.innerHTML = `
967
- <div class="msg-bubble">${text.replace(/</g,'&lt;').replace(/\n/g,'<br>')}</div>
968
- ${chips}
969
- <div class="msg-time">${new Date().toLocaleTimeString()}</div>
970
- `;
971
- c.appendChild(div);
972
- c.scrollTop = c.scrollHeight;
973
- return div;
974
- }
975
-
976
- let typingEl = null;
977
- function showTyping() {
978
- const c = $('chat-messages');
979
- typingEl = document.createElement('div');
980
- typingEl.className = 'msg assistant';
981
- typingEl.id = '__typing';
982
- typingEl.innerHTML = `<div class="msg-bubble"><div class="typing"><i></i><i></i><i></i></div></div>`;
983
- c.appendChild(typingEl);
984
- c.scrollTop = c.scrollHeight;
985
- }
986
- function removeTyping() { $('__typing')?.remove(); typingEl = null; }
987
-
988
- /* ─────────────────────────────────────────────────────────── */
989
- /* RAG PIPELINE */
990
- /* ─────────────────────────────────────────────────────────── */
991
- async function runRAG(query) {
992
- if (ST.busy) return;
993
- if (!ST.embedder) { toast('Embedding model not loaded yet'); return; }
994
- ST.busy = true;
995
-
996
- $('send-btn').disabled = true;
997
- $('chat-status').textContent = 'processing…';
998
- $('ret-status').textContent = 'searching…';
999
-
1000
- chatMsg('user', query);
1001
- showTyping();
1002
-
1003
- // Clear retrieval panels
1004
- $('ret-list').innerHTML = `<div class="empty"><div class="empty-icon">⏳</div><span>Searching…</span></div>`;
1005
- $('rerank-list').innerHTML = `<div class="empty"><div class="empty-icon">⏳</div><span>Waiting…</span></div>`;
1006
- clearHighlightRows();
1007
-
1008
- FE.setAllIdle();
1009
-
1010
- try {
1011
- /* ── 1. Query node ── */
1012
- FE.setStatus('query','active');
1013
- await sleep(200);
1014
- FE.setStatus('query','done');
1015
- await FE.animEdgePromise('query','embed');
1016
-
1017
- /* ── 2. Embed query ── */
1018
- FE.setStatus('embed','active');
1019
- $('chat-status').textContent = 'embedding query…';
1020
- const qVec = await embed(query);
1021
- FE.setStatus('embed','done');
1022
- await FE.animEdgePromise('embed','search');
1023
-
1024
- /* ── 3. Vector search ── */
1025
- FE.setStatus('search','active');
1026
- $('chat-status').textContent = 'vector search…';
1027
- await sleep(150);
1028
- const retrieved = vecSearch(qVec, CFG.topK);
1029
- FE.setStatus('search','done');
1030
- await FE.animEdgePromise('search','topk');
1031
-
1032
- /* ── 4. Top-K ── */
1033
- FE.setStatus('topk','active');
1034
- $('ret-k-badge').textContent = `k=${retrieved.length}`;
1035
- renderRetList(retrieved,'ret-list',false);
1036
- highlightRows(retrieved.map(r=>r.id));
1037
- await sleep(400);
1038
- FE.setStatus('topk','done');
1039
- await FE.animEdgePromise('topk','rerank');
1040
-
1041
- /* ── 5. Rerank ── */
1042
- FE.setStatus('rerank','active');
1043
- $('ret-status').textContent = 'reranking…';
1044
- $('chat-status').textContent = 'reranking…';
1045
- const reranked = await rerank(query, retrieved);
1046
- $('rerank-k-badge').textContent = `k=${reranked.length}`;
1047
- renderRetList(reranked,'rerank-list',true);
1048
- FE.setStatus('rerank','done');
1049
- await FE.animEdgePromise('rerank','llm');
1050
-
1051
- /* ── 6. LLM ── */
1052
- FE.setStatus('llm','active');
1053
- $('chat-status').textContent = 'generating…';
1054
- const context = reranked.length > 0 ? reranked : retrieved.slice(0, CFG.topKRerank);
1055
- const answer = await generate(query, context);
1056
- FE.setStatus('llm','done');
1057
- await FE.animEdgePromise('llm','answer');
1058
-
1059
- /* ── 7. Answer ── */
1060
- FE.setStatus('answer','active');
1061
- removeTyping();
1062
- chatMsg('assistant', answer, context);
1063
- FE.setStatus('answer','done');
1064
-
1065
- } catch(e) {
1066
- removeTyping();
1067
- chatMsg('assistant', '❌ Pipeline error: '+e.message);
1068
- console.error(e);
1069
- FE.setAllIdle();
1070
- }
1071
-
1072
- $('send-btn').disabled = false;
1073
- $('chat-status').textContent = 'idle';
1074
- $('ret-status').textContent = 'idle';
1075
- ST.busy = false;
1076
- clearHighlightRows();
1077
- }
1078
-
1079
- /* ─────────────────────────────────────────────────────────── */
1080
- /* SAMPLE DATA */
1081
- /* ─────────────────────────────────────────────────────────── */
1082
- async function insertSamples() {
1083
- toast('Adding sample documents…', 5000);
1084
- const samples = [
1085
- { text:'The Eiffel Tower in Paris was built in 1889 by Gustave Eiffel for the World Fair. It stands 330 metres tall.', source:'wiki.txt', category:'history' },
1086
- { text:'Machine learning is a branch of AI that enables systems to learn from data without being explicitly programmed.', source:'ml-intro.md', category:'tech' },
1087
- { text:'The Amazon River carries more water than any other river on Earth and drains into the Atlantic Ocean.', source:'geo.txt', category:'geography' },
1088
- { text:'Python is a high-level interpreted language prized for readability, used widely in data science and web development.', source:'prog.md', category:'tech' },
1089
- { text:'The speed of light in a vacuum is exactly 299,792,458 metres per second, a fundamental constant of nature.', source:'physics.txt', category:'science' },
1090
- { text:'Transformers are a neural network architecture introduced in "Attention is All You Need" (2017). Self-attention is core.', source:'dl-paper.txt', category:'AI' },
1091
- { text:'Retrieval-Augmented Generation (RAG) combines a retriever to find relevant passages with a language model to generate answers.', source:'rag-overview.md', category:'AI' },
1092
- ];
1093
- for (const s of samples) {
1094
- FE.setStatus('embed','active');
1095
- const vec = await embed(s.text);
1096
- FE.setStatus('embed','done');
1097
- const entry = {
1098
- id: Date.now().toString(36)+Math.random().toString(36).slice(2,5),
1099
- text: s.text, vec, source: s.source, category: s.category,
1100
- date: new Date().toISOString()
1101
- };
1102
- ST.db.push(entry);
1103
- await idbPut(entry);
1104
- addTableRow(entry, true);
1105
- await sleep(80);
1106
- }
1107
- FE.setAllIdle();
1108
- toast('✅ Sample documents loaded. Try asking a question!', 4000);
1109
- }
1110
-
1111
- /* ─────────────────────────────────────────────────────────── */
1112
- /* NODE FLOW EDITOR */
1113
- /* ─────────────────────────────────────────────────────────── */
1114
- class FlowEditor {
1115
- constructor() {
1116
- this.canvas = $('flow-canvas');
1117
- this.ctx = this.canvas.getContext('2d');
1118
- this.dpr = Math.min(window.devicePixelRatio || 1, 2);
1119
- this.W = 0; this.H = 0;
1120
- this.selectedId = null;
1121
- this.particles = [];
1122
-
1123
- this.nodes = [
1124
- { id:'query', label:'Query', icon:'📝', color:'#a855f7',
1125
- settings:{ Type:'User Input', Description:'Raw text question from user' }},
1126
- { id:'embed', label:'Embed Query', icon:'🔢', color:'#00e5ff',
1127
- settings:{ Model:CFG.embedModel, Dims:384, Pooling:'mean', Normalize:true }},
1128
- { id:'search', label:'Vector Search', icon:'🗄️', color:'#ffaa00',
1129
- settings:{ Metric:'Cosine', 'Top-K':CFG.topK, Index:'Flat (in-memory)' }},
1130
- { id:'topk', label:'Top-K Select', icon:'📊', color:'#f472b6',
1131
- settings:{ K:CFG.topK, Threshold:'0.0', Dedup:'false' }},
1132
- { id:'rerank', label:'Rerank', icon:'🔄', color:'#00ff88',
1133
- settings:{ Model:CFG.rerankModel, Type:'Cross-Encoder', 'Keep':CFG.topKRerank }},
1134
- { id:'llm', label:'LLM', icon:'🤖', color:'#fb923c',
1135
- settings:{ Model:CFG.llmModel, MaxTokens:CFG.maxTokens, Sample:false }},
1136
- { id:'answer', label:'Answer', icon:'💬', color:'#34d399',
1137
- settings:{ Format:'Text', Grounded:true, Sources:'Top reranked chunks' }},
1138
- ];
1139
- this.edges = [
1140
- ['query','embed'],['embed','search'],['search','topk'],
1141
- ['topk','rerank'],['rerank','llm'],['llm','answer']
1142
- ];
1143
- this.statuses = {};
1144
- this.nodes.forEach(n => this.statuses[n.id] = 'idle');
1145
-
1146
- this.resize();
1147
- window.addEventListener('resize', () => this.resize());
1148
- this.canvas.addEventListener('click', e => this.onClick(e));
1149
- this.raf();
1150
- }
1151
-
1152
- resize() {
1153
- const wrap = this.canvas.parentElement;
1154
- const rect = wrap.getBoundingClientRect();
1155
- this.W = rect.width; this.H = rect.height;
1156
- this.canvas.width = this.W * this.dpr;
1157
- this.canvas.height = this.H * this.dpr;
1158
- this.canvas.style.cssText = `width:${this.W}px;height:${this.H}px;`;
1159
- this.ctx.scale(this.dpr, this.dpr);
1160
- this.layout();
1161
- }
1162
-
1163
- layout() {
1164
- const n = this.nodes.length;
1165
- const nW = 110, nH = 68;
1166
- const pad = 28;
1167
- const gap = (this.W - pad*2 - n*nW) / (n - 1);
1168
- this.nodes.forEach((node, i) => {
1169
- node.x = pad + i*(nW+gap);
1170
- node.y = (this.H - nH) / 2;
1171
- node.w = nW; node.h = nH;
1172
- });
1173
- }
1174
-
1175
- draw() {
1176
- const { ctx, W, H } = this;
1177
- ctx.clearRect(0, 0, W, H);
1178
-
1179
- // Grid
1180
- ctx.save();
1181
- ctx.strokeStyle = 'rgba(29,50,84,0.35)';
1182
- ctx.lineWidth = 1;
1183
- for (let x = 0; x < W; x += 28) { ctx.beginPath(); ctx.moveTo(x,0); ctx.lineTo(x,H); ctx.stroke(); }
1184
- for (let y = 0; y < H; y += 28) { ctx.beginPath(); ctx.moveTo(0,y); ctx.lineTo(W,y); ctx.stroke(); }
1185
- ctx.restore();
1186
-
1187
- // Edges
1188
- this.edges.forEach(([fId,tId]) => {
1189
- const fn = this.nodeById(fId), tn = this.nodeById(tId);
1190
- const fx = fn.x+fn.w, fy = fn.y+fn.h/2;
1191
- const tx = tn.x, ty = tn.y+tn.h/2;
1192
- const cx = (fx+tx)/2;
1193
- ctx.beginPath();
1194
- ctx.moveTo(fx,fy);
1195
- ctx.bezierCurveTo(cx,fy,cx,ty,tx,ty);
1196
- ctx.strokeStyle = 'rgba(29,50,84,0.9)';
1197
- ctx.lineWidth = 2;
1198
- ctx.stroke();
1199
- });
1200
-
1201
- // Particles
1202
- this.particles.forEach(p => {
1203
- const fn = this.nodeById(p.from), tn = this.nodeById(p.to);
1204
- const fx = fn.x+fn.w, fy = fn.y+fn.h/2;
1205
- const tx = tn.x, ty = tn.y+tn.h/2;
1206
- const cx = (fx+tx)/2;
1207
- const pt = this.bezPt(fx,fy,cx,fy,cx,ty,tx,ty,p.t);
1208
- const grd = ctx.createRadialGradient(pt.x,pt.y,0,pt.x,pt.y,10);
1209
- grd.addColorStop(0, fn.color+'cc');
1210
- grd.addColorStop(1, 'transparent');
1211
- ctx.beginPath(); ctx.arc(pt.x,pt.y,10,0,Math.PI*2);
1212
- ctx.fillStyle = grd; ctx.fill();
1213
- ctx.beginPath(); ctx.arc(pt.x,pt.y,3,0,Math.PI*2);
1214
- ctx.fillStyle = fn.color; ctx.fill();
1215
- });
1216
-
1217
- // Nodes
1218
- this.nodes.forEach(n => this.drawNode(n));
1219
- }
1220
-
1221
- drawNode(n) {
1222
- const { ctx } = this;
1223
- const { x, y, w, h, color } = n;
1224
- const st = this.statuses[n.id];
1225
- const sel = this.selectedId === n.id;
1226
-
1227
- // Glow
1228
- if (st === 'active') { ctx.shadowColor = color; ctx.shadowBlur = 18; }
1229
- else if (sel) { ctx.shadowColor = color; ctx.shadowBlur = 10; }
1230
-
1231
- // BG
1232
- ctx.beginPath();
1233
- ctx.roundRect(x, y, w, h, 7);
1234
- ctx.fillStyle = st==='active' ? color+'1a' : st==='done' ? '#001f14' : '#0b1a2e';
1235
- ctx.fill();
1236
- ctx.shadowBlur = 0;
1237
-
1238
- // Border
1239
- ctx.beginPath();
1240
- ctx.roundRect(x, y, w, h, 7);
1241
- ctx.strokeStyle = st==='idle' ? '#1d3254' :
1242
- st==='active'? color :
1243
- st==='done' ? '#00ff88' : color;
1244
- ctx.lineWidth = sel ? 2.5 : (st==='active' ? 2 : 1.5);
1245
- ctx.stroke();
1246
-
1247
- // Status dot
1248
- const dotColor = { idle:'#1d3254', loading:CFG.amber, active:color, done:'#00ff88' }[st] || '#1d3254';
1249
- ctx.beginPath(); ctx.arc(x+w-9, y+9, 4, 0, Math.PI*2);
1250
- ctx.fillStyle = dotColor; ctx.fill();
1251
- if (st==='active') {
1252
- ctx.beginPath(); ctx.arc(x+w-9, y+9, 7, 0, Math.PI*2);
1253
- ctx.strokeStyle = dotColor+'55'; ctx.lineWidth=2; ctx.stroke();
1254
- }
1255
-
1256
- // Icon
1257
- ctx.font = '18px serif';
1258
- ctx.textAlign = 'center';
1259
- ctx.fillText(n.icon, x+w/2, y+h/2-3);
1260
-
1261
- // Label
1262
- ctx.font = `500 10px 'JetBrains Mono', monospace`;
1263
- ctx.fillStyle = st==='active' ? color : '#3d5a7a';
1264
- ctx.fillText(n.label, x+w/2, y+h-11);
1265
-
1266
- // Mini status
1267
- if (st !== 'idle') {
1268
- const lbl = { loading:'loading…', active:'processing', done:'done ✓' }[st] || '';
1269
- ctx.font = `8px 'JetBrains Mono', monospace`;
1270
- ctx.fillStyle = dotColor;
1271
- ctx.fillText(lbl, x+w/2, y+h-2);
1272
- }
1273
- }
1274
-
1275
- bezPt(x0,y0,cx1,cy1,cx2,cy2,x1,y1,t) {
1276
- const m=1-t;
1277
- return {
1278
- x: m*m*m*x0+3*m*m*t*cx1+3*m*t*t*cx2+t*t*t*x1,
1279
- y: m*m*m*y0+3*m*m*t*cy1+3*m*t*t*cy2+t*t*t*y1
1280
- };
1281
- }
1282
-
1283
- onClick(e) {
1284
- const r = this.canvas.getBoundingClientRect();
1285
- const mx = e.clientX-r.left, my = e.clientY-r.top;
1286
- const hit = this.nodes.find(n => mx>=n.x&&mx<=n.x+n.w&&my>=n.y&&my<=n.y+n.h);
1287
- if (hit) {
1288
- this.selectedId = hit.id;
1289
- this.openSettings(hit);
1290
  } else {
1291
- this.selectedId = null;
1292
- $('node-settings').classList.remove('open');
1293
  }
 
 
1294
  }
1295
-
1296
- openSettings(node) {
1297
- const panel = $('node-settings');
1298
- $('ns-title').textContent = `${node.icon} ${node.label}`;
1299
- $('ns-title').style.color = node.color;
1300
- $('ns-body').innerHTML = Object.entries(node.settings).map(([k,v]) => `
1301
- <div class="ns-row">
1302
- <div class="ns-label">${k}</div>
1303
- <input class="ns-input" value="${v}" data-node="${node.id}" data-key="${k}"
1304
- onchange="window.FE.onSettingChange(this)">
1305
- </div>
1306
- `).join('');
1307
- panel.classList.add('open');
1308
- }
1309
-
1310
- onSettingChange(input) {
1311
- const nodeId = input.dataset.node, key = input.dataset.key, val = input.value;
1312
- const node = this.nodeById(nodeId);
1313
- if (node) node.settings[key] = val;
1314
- // Apply live
1315
- if (nodeId==='search' && key==='Top-K') CFG.topK = parseInt(val)||5;
1316
- if (nodeId==='rerank' && key==='Keep') CFG.topKRerank = parseInt(val)||3;
1317
- if (nodeId==='llm' && key==='MaxTokens') CFG.maxTokens = parseInt(val)||180;
1318
- }
1319
-
1320
- setStatus(id, st) { this.statuses[id] = st; }
1321
-
1322
- setAllIdle() {
1323
- this.nodes.forEach(n => this.statuses[n.id] = 'idle');
1324
- this.particles = [];
1325
- }
1326
-
1327
- animEdgePromise(from, to) {
1328
- return new Promise(resolve => {
1329
- const p = { from, to, t: 0, speed: 0.03, done: resolve };
1330
- this.particles.push(p);
1331
- });
1332
- }
1333
-
1334
- nodeById(id) { return this.nodes.find(n=>n.id===id); }
1335
-
1336
- raf() {
1337
- // Advance particles
1338
- this.particles = this.particles.filter(p => {
1339
- p.t += p.speed;
1340
- if (p.t >= 1) { p.done?.(); return false; }
1341
- return true;
1342
- });
1343
- this.draw();
1344
- requestAnimationFrame(() => this.raf());
1345
- }
1346
- }
1347
-
1348
- /* ─────────────────────────────────────────────────────────── */
1349
- /* EXPOSE & WIRE UP */
1350
- /* ─────────────────────────────────────────────────────────── */
1351
- window.FE = null;
1352
-
1353
- function closeSettings() {
1354
- $('node-settings').classList.remove('open');
1355
- if (window.FE) window.FE.selectedId = null;
1356
- }
1357
- window.closeSettings = closeSettings;
1358
-
1359
- /* ─────────────────────────────────────────────────────────── */
1360
- /* MAIN INIT */
1361
- /* ─────────────────────────────────────────────────────────── */
1362
- async function main() {
1363
- // Node editor
1364
- window.FE = new FlowEditor();
1365
-
1366
- // IndexedDB
1367
- try {
1368
- await openIDB();
1369
- const saved = await idbGetAll();
1370
- ST.db = saved;
1371
- rebuildTable();
1372
- } catch(e) {
1373
- console.warn('IndexedDB unavailable:', e.message);
1374
- }
1375
-
1376
- // Send button
1377
- $('send-btn').addEventListener('click', () => {
1378
- const q = $('chat-input').value.trim();
1379
- if (q) { $('chat-input').value=''; runRAG(q); }
1380
- });
1381
- $('chat-input').addEventListener('keydown', e => {
1382
- if (e.key==='Enter' && !e.shiftKey && !ST.busy) {
1383
- const q = $('chat-input').value.trim();
1384
- if (q) { $('chat-input').value=''; runRAG(q); }
1385
- }
1386
  });
1387
-
1388
- // Add entry
1389
- $('add-btn').addEventListener('click', addEntry);
1390
-
1391
- // Load models
1392
- await loadModels();
1393
- }
1394
-
1395
- main();
1396
- </script>
1397
- </body>
1398
- </html>
 
1
+ function renderRet(items, lid, rr = false) {
2
+ const el = $(lid);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3
  el.innerHTML = '';
4
+ if (!items?.length) {
5
+ el.innerHTML = `<div class="empty"><div class="empty-icon">${rr ? '🔄' : '📚'}</div><span>No results</span></div>`;
6
  return;
7
  }
8
+
9
+ // --- FIX: Normalize reranking scores for display ---
10
+ let displayScores;
11
+ if (rr) {
12
+ const rawScores = items.map(item => item.rerankScore ?? item.score);
13
+ const minScore = Math.min(...rawScores);
14
+ const maxScore = Math.max(...rawScores);
15
+ const range = maxScore - minScore;
16
+ if (range > 0.0001 && rawScores.length > 1) {
17
+ // Min-max normalize: best = 1.0, worst = 0.0, others spread between
18
+ displayScores = rawScores.map(s => (s - minScore) / range);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
19
  } else {
20
+ // All scores essentially identical or single item
21
+ displayScores = rawScores.map(() => 1.0);
22
  }
23
+ } else {
24
+ displayScores = items.map(item => item.score);
25
  }
26
+ // --- End fix ---
27
+
28
+ items.forEach((item, i) => {
29
+ const d = document.createElement('div');
30
+ d.className = `ret-item ${rr ? 'sel' : ''}`;
31
+ const displayScore = displayScores[i]; // Use normalized score for display
32
+
33
+ d.innerHTML = `<div class="ret-rank">#${i + 1}</div>
34
+ <div class="ret-text">${escH(item.text)}</div>
35
+ <div class="ret-foot">
36
+ <span class="ret-meta">${item.source || '?'} · ${item.category || '?'}</span>
37
+ <div class="score-bar">
38
+ <div class="score-fill" style="width:0"></div>
39
+ </div>
40
+ <span class="score-pct">${(displayScore * 100).toFixed(1)}%</span>
41
+ </div>`;
42
+ el.appendChild(d);
43
+
44
+ setTimeout(() => {
45
+ d.classList.add(rr ? 'sel' : 'lit');
46
+ // Also animate the bar with the normalized score
47
+ d.querySelector('.score-fill').style.width = Math.min(100, Math.abs(displayScore) * 100) + '%';
48
+ }, i * 80);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
49
  });
50
+ }