RAG-Visualizer / index.html
quickgrid's picture
Update index.html
6fbbbb9 verified
raw
history blame
20 kB
<!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;}
/* Custom Scrollbar */
::-webkit-scrollbar { width: 6px; }
::-webkit-scrollbar-track { background: #1e293b; }
::-webkit-scrollbar-thumb { background: #475569; border-radius: 4px; }
/* Animations */
@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; }
/* LiteGraph Customizations */
.lgraphcanvas { background-color: #0f172a !important; }
</style>
</head>
<body>
<div class="grid-layout">
<!-- LEFT PANEL: Chat & Data Entry -->
<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>
<!-- Add to Vector DB -->
<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>
<!-- Chat Interface -->
<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>
<!-- MIDDLE PANEL: Visual Node Flow -->
<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>
<!-- RIGHT PANEL: Vector DB & Reranking -->
<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>
<!-- Reranker Visualization -->
<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">
<!-- Reranked items appear here -->
</div>
</div>
<!-- Vector DB Table -->
<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">
<!-- DB Rows -->
</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>
<!-- LiteGraph CSS -->
<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 Section (Left) */
#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 Editor (Center Bottom) */
#node-panel { grid-column: 2; grid-row: 2; }
#litegraph-canvas { width: 100%; height: 100%; }
/* DB Input & Reranker (Center Top) */
#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;}
/* Vector DB Table (Right) */
#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; }
/* Animations */
@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>
<!-- Loading Overlay -->
<div id="loading-overlay">
<h2>Downloading Quantized WebGPU Models...</h2>
<p>Qwen3.5-0.8B, MiniLM, and BGE-Reran