quickgrid commited on
Commit
6fbbbb9
·
verified ·
1 Parent(s): 455e371

Update index.html

Browse files
Files changed (1) hide show
  1. index.html +449 -19
index.html CHANGED
@@ -1,19 +1,449 @@
1
- <!doctype html>
2
- <html>
3
- <head>
4
- <meta charset="utf-8" />
5
- <meta name="viewport" content="width=device-width" />
6
- <title>My static Space</title>
7
- <link rel="stylesheet" href="style.css" />
8
- </head>
9
- <body>
10
- <div class="card">
11
- <h1>Welcome to your static Space!</h1>
12
- <p>You can modify this app directly by editing <i>index.html</i> in the Files and versions tab.</p>
13
- <p>
14
- Also don't forget to check the
15
- <a href="https://huggingface.co/docs/hub/spaces" target="_blank">Spaces documentation</a>.
16
- </p>
17
- </div>
18
- </body>
19
- </html>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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