Spaces:
Running
Running
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>RAG Visualizer (HF Spaces)</title> | |
| <script src="https://cdn.jsdelivr.net/npm/@tensorflow/tfjs@4.11.0/dist/tf.min.js"></script> | |
| <script src="https://cdn.jsdelivr.net/npm/@xenova/transformers@2.10.0/dist/transformers.min.js"></script> | |
| <script src="https://cdn.jsdelivr.net/npm/lancedb@0.5.0/dist/lance-wasm.min.js"></script> | |
| <style> | |
| :root { | |
| --primary: #3b82f6; | |
| --secondary: #1e40af; | |
| --bg: #f8fafc; | |
| --text: #0f172a; | |
| --border: #e2e8f0; | |
| } | |
| * { margin: 0; padding: 0; box-sizing: border-box; font-family: 'Segoe UI', sans-serif; } | |
| body { | |
| background: var(--bg); | |
| color: var(--text); | |
| display: grid; | |
| grid-template-columns: 1fr 1fr; | |
| grid-template-rows: 1fr auto; | |
| height: 100vh; | |
| gap: 1rem; | |
| padding: 1rem; | |
| } | |
| .section { | |
| background: white; | |
| border: 1px solid var(--border); | |
| border-radius: 0.5rem; | |
| padding: 1rem; | |
| overflow: auto; | |
| } | |
| .chat-window { | |
| display: flex; | |
| flex-direction: column; | |
| gap: 1rem; | |
| height: 100%; | |
| } | |
| .chat-messages { | |
| flex: 1; | |
| overflow-y: auto; | |
| display: flex; | |
| flex-direction: column; | |
| gap: 0.5rem; | |
| } | |
| .message { padding: 0.5rem; border-radius: 0.25rem; background: var(--border); } | |
| .message.user { background: var(--primary); color: white; } | |
| .input-area { display: flex; gap: 0.5rem; } | |
| input, button { padding: 0.5rem; border: 1px solid var(--border); border-radius: 0.25rem; } | |
| button { background: var(--primary); color: white; cursor: pointer; } | |
| button:hover { background: var(--secondary); } | |
| #vector-table { width: 100%; border-collapse: collapse; } | |
| #vector-table th, #vector-table td { border: 1px solid var(--border); padding: 0.5rem; text-align: left; } | |
| #vector-table tr.highlight { background: #fef3c7; animation: pulse 1s; } | |
| @keyframes pulse { 0% { background: #fef3c7; } 50% { background: #fde68a; } 100% { background: #fef3c7; } } | |
| #node-editor { height: 300px; border: 1px solid var(--border); border-radius: 0.25rem; } | |
| .tab { padding: 0.5rem 1rem; background: var(--border); cursor: pointer; } | |
| .tab.active { background: var(--primary); color: white; } | |
| .tabs { display: flex; gap: 0.25rem; margin-bottom: 1rem; } | |
| .tab-content { display: none; } | |
| .tab-content.active { display: block; } | |
| </style> | |
| </head> | |
| <body> | |
| <!-- Left: Chat Section --> | |
| <div class="section"> | |
| <h2>Chat with RAG</h2> | |
| <div class="chat-window"> | |
| <div class="chat-messages" id="chat-messages"></div> | |
| <div class="input-area"> | |
| <input type="text" id="user-input" placeholder="Ask a question..." /> | |
| <button onclick="sendMessage()">Send</button> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Right: Vector DB + Node Editor --> | |
| <div class="section"> | |
| <div class="tabs"> | |
| <div class="tab active" onclick="switchTab('vector-db')">Vector DB</div> | |
| <div class="tab" onclick="switchTab('node-editor')">Node Flow</div> | |
| <div class="tab" onclick="switchTab('reranker')">Reranker</div> | |
| </div> | |
| <div id="vector-db" class="tab-content active"> | |
| <h2>Vector DB Entries</h2> | |
| <div class="input-area"> | |
| <input type="text" id="db-input" placeholder="Add text to vector DB..." /> | |
| <button onclick="addToVectorDB()">Add</button> | |
| </div> | |
| <table id="vector-table"> | |
| <thead> | |
| <tr> | |
| <th>Text</th> | |
| <th>Metadata</th> | |
| <th>Date</th> | |
| <th>Score</th> | |
| </tr> | |
| </thead> | |
| <tbody id="vector-entries"></tbody> | |
| </table> | |
| </div> | |
| <div id="node-editor" class="tab-content"> | |
| <h2>Node Flow Editor</h2> | |
| <div id="node-editor-container" style="height: 100%;"></div> | |
| </div> | |
| <div id="reranker" class="tab-content"> | |
| <h2>Reranker</h2> | |
| <div id="reranker-results"></div> | |
| </div> | |
| </div> | |
| <script> | |
| // --- State --- | |
| let db; | |
| let pipeline; | |
| let currentTab = 'vector-db'; | |
| let vectorEntries = []; | |
| // --- Init --- | |
| async function init() { | |
| // 1. Load Transformers.js pipeline for Qwen3.5-0.8B (quantized) | |
| console.log("Loading Qwen3.5-0.8B (4-bit quantized)..."); | |
| pipeline = await transformers.pipeline( | |
| 'text-generation', | |
| 'Qwen/Qwen3.5-0.8B-4bit', | |
| { device: 'webgpu' } // or 'webnn'/'cpu' | |
| ); | |
| console.log("Qwen3.5-0.8B loaded!"); | |
| // 2. Initialize LanceDB (WASM) | |
| console.log("Initializing LanceDB (WASM)..."); | |
| await lanceDb.init(); | |
| db = await lanceDb.connect("/lancedb"); // Uses IndexedDB | |
| const table = await db.createTable("vectors", [ | |
| { vector: [], text: "", metadata: {}, date: new Date().toISOString() } | |
| ]); | |
| console.log("LanceDB ready!"); | |
| // 3. Load existing entries (if any) | |
| const existing = await table.search().limit(100).execute(); | |
| vectorEntries = existing; | |
| renderVectorTable(); | |
| // 4. Init Node Editor (placeholder) | |
| initNodeEditor(); | |
| } | |
| // --- Chat --- | |
| function sendMessage() { | |
| const input = document.getElementById("user-input"); | |
| const message = input.value.trim(); | |
| if (!message) return; | |
| addMessage("user", message); | |
| input.value = ""; | |
| // Simulate RAG workflow | |
| setTimeout(() => { | |
| // 1. Embed query (placeholder: use actual embedding model) | |
| const queryEmbedding = [0.1, 0.2, 0.3]; // Replace with real embedding | |
| // 2. Search LanceDB | |
| searchVectorDB(queryEmbedding).then(results => { | |
| // 3. Rerank (placeholder) | |
| const reranked = rerankResults(results); | |
| addMessage("assistant", `Answer: ${generateResponse(reranked)}`); | |
| // 4. Highlight top-K in table | |
| highlightEntries(reranked.slice(0, 3).map(r => r.id)); | |
| }); | |
| }, 500); | |
| } | |
| function addMessage(sender, text) { | |
| const chat = document.getElementById("chat-messages"); | |
| const msg = document.createElement("div"); | |
| msg.className = `message ${sender}`; | |
| msg.textContent = text; | |
| chat.appendChild(msg); | |
| chat.scrollTop = chat.scrollHeight; | |
| } | |
| // --- Vector DB --- | |
| async function addToVectorDB() { | |
| const input = document.getElementById("db-input"); | |
| const text = input.value.trim(); | |
| if (!text) return; | |
| input.value = ""; | |
| // 1. Embed text (placeholder) | |
| const embedding = [0.4, 0.5, 0.6]; // Replace with real embedding model | |
| // 2. Add to LanceDB | |
| const table = await db.openTable("vectors"); | |
| const entry = { | |
| vector: embedding, | |
| text: text, | |
| metadata: { source: "user" }, | |
| date: new Date().toISOString() | |
| }; | |
| await table.add([entry]); | |
| vectorEntries.push(entry); | |
| renderVectorTable(); | |
| } | |
| async function searchVectorDB(queryEmbedding, k = 3) { | |
| const table = await db.openTable("vectors"); | |
| const results = await table | |
| .search(queryEmbedding) | |
| .limit(k) | |
| .execute(); | |
| return results; | |
| } | |
| function renderVectorTable() { | |
| const tbody = document.getElementById("vector-entries"); | |
| tbody.innerHTML = ""; | |
| vectorEntries.forEach((entry, i) => { | |
| const row = document.createElement("tr"); | |
| row.innerHTML = ` | |
| <td>${entry.text.substring(0, 50)}...</td> | |
| <td>${JSON.stringify(entry.metadata)}</td> | |
| <td>${new Date(entry.date).toLocaleString()}</td> | |
| <td>${entry.score || "N/A"}</td> | |
| `; | |
| row.id = `entry-${i}`; | |
| tbody.appendChild(row); | |
| }); | |
| } | |
| function highlightEntries(ids) { | |
| document.querySelectorAll("#vector-entries tr").forEach((row, i) => { | |
| row.classList.toggle("highlight", ids.includes(i)); | |
| }); | |
| } | |
| // --- Reranker (Placeholder) --- | |
| function rerankResults(results) { | |
| // Replace with actual reranker model | |
| return results.map((r, i) => ({ ...r, score: 1 - (i * 0.1) })); | |
| } | |
| // --- Node Editor (Placeholder) --- | |
| function initNodeEditor() { | |
| const container = document.getElementById("node-editor-container"); | |
| container.innerHTML = ` | |
| <div style="padding: 1rem; background: #f1f5f9; border-radius: 0.25rem;"> | |
| <p>Node flow editor will go here. Use <a href="https://xyflow.com/" target="_blank">xyflow</a> or a custom SVG-based solution.</p> | |
| <p>Example nodes:</p> | |
| <ul> | |
| <li>🔹 Embedding Model (BAAI/bge-small-en-v1.5)</li> | |
| <li>📊 Vector DB (LanceDB WASM)</li> | |
| <li>🔄 Reranker (BAAI/bge-reranker-base)</li> | |
| <li>💬 LLM (Qwen3.5-0.8B)</li> | |
| </ul> | |
| </div> | |
| `; | |
| } | |
| // --- Generation (Placeholder) --- | |
| async function generateResponse(context) { | |
| // Replace with actual Qwen3.5-0.8B generation | |
| const prompt = `Context: ${context.map(c => c.text).join("\n")}\n\nAnswer:`; | |
| const output = await pipeline(prompt, { | |
| max_new_tokens: 200, | |
| temperature: 0.7, | |
| }); | |
| return output[0].generated_text; | |
| } | |
| // --- Tabs --- | |
| function switchTab(tabName) { | |
| document.querySelectorAll(".tab").forEach(t => t.classList.remove("active")); | |
| document.querySelectorAll(".tab-content").forEach(c => c.classList.remove("active")); | |
| document.getElementById(tabName).classList.add("active"); | |
| event.target.classList.add("active"); | |
| currentTab = tabName; | |
| } | |
| // --- Start App --- | |
| init().catch(console.error); | |
| </script> | |
| </body> | |
| </html> |