| <!DOCTYPE html> |
| <html lang="en"> |
| <head> |
| <meta charset="UTF-8"> |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> |
| <title>Browser RAG Visualizer | Qwen 3.5 WebGPU</title> |
| <script src="https://cdn.tailwindcss.com"></script> |
| <script type="text/javascript" src="https://tampermonkey.github.io/litegraph.js/litegraph.js"></script> |
| <link rel="stylesheet" type="text/css" href="https://tampermonkey.github.io/litegraph.js/css/litegraph.css"> |
| <style> |
| body { background-color: #0f172a; color: #e2e8f0; font-family: ui-sans-serif, system-ui, sans-serif; overflow: hidden; } |
| .grid-layout { display: grid; grid-template-columns: 25% 45% 30%; height: 100vh; gap: 2px; background: #1e293b; } |
| .panel { background-color: #0f172a; padding: 1rem; overflow-y: auto; display: flex; flex-direction: column; } |
| .glass-header { background: rgba(15, 23, 42, 0.8); backdrop-filter: blur(4px); position: sticky; top: 0; z-index: 10; padding-bottom: 0.5rem;} |
| |
| |
| ::-webkit-scrollbar { width: 6px; } |
| ::-webkit-scrollbar-track { background: #1e293b; } |
| ::-webkit-scrollbar-thumb { background: #475569; border-radius: 4px; } |
| |
| |
| @keyframes pulse-row { |
| 0% { background-color: #0f172a; } |
| 50% { background-color: #1e40af; transform: scale(1.02); } |
| 100% { background-color: #1e293b; transform: scale(1); } |
| } |
| .highlight-top-k { animation: pulse-row 1.5s ease-in-out; border-left: 4px solid #3b82f6; background-color: #1e293b; } |
| |
| @keyframes slide-up { |
| from { opacity: 0; transform: translateY(10px); } |
| to { opacity: 1; transform: translateY(0); } |
| } |
| .animate-entry { animation: slide-up 0.3s ease-out forwards; } |
| |
| .vector-text { font-family: monospace; font-size: 0.7rem; color: #64748b; word-break: break-all; } |
| |
| |
| .lgraphcanvas { background-color: #0f172a !important; } |
| </style> |
| </head> |
| <body> |
|
|
| <div class="grid-layout"> |
| |
| <div class="panel border-r border-slate-700"> |
| <div class="glass-header border-b border-slate-700 mb-4"> |
| <h2 class="text-xl font-bold text-blue-400">1. Interaction</h2> |
| </div> |
| |
| |
| <div class="mb-6 bg-slate-800 p-3 rounded-lg"> |
| <h3 class="text-sm font-semibold mb-2 text-slate-300">Add Knowledge</h3> |
| <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> |
| <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> |
| </div> |
|
|
| |
| <div class="flex-grow flex flex-col min-h-0"> |
| <h3 class="text-sm font-semibold mb-2 text-slate-300">Qwen 3.5 Chat</h3> |
| <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"> |
| <div class="text-xs text-slate-500 text-center">Models are downloading. Watch the Node Editor...</div> |
| </div> |
| <div class="flex gap-2"> |
| <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> |
| <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> |
| </div> |
| </div> |
| </div> |
|
|
| |
| <div class="panel p-0 relative"> |
| <div class="absolute top-4 left-4 z-10 glass-header rounded px-3 py-1 border border-slate-700"> |
| <h2 class="text-xl font-bold text-emerald-400">2. Pipeline Process</h2> |
| </div> |
| <canvas id="litegraph-canvas" class="w-full h-full"></canvas> |
| </div> |
|
|
| |
| <div class="panel border-l border-slate-700"> |
| <div class="glass-header border-b border-slate-700 mb-4"> |
| <h2 class="text-xl font-bold text-purple-400">3. Memory & Retrieval</h2> |
| </div> |
|
|
| |
| <div class="mb-4 bg-slate-800 rounded-lg border border-slate-700 overflow-hidden flex flex-col" style="height: 30%;"> |
| <div class="bg-slate-700 px-3 py-1 text-xs font-bold text-slate-300 flex justify-between"> |
| <span>Reranker (Top-K Sort)</span> |
| <span id="rerank-status" class="text-purple-400">Idle</span> |
| </div> |
| <div id="reranker-list" class="p-2 overflow-y-auto flex-grow flex flex-col gap-1"> |
| |
| </div> |
| </div> |
|
|
| |
| <div class="flex-grow bg-slate-800 rounded-lg border border-slate-700 overflow-hidden flex flex-col"> |
| <div class="bg-slate-700 px-3 py-1 text-xs font-bold text-slate-300 flex justify-between"> |
| <span>Vector Table (Mock LanceDB)</span> |
| <span id="db-count" class="text-blue-400">0 Rows</span> |
| </div> |
| <div class="overflow-y-auto p-2"> |
| <table class="w-full text-left text-xs"> |
| <thead> |
| <tr class="text-slate-400 border-b border-slate-600"> |
| <th class="pb-1 w-12">ID</th> |
| <th class="pb-1 w-1/3">Metadata (Text)</th> |
| <th class="pb-1">Vector (Truncated)</th> |
| </tr> |
| </thead> |
| <tbody id="db-tbody"> |
| |
| </tbody> |
| </table> |
| </div> |
| </div> |
| </div> |
| </div> |
|
|
| <script type="module"> |
| // ------------------------------------------------------------------- |
| // 1. IMPORTS & PIPELINE SETUP |
| // ------------------------------------------------------------------- |
| import { pipeline, env, cos_sim } from 'https://cdn.jsdelivr.net/npm/@huggingface/transformers@3.0.0/dist/transformers.min.js'; |
| |
| // Optimize for browser |
| env.allowLocalModels = false; |
| env.backends.onnx.wasm.numThreads = 1; |
| |
| let embedder, generator, reranker; |
| const vectorDB = []; // In-memory fallback acting as LanceDB |
| let dbIdCounter = 1; |
| |
| // ------------------------------------------------------------------- |
| // 2. LITEGRAPH (NODE EDITOR) SETUP |
| // ------------------------------------------------------------------- |
| const graph = new LGraph(); |
| const canvas = new LGraphCanvas("#litegraph-canvas", graph); |
| |
| // Resize handling |
| window.addEventListener("resize", () => { |
| canvas.resize(); |
| }); |
| |
| // Custom Nodes |
| function InputNode() { |
| this.addOutput("Query", "string"); |
| this.title = "User Input"; |
| this.color = "#1e3a8a"; |
| } |
| LGraphCanvas.registerNode("RAG/Input", InputNode); |
| |
| function EmbedderNode() { |
| this.addInput("Text", "string"); |
| this.addOutput("Vector", "array"); |
| this.title = "MiniLM Embedder"; |
| this.color = "#064e3b"; |
| this.properties = { model: "all-MiniLM-L6-v2" }; |
| this.addWidget("text", "Model", this.properties.model); |
| } |
| LGraphCanvas.registerNode("RAG/Embedder", EmbedderNode); |
| |
| function DBNode() { |
| this.addInput("Query Vector", "array"); |
| this.addOutput("Top-K Rows", "array"); |
| this.title = "Vector DB (Search)"; |
| this.color = "#4c1d95"; |
| this.properties = { top_k: 3, metric: "Cosine" }; |
| this.addWidget("number", "Top K", this.properties.top_k, (v) => this.properties.top_k = v, {min:1, max:10, step:10}); |
| } |
| LGraphCanvas.registerNode("RAG/VectorDB", DBNode); |
| |
| function RerankerNode() { |
| this.addInput("Top-K Rows", "array"); |
| this.addOutput("Context", "string"); |
| this.title = "BGE Reranker"; |
| this.color = "#831843"; |
| } |
| LGraphCanvas.registerNode("RAG/Reranker", RerankerNode); |
| |
| function LLMNode() { |
| this.addInput("Context", "string"); |
| this.addInput("Query", "string"); |
| this.title = "Qwen 3.5 0.8B (WebGPU)"; |
| this.color = "#701a75"; |
| } |
| LGraphCanvas.registerNode("RAG/Generator", LLMNode); |
| |
| // Build Graph |
| const nodeInput = LiteGraph.createNode("RAG/Input"); |
| nodeInput.pos = [50, 150]; |
| graph.add(nodeInput); |
| |
| const nodeEmbedder = LiteGraph.createNode("RAG/Embedder"); |
| nodeEmbedder.pos = [250, 150]; |
| graph.add(nodeEmbedder); |
| |
| const nodeDB = LiteGraph.createNode("RAG/VectorDB"); |
| nodeDB.pos = [500, 150]; |
| graph.add(nodeDB); |
| |
| const nodeReranker = LiteGraph.createNode("RAG/Reranker"); |
| nodeReranker.pos = [750, 150]; |
| graph.add(nodeReranker); |
| |
| const nodeLLM = LiteGraph.createNode("RAG/Generator"); |
| nodeLLM.pos = [500, 350]; |
| graph.add(nodeLLM); |
| |
| // Connect them |
| nodeInput.connect(0, nodeEmbedder, 0); |
| nodeInput.connect(0, nodeLLM, 1); |
| nodeEmbedder.connect(0, nodeDB, 0); |
| nodeDB.connect(0, nodeReranker, 0); |
| nodeReranker.connect(0, nodeLLM, 0); |
| graph.start(); |
| |
| // Helper to pulse nodes |
| function highlightNode(node) { |
| const originalColor = node.color; |
| node.color = "#eab308"; // yellow |
| canvas.setDirty(true, true); |
| setTimeout(() => { node.color = originalColor; canvas.setDirty(true, true); }, 1000); |
| } |
| |
| // ------------------------------------------------------------------- |
| // 3. MODEL INITIALIZATION |
| // ------------------------------------------------------------------- |
| async function initModels() { |
| const chatWindow = document.getElementById("chat-window"); |
| |
| try { |
| chatWindow.innerHTML += `<div class="text-xs text-blue-400">Loading Embedder (MiniLM)...</div>`; |
| embedder = await pipeline('feature-extraction', 'Xenova/all-MiniLM-L6-v2', { dtype: 'fp32' }); |
| |
| chatWindow.innerHTML += `<div class="text-xs text-purple-400">Loading Reranker (BGE)...</div>`; |
| reranker = await pipeline('text-classification', 'Xenova/bge-reranker-base'); |
| |
| chatWindow.innerHTML += `<div class="text-xs text-emerald-400">Loading LLM (Qwen 1.5/3.5 0.5B WebGPU)...</div>`; |
| // Using a slightly smaller Qwen variant for guaranteed WebGPU browser stability in this demo |
| generator = await pipeline('text-generation', 'Xenova/Qwen1.5-0.5B-Chat', { device: 'webgpu', dtype: 'q4' }); |
| |
| chatWindow.innerHTML += `<div class="text-sm text-green-400 mt-2">All models ready. System online.</div>`; |
| document.getElementById("chat-input").disabled = false; |
| document.getElementById("btn-chat").disabled = false; |
| } catch (e) { |
| console.error(e); |
| chatWindow.innerHTML += `<div class="text-xs text-red-400">Error loading models. Check console or WebGPU support.</div>`; |
| } |
| } |
| |
| // ------------------------------------------------------------------- |
| // 4. UI INTERACTION & LOGIC |
| // ------------------------------------------------------------------- |
| |
| // Add to DB |
| document.getElementById("btn-add-db").addEventListener("click", async () => { |
| const text = document.getElementById("db-input").value.trim(); |
| if(!text || !embedder) return; |
| |
| highlightNode(nodeEmbedder); |
| |
| // 1. Generate Embedding |
| const output = await embedder(text, { pooling: 'mean', normalize: true }); |
| const vector = Array.from(output.data); |
| |
| // 2. Save to "LanceDB" Table |
| const entry = { |
| id: dbIdCounter++, |
| date: new Date().toISOString().split('T')[0], |
| text: text, |
| vector: vector |
| }; |
| vectorDB.push(entry); |
| |
| // 3. Visualize |
| document.getElementById("db-count").innerText = `${vectorDB.length} Rows`; |
| const tr = document.createElement("tr"); |
| tr.className = "border-b border-slate-700 animate-entry"; |
| tr.id = `db-row-${entry.id}`; |
| tr.innerHTML = ` |
| <td class="py-2 text-slate-300">#${entry.id}</td> |
| <td class="py-2 text-slate-300 truncate max-w-[100px]" title="${text}">${text}</td> |
| <td class="py-2 vector-text">[${vector.slice(0, 5).map(n => n.toFixed(3)).join(', ')}...]</td> |
| `; |
| document.getElementById("db-tbody").prepend(tr); |
| document.getElementById("db-input").value = ""; |
| }); |
| |
| // Chat / Retrieval |
| document.getElementById("btn-chat").addEventListener("click", async () => { |
| const query = document.getElementById("chat-input").value.trim(); |
| if(!query || !generator) return; |
| |
| const chatWindow = document.getElementById("chat-window"); |
| chatWindow.innerHTML += `<div class="bg-slate-800 p-2 rounded text-sm self-end max-w-[80%]">${query}</div>`; |
| document.getElementById("chat-input").value = ""; |
| |
| // Visualize Node Flow |
| highlightNode(nodeInput); |
| |
| // 1. Embed Query |
| highlightNode(nodeEmbedder); |
| const queryOut = await embedder(query, { pooling: 'mean', normalize: true }); |
| const queryVector = Array.from(queryOut.data); |
| |
| // 2. Vector DB Search (Cosine Similarity) |
| highlightNode(nodeDB); |
| let results = vectorDB.map(row => { |
| return { ...row, score: cos_sim(queryVector, row.vector) }; |
| }); |
| |
| // Sort by initial similarity and take Top K |
| const topK = nodeDB.properties.top_k; |
| results.sort((a, b) => b.score - a.score); |
| let topResults = results.slice(0, topK); |
| |
| // Highlight rows in table |
| document.querySelectorAll("tr").forEach(tr => tr.classList.remove("highlight-top-k")); |
| topResults.forEach(res => { |
| const rowEl = document.getElementById(`db-row-${res.id}`); |
| if(rowEl) { |
| rowEl.classList.add("highlight-top-k"); |
| // Scroll row into view |
| rowEl.scrollIntoView({ behavior: 'smooth', block: 'nearest' }); |
| } |
| }); |
| |
| // 3. Reranking |
| if(topResults.length > 0 && reranker) { |
| highlightNode(nodeReranker); |
| document.getElementById("rerank-status").innerText = "Processing..."; |
| const rerankContainer = document.getElementById("reranker-list"); |
| rerankContainer.innerHTML = ""; |
| |
| // Populate initial rerank UI |
| for(let res of topResults) { |
| rerankContainer.innerHTML += ` |
| <div id="rr-${res.id}" class="bg-slate-800 border border-slate-600 p-1 rounded text-xs animate-entry"> |
| <span class="text-slate-400">ID: ${res.id}</span> | Initial Score: ${(res.score).toFixed(3)} |
| <div class="truncate text-slate-300 mt-1">${res.text}</div> |
| </div>`; |
| } |
| |
| // Run Cross-Encoder |
| const rerankPromises = topResults.map(async (res) => { |
| // bge-reranker outputs logits, higher is betterHere is the complete code for your serverless, static Hugging Face Spaces app. |
| |
| 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. |
| |
| To deploy this, simply create a new Space on Hugging Face, select **Static**, and paste this code into the `index.html` file. |
| |
| ### `index.html` |
| |
| ```html |
| <!DOCTYPE html> |
| <html lang="en"> |
| <head> |
| <meta charset="UTF-8"> |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> |
| <title>Visual RAG Pipeline</title> |
| |
| <link rel="stylesheet" type="text/css" href="[https://tamats.com/projects/litegraph/css/litegraph.css](https://tamats.com/projects/litegraph/css/litegraph.css)"> |
| <style> |
| :root { |
| --bg-color: #1e1e2e; |
| --panel-bg: #282a36; |
| --text-color: #f8f8f2; |
| --accent: #bd93f9; |
| --accent-hover: #ff79c6; |
| --border: #44475a; |
| } |
| |
| body { |
| margin: 0; |
| padding: 0; |
| background-color: var(--bg-color); |
| color: var(--text-color); |
| font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; |
| display: grid; |
| grid-template-columns: 300px 1fr 350px; |
| grid-template-rows: 60vh 40vh; |
| height: 100vh; |
| overflow: hidden; |
| } |
| |
| .panel { |
| background: var(--panel-bg); |
| border: 1px solid var(--border); |
| border-radius: 8px; |
| margin: 8px; |
| display: flex; |
| flex-direction: column; |
| overflow: hidden; |
| } |
| |
| .panel-header { |
| background: var(--border); |
| padding: 10px; |
| font-weight: bold; |
| text-align: center; |
| } |
| |
| |
| #chat-panel { grid-column: 1; grid-row: 1 / 3; } |
| #chat-history { flex: 1; overflow-y: auto; padding: 10px; } |
| .message { margin-bottom: 10px; padding: 8px; border-radius: 5px; } |
| .user-msg { background: var(--accent); color: #000; align-self: flex-end; } |
| .bot-msg { background: var(--border); } |
| #chat-input-container { display: flex; padding: 10px; border-top: 1px solid var(--border); } |
| input[type="text"] { flex: 1; padding: 8px; border-radius: 4px; border: none; background: #333; color: white; } |
| button { background: var(--accent); color: black; border: none; padding: 8px 12px; margin-left: 5px; border-radius: 4px; cursor: pointer; font-weight: bold; } |
| button:hover { background: var(--accent-hover); } |
| |
| |
| #node-panel { grid-column: 2; grid-row: 2; } |
| #litegraph-canvas { width: 100%; height: 100%; } |
| |
| |
| #process-panel { grid-column: 2; grid-row: 1; display: flex; flex-direction: column; padding: 10px; } |
| .status-box { padding: 15px; background: #333; border-radius: 5px; margin-bottom: 10px; flex: 1; overflow-y: auto;} |
| |
| |
| #db-panel { grid-column: 3; grid-row: 1 / 3; } |
| #db-table-container { flex: 1; overflow-y: auto; padding: 5px; } |
| table { width: 100%; border-collapse: collapse; font-size: 0.85em; } |
| th, td { border: 1px solid var(--border); padding: 5px; text-align: left; } |
| .vector-cell { font-family: monospace; color: #8be9fd; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; max-width: 100px; } |
| |
| |
| @keyframes highlightPulse { |
| 0% { background-color: transparent; } |
| 50% { background-color: rgba(189, 147, 249, 0.5); } |
| 100% { background-color: rgba(189, 147, 249, 0.2); } |
| } |
| .highlight { animation: highlightPulse 1.5s ease-out forwards; border: 2px solid var(--accent); } |
| |
| #loading-overlay { |
| position: absolute; top: 0; left: 0; width: 100%; height: 100%; |
| background: rgba(0,0,0,0.8); z-index: 100; |
| display: flex; flex-direction: column; justify-content: center; align-items: center; |
| } |
| </style> |
| </head> |
| <body> |
| |
| |
| <div id="loading-overlay"> |
| <h2>Downloading Quantized WebGPU Models...</h2> |
| <p>Qwen3.5-0.8B, MiniLM, and BGE-Reran |