quickgrid commited on
Commit
d190577
·
verified ·
1 Parent(s): a918b10

Update index.html

Browse files
Files changed (1) hide show
  1. index.html +1364 -415
index.html CHANGED
@@ -1,449 +1,1398 @@
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>Browser RAG Visualizer | Qwen 3.5 WebGPU</title>
7
- <script src="https://cdn.tailwindcss.com"></script>
8
- <script type="text/javascript" src="https://tampermonkey.github.io/litegraph.js/litegraph.js"></script>
9
- <link rel="stylesheet" type="text/css" href="https://tampermonkey.github.io/litegraph.js/css/litegraph.css">
10
- <style>
11
- body { background-color: #0f172a; color: #e2e8f0; font-family: ui-sans-serif, system-ui, sans-serif; overflow: hidden; }
12
- .grid-layout { display: grid; grid-template-columns: 25% 45% 30%; height: 100vh; gap: 2px; background: #1e293b; }
13
- .panel { background-color: #0f172a; padding: 1rem; overflow-y: auto; display: flex; flex-direction: column; }
14
- .glass-header { background: rgba(15, 23, 42, 0.8); backdrop-filter: blur(4px); position: sticky; top: 0; z-index: 10; padding-bottom: 0.5rem;}
15
-
16
- /* Custom Scrollbar */
17
- ::-webkit-scrollbar { width: 6px; }
18
- ::-webkit-scrollbar-track { background: #1e293b; }
19
- ::-webkit-scrollbar-thumb { background: #475569; border-radius: 4px; }
20
-
21
- /* Animations */
22
- @keyframes pulse-row {
23
- 0% { background-color: #0f172a; }
24
- 50% { background-color: #1e40af; transform: scale(1.02); }
25
- 100% { background-color: #1e293b; transform: scale(1); }
26
- }
27
- .highlight-top-k { animation: pulse-row 1.5s ease-in-out; border-left: 4px solid #3b82f6; background-color: #1e293b; }
28
-
29
- @keyframes slide-up {
30
- from { opacity: 0; transform: translateY(10px); }
31
- to { opacity: 1; transform: translateY(0); }
32
- }
33
- .animate-entry { animation: slide-up 0.3s ease-out forwards; }
34
-
35
- .vector-text { font-family: monospace; font-size: 0.7rem; color: #64748b; word-break: break-all; }
36
-
37
- /* LiteGraph Customizations */
38
- .lgraphcanvas { background-color: #0f172a !important; }
39
- </style>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
40
  </head>
41
  <body>
42
 
43
- <div class="grid-layout">
44
- <!-- LEFT PANEL: Chat & Data Entry -->
45
- <div class="panel border-r border-slate-700">
46
- <div class="glass-header border-b border-slate-700 mb-4">
47
- <h2 class="text-xl font-bold text-blue-400">1. Interaction</h2>
48
- </div>
49
-
50
- <!-- Add to Vector DB -->
51
- <div class="mb-6 bg-slate-800 p-3 rounded-lg">
52
- <h3 class="text-sm font-semibold mb-2 text-slate-300">Add Knowledge</h3>
53
- <textarea id="db-input" rows="2" class="w-full bg-slate-900 border border-slate-700 rounded p-2 text-sm focus:outline-none focus:border-blue-500" placeholder="Enter text to embed..."></textarea>
54
- <button id="btn-add-db" class="mt-2 w-full bg-slate-700 hover:bg-blue-600 text-white text-sm py-1.5 rounded transition">Embed & Add to DB</button>
55
- </div>
 
 
 
 
 
 
 
 
56
 
57
- <!-- Chat Interface -->
58
- <div class="flex-grow flex flex-col min-h-0">
59
- <h3 class="text-sm font-semibold mb-2 text-slate-300">Qwen 3.5 Chat</h3>
60
- <div id="chat-window" class="flex-grow bg-slate-900 border border-slate-700 rounded-lg p-3 mb-3 overflow-y-auto flex flex-col gap-2">
61
- <div class="text-xs text-slate-500 text-center">Models are downloading. Watch the Node Editor...</div>
62
- </div>
63
- <div class="flex gap-2">
64
- <input type="text" id="chat-input" class="flex-grow bg-slate-900 border border-slate-700 rounded p-2 text-sm focus:outline-none focus:border-blue-500" placeholder="Ask a question..." disabled>
65
- <button id="btn-chat" class="bg-blue-600 hover:bg-blue-500 text-white px-4 rounded text-sm disabled:opacity-50 transition" disabled>Send</button>
66
- </div>
67
- </div>
 
 
 
 
 
 
68
  </div>
 
 
 
69
 
70
- <!-- MIDDLE PANEL: Visual Node Flow -->
71
- <div class="panel p-0 relative">
72
- <div class="absolute top-4 left-4 z-10 glass-header rounded px-3 py-1 border border-slate-700">
73
- <h2 class="text-xl font-bold text-emerald-400">2. Pipeline Process</h2>
74
- </div>
75
- <canvas id="litegraph-canvas" class="w-full h-full"></canvas>
 
 
 
 
 
 
 
76
  </div>
 
 
 
 
 
77
 
78
- <!-- RIGHT PANEL: Vector DB & Reranking -->
79
- <div class="panel border-l border-slate-700">
80
- <div class="glass-header border-b border-slate-700 mb-4">
81
- <h2 class="text-xl font-bold text-purple-400">3. Memory & Retrieval</h2>
 
 
 
 
 
 
 
 
 
 
 
82
  </div>
83
-
84
- <!-- Reranker Visualization -->
85
- <div class="mb-4 bg-slate-800 rounded-lg border border-slate-700 overflow-hidden flex flex-col" style="height: 30%;">
86
- <div class="bg-slate-700 px-3 py-1 text-xs font-bold text-slate-300 flex justify-between">
87
- <span>Reranker (Top-K Sort)</span>
88
- <span id="rerank-status" class="text-purple-400">Idle</span>
89
- </div>
90
- <div id="reranker-list" class="p-2 overflow-y-auto flex-grow flex flex-col gap-1">
91
- <!-- Reranked items appear here -->
92
- </div>
 
93
  </div>
 
 
 
94
 
95
- <!-- Vector DB Table -->
96
- <div class="flex-grow bg-slate-800 rounded-lg border border-slate-700 overflow-hidden flex flex-col">
97
- <div class="bg-slate-700 px-3 py-1 text-xs font-bold text-slate-300 flex justify-between">
98
- <span>Vector Table (Mock LanceDB)</span>
99
- <span id="db-count" class="text-blue-400">0 Rows</span>
100
- </div>
101
- <div class="overflow-y-auto p-2">
102
- <table class="w-full text-left text-xs">
103
- <thead>
104
- <tr class="text-slate-400 border-b border-slate-600">
105
- <th class="pb-1 w-12">ID</th>
106
- <th class="pb-1 w-1/3">Metadata (Text)</th>
107
- <th class="pb-1">Vector (Truncated)</th>
108
- </tr>
109
- </thead>
110
- <tbody id="db-tbody">
111
- <!-- DB Rows -->
112
- </tbody>
113
- </table>
114
- </div>
115
- </div>
116
  </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
117
  </div>
118
 
 
 
 
 
 
 
119
  <script type="module">
120
- // -------------------------------------------------------------------
121
- // 1. IMPORTS & PIPELINE SETUP
122
- // -------------------------------------------------------------------
123
- import { pipeline, env, cos_sim } from 'https://cdn.jsdelivr.net/npm/@huggingface/transformers@3.0.0/dist/transformers.min.js';
124
-
125
- // Optimize for browser
126
- env.allowLocalModels = false;
127
- env.backends.onnx.wasm.numThreads = 1;
128
-
129
- let embedder, generator, reranker;
130
- const vectorDB = []; // In-memory fallback acting as LanceDB
131
- let dbIdCounter = 1;
132
-
133
- // -------------------------------------------------------------------
134
- // 2. LITEGRAPH (NODE EDITOR) SETUP
135
- // -------------------------------------------------------------------
136
- const graph = new LGraph();
137
- const canvas = new LGraphCanvas("#litegraph-canvas", graph);
138
-
139
- // Resize handling
140
- window.addEventListener("resize", () => {
141
- canvas.resize();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
142
  });
 
 
 
 
 
 
143
 
144
- // Custom Nodes
145
- function InputNode() {
146
- this.addOutput("Query", "string");
147
- this.title = "User Input";
148
- this.color = "#1e3a8a";
149
- }
150
- LGraphCanvas.registerNode("RAG/Input", InputNode);
151
-
152
- function EmbedderNode() {
153
- this.addInput("Text", "string");
154
- this.addOutput("Vector", "array");
155
- this.title = "MiniLM Embedder";
156
- this.color = "#064e3b";
157
- this.properties = { model: "all-MiniLM-L6-v2" };
158
- this.addWidget("text", "Model", this.properties.model);
159
- }
160
- LGraphCanvas.registerNode("RAG/Embedder", EmbedderNode);
161
-
162
- function DBNode() {
163
- this.addInput("Query Vector", "array");
164
- this.addOutput("Top-K Rows", "array");
165
- this.title = "Vector DB (Search)";
166
- this.color = "#4c1d95";
167
- this.properties = { top_k: 3, metric: "Cosine" };
168
- this.addWidget("number", "Top K", this.properties.top_k, (v) => this.properties.top_k = v, {min:1, max:10, step:10});
169
- }
170
- LGraphCanvas.registerNode("RAG/VectorDB", DBNode);
171
 
172
- function RerankerNode() {
173
- this.addInput("Top-K Rows", "array");
174
- this.addOutput("Context", "string");
175
- this.title = "BGE Reranker";
176
- this.color = "#831843";
177
- }
178
- LGraphCanvas.registerNode("RAG/Reranker", RerankerNode);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
179
 
180
- function LLMNode() {
181
- this.addInput("Context", "string");
182
- this.addInput("Query", "string");
183
- this.title = "Qwen 3.5 0.8B (WebGPU)";
184
- this.color = "#701a75";
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
185
  }
186
- LGraphCanvas.registerNode("RAG/Generator", LLMNode);
187
-
188
- // Build Graph
189
- const nodeInput = LiteGraph.createNode("RAG/Input");
190
- nodeInput.pos = [50, 150];
191
- graph.add(nodeInput);
192
-
193
- const nodeEmbedder = LiteGraph.createNode("RAG/Embedder");
194
- nodeEmbedder.pos = [250, 150];
195
- graph.add(nodeEmbedder);
196
-
197
- const nodeDB = LiteGraph.createNode("RAG/VectorDB");
198
- nodeDB.pos = [500, 150];
199
- graph.add(nodeDB);
200
-
201
- const nodeReranker = LiteGraph.createNode("RAG/Reranker");
202
- nodeReranker.pos = [750, 150];
203
- graph.add(nodeReranker);
204
-
205
- const nodeLLM = LiteGraph.createNode("RAG/Generator");
206
- nodeLLM.pos = [500, 350];
207
- graph.add(nodeLLM);
208
-
209
- // Connect them
210
- nodeInput.connect(0, nodeEmbedder, 0);
211
- nodeInput.connect(0, nodeLLM, 1);
212
- nodeEmbedder.connect(0, nodeDB, 0);
213
- nodeDB.connect(0, nodeReranker, 0);
214
- nodeReranker.connect(0, nodeLLM, 0);
215
- graph.start();
216
-
217
- // Helper to pulse nodes
218
- function highlightNode(node) {
219
- const originalColor = node.color;
220
- node.color = "#eab308"; // yellow
221
- canvas.setDirty(true, true);
222
- setTimeout(() => { node.color = originalColor; canvas.setDirty(true, true); }, 1000);
223
  }
 
224
 
225
- // -------------------------------------------------------------------
226
- // 3. MODEL INITIALIZATION
227
- // -------------------------------------------------------------------
228
- async function initModels() {
229
- const chatWindow = document.getElementById("chat-window");
230
-
231
- try {
232
- chatWindow.innerHTML += `<div class="text-xs text-blue-400">Loading Embedder (MiniLM)...</div>`;
233
- embedder = await pipeline('feature-extraction', 'Xenova/all-MiniLM-L6-v2', { dtype: 'fp32' });
234
-
235
- chatWindow.innerHTML += `<div class="text-xs text-purple-400">Loading Reranker (BGE)...</div>`;
236
- reranker = await pipeline('text-classification', 'Xenova/bge-reranker-base');
237
-
238
- chatWindow.innerHTML += `<div class="text-xs text-emerald-400">Loading LLM (Qwen 1.5/3.5 0.5B WebGPU)...</div>`;
239
- // Using a slightly smaller Qwen variant for guaranteed WebGPU browser stability in this demo
240
- generator = await pipeline('text-generation', 'Xenova/Qwen1.5-0.5B-Chat', { device: 'webgpu', dtype: 'q4' });
241
-
242
- chatWindow.innerHTML += `<div class="text-sm text-green-400 mt-2">All models ready. System online.</div>`;
243
- document.getElementById("chat-input").disabled = false;
244
- document.getElementById("btn-chat").disabled = false;
245
- } catch (e) {
246
- console.error(e);
247
- chatWindow.innerHTML += `<div class="text-xs text-red-400">Error loading models. Check console or WebGPU support.</div>`;
248
- }
249
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
250
 
251
- // -------------------------------------------------------------------
252
- // 4. UI INTERACTION & LOGIC
253
- // -------------------------------------------------------------------
254
-
255
- // Add to DB
256
- document.getElementById("btn-add-db").addEventListener("click", async () => {
257
- const text = document.getElementById("db-input").value.trim();
258
- if(!text || !embedder) return;
259
-
260
- highlightNode(nodeEmbedder);
261
-
262
- // 1. Generate Embedding
263
- const output = await embedder(text, { pooling: 'mean', normalize: true });
264
- const vector = Array.from(output.data);
265
-
266
- // 2. Save to "LanceDB" Table
267
- const entry = {
268
- id: dbIdCounter++,
269
- date: new Date().toISOString().split('T')[0],
270
- text: text,
271
- vector: vector
272
- };
273
- vectorDB.push(entry);
274
-
275
- // 3. Visualize
276
- document.getElementById("db-count").innerText = `${vectorDB.length} Rows`;
277
- const tr = document.createElement("tr");
278
- tr.className = "border-b border-slate-700 animate-entry";
279
- tr.id = `db-row-${entry.id}`;
280
- tr.innerHTML = `
281
- <td class="py-2 text-slate-300">#${entry.id}</td>
282
- <td class="py-2 text-slate-300 truncate max-w-[100px]" title="${text}">${text}</td>
283
- <td class="py-2 vector-text">[${vector.slice(0, 5).map(n => n.toFixed(3)).join(', ')}...]</td>
284
- `;
285
- document.getElementById("db-tbody").prepend(tr);
286
- document.getElementById("db-input").value = "";
287
  });
 
288
 
289
- // Chat / Retrieval
290
- document.getElementById("btn-chat").addEventListener("click", async () => {
291
- const query = document.getElementById("chat-input").value.trim();
292
- if(!query || !generator) return;
293
-
294
- const chatWindow = document.getElementById("chat-window");
295
- chatWindow.innerHTML += `<div class="bg-slate-800 p-2 rounded text-sm self-end max-w-[80%]">${query}</div>`;
296
- document.getElementById("chat-input").value = "";
297
-
298
- // Visualize Node Flow
299
- highlightNode(nodeInput);
300
-
301
- // 1. Embed Query
302
- highlightNode(nodeEmbedder);
303
- const queryOut = await embedder(query, { pooling: 'mean', normalize: true });
304
- const queryVector = Array.from(queryOut.data);
305
-
306
- // 2. Vector DB Search (Cosine Similarity)
307
- highlightNode(nodeDB);
308
- let results = vectorDB.map(row => {
309
- return { ...row, score: cos_sim(queryVector, row.vector) };
310
- });
311
-
312
- // Sort by initial similarity and take Top K
313
- const topK = nodeDB.properties.top_k;
314
- results.sort((a, b) => b.score - a.score);
315
- let topResults = results.slice(0, topK);
316
-
317
- // Highlight rows in table
318
- document.querySelectorAll("tr").forEach(tr => tr.classList.remove("highlight-top-k"));
319
- topResults.forEach(res => {
320
- const rowEl = document.getElementById(`db-row-${res.id}`);
321
- if(rowEl) {
322
- rowEl.classList.add("highlight-top-k");
323
- // Scroll row into view
324
- rowEl.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
325
- }
326
- });
327
-
328
- // 3. Reranking
329
- if(topResults.length > 0 && reranker) {
330
- highlightNode(nodeReranker);
331
- document.getElementById("rerank-status").innerText = "Processing...";
332
- const rerankContainer = document.getElementById("reranker-list");
333
- rerankContainer.innerHTML = "";
334
-
335
- // Populate initial rerank UI
336
- for(let res of topResults) {
337
- rerankContainer.innerHTML += `
338
- <div id="rr-${res.id}" class="bg-slate-800 border border-slate-600 p-1 rounded text-xs animate-entry">
339
- <span class="text-slate-400">ID: ${res.id}</span> | Initial Score: ${(res.score).toFixed(3)}
340
- <div class="truncate text-slate-300 mt-1">${res.text}</div>
341
- </div>`;
342
- }
343
-
344
- // Run Cross-Encoder
345
- const rerankPromises = topResults.map(async (res) => {
346
- // bge-reranker outputs logits, higher is betterHere is the complete code for your serverless, static Hugging Face Spaces app.
347
-
348
- This single `index.html` file combines the HTML structure, CSS Grid layout, and JavaScript logic using **Transformers.js (v3)**, **LiteGraph.js** for the node editor, and a browser-native vector database implementation.
349
-
350
- To deploy this, simply create a new Space on Hugging Face, select **Static**, and paste this code into the `index.html` file.
351
-
352
- ### `index.html`
353
-
354
- ```html
355
- <!DOCTYPE html>
356
- <html lang="en">
357
- <head>
358
- <meta charset="UTF-8">
359
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
360
- <title>Visual RAG Pipeline</title>
361
- <!-- LiteGraph CSS -->
362
- <link rel="stylesheet" type="text/css" href="[https://tamats.com/projects/litegraph/css/litegraph.css](https://tamats.com/projects/litegraph/css/litegraph.css)">
363
- <style>
364
- :root {
365
- --bg-color: #1e1e2e;
366
- --panel-bg: #282a36;
367
- --text-color: #f8f8f2;
368
- --accent: #bd93f9;
369
- --accent-hover: #ff79c6;
370
- --border: #44475a;
371
- }
372
 
373
- body {
374
- margin: 0;
375
- padding: 0;
376
- background-color: var(--bg-color);
377
- color: var(--text-color);
378
- font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
379
- display: grid;
380
- grid-template-columns: 300px 1fr 350px;
381
- grid-template-rows: 60vh 40vh;
382
- height: 100vh;
383
- overflow: hidden;
384
- }
385
 
386
- .panel {
387
- background: var(--panel-bg);
388
- border: 1px solid var(--border);
389
- border-radius: 8px;
390
- margin: 8px;
391
- display: flex;
392
- flex-direction: column;
393
- overflow: hidden;
394
- }
395
 
396
- .panel-header {
397
- background: var(--border);
398
- padding: 10px;
399
- font-weight: bold;
400
- text-align: center;
401
- }
402
 
403
- /* Chat Section (Left) */
404
- #chat-panel { grid-column: 1; grid-row: 1 / 3; }
405
- #chat-history { flex: 1; overflow-y: auto; padding: 10px; }
406
- .message { margin-bottom: 10px; padding: 8px; border-radius: 5px; }
407
- .user-msg { background: var(--accent); color: #000; align-self: flex-end; }
408
- .bot-msg { background: var(--border); }
409
- #chat-input-container { display: flex; padding: 10px; border-top: 1px solid var(--border); }
410
- input[type="text"] { flex: 1; padding: 8px; border-radius: 4px; border: none; background: #333; color: white; }
411
- button { background: var(--accent); color: black; border: none; padding: 8px 12px; margin-left: 5px; border-radius: 4px; cursor: pointer; font-weight: bold; }
412
- button:hover { background: var(--accent-hover); }
413
-
414
- /* Node Editor (Center Bottom) */
415
- #node-panel { grid-column: 2; grid-row: 2; }
416
- #litegraph-canvas { width: 100%; height: 100%; }
417
-
418
- /* DB Input & Reranker (Center Top) */
419
- #process-panel { grid-column: 2; grid-row: 1; display: flex; flex-direction: column; padding: 10px; }
420
- .status-box { padding: 15px; background: #333; border-radius: 5px; margin-bottom: 10px; flex: 1; overflow-y: auto;}
421
-
422
- /* Vector DB Table (Right) */
423
- #db-panel { grid-column: 3; grid-row: 1 / 3; }
424
- #db-table-container { flex: 1; overflow-y: auto; padding: 5px; }
425
- table { width: 100%; border-collapse: collapse; font-size: 0.85em; }
426
- th, td { border: 1px solid var(--border); padding: 5px; text-align: left; }
427
- .vector-cell { font-family: monospace; color: #8be9fd; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; max-width: 100px; }
428
-
429
- /* Animations */
430
- @keyframes highlightPulse {
431
- 0% { background-color: transparent; }
432
- 50% { background-color: rgba(189, 147, 249, 0.5); }
433
- 100% { background-color: rgba(189, 147, 249, 0.2); }
434
- }
435
- .highlight { animation: highlightPulse 1.5s ease-out forwards; border: 2px solid var(--accent); }
436
-
437
- #loading-overlay {
438
- position: absolute; top: 0; left: 0; width: 100%; height: 100%;
439
- background: rgba(0,0,0,0.8); z-index: 100;
440
- display: flex; flex-direction: column; justify-content: center; align-items: center;
441
- }
442
- </style>
443
- </head>
444
- <body>
445
 
446
- <!-- Loading Overlay -->
447
- <div id="loading-overlay">
448
- <h2>Downloading Quantized WebGPU Models...</h2>
449
- <p>Qwen3.5-0.8B, MiniLM, and BGE-Reran
 
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 · powered by transformers.js</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>