| <!DOCTYPE html> |
| <html lang="en"> |
| <head> |
| <meta charset="UTF-8"> |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> |
| <title>Corrective RAG</title> |
| <link href="https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:wght@400;600;700&family=Literata:ital,wght@0,300;0,400;0,500;1,300&display=swap" rel="stylesheet"> |
| <style> |
| :root { |
| --bg: #0a0a0c; |
| --surface: #111116; |
| --border: #1e1e28; |
| --accent: #7c6af7; |
| --accent-dim: #3d3478; |
| --text: #ddd9d0; |
| --text-muted: #555560; |
| --text-dim: #888890; |
| --pass: #3dba6f; |
| --fail: #e05252; |
| --warn: #e0a832; |
| --mono: 'IBM Plex Mono', monospace; |
| --serif: 'Literata', Georgia, serif; |
| } |
| |
| * { box-sizing: border-box; margin: 0; padding: 0; } |
| |
| body { |
| background: var(--bg); |
| color: var(--text); |
| font-family: var(--serif); |
| font-size: 15px; |
| line-height: 1.6; |
| height: 100vh; |
| display: flex; |
| overflow: hidden; |
| } |
| |
| |
| body::before { |
| content: ''; |
| position: fixed; |
| inset: 0; |
| background-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 256 256' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='noise'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='4' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23noise)' opacity='0.04'/%3E%3C/svg%3E"); |
| pointer-events: none; |
| z-index: 999; |
| opacity: 0.4; |
| } |
| |
| |
| .sidebar { |
| width: 280px; |
| min-width: 280px; |
| background: var(--surface); |
| border-right: 1px solid var(--border); |
| display: flex; |
| flex-direction: column; |
| padding: 28px 20px; |
| gap: 20px; |
| overflow-y: auto; |
| } |
| |
| .logo { |
| font-family: var(--mono); |
| font-size: 0.7rem; |
| font-weight: 700; |
| letter-spacing: 0.15em; |
| text-transform: uppercase; |
| color: var(--accent); |
| border-bottom: 1px solid var(--border); |
| padding-bottom: 16px; |
| } |
| .logo span { color: var(--text-muted); font-weight: 400; } |
| |
| .status-row { |
| display: flex; |
| flex-direction: column; |
| gap: 8px; |
| } |
| |
| .pill { |
| display: inline-flex; |
| align-items: center; |
| gap: 6px; |
| font-family: var(--mono); |
| font-size: 0.65rem; |
| font-weight: 600; |
| letter-spacing: 0.1em; |
| text-transform: uppercase; |
| padding: 4px 10px; |
| border-radius: 4px; |
| width: fit-content; |
| } |
| .pill-ok { background: #0d2e1a; color: var(--pass); border: 1px solid #1a4a2a; } |
| .pill-fail { background: #2e0d0d; color: var(--fail); border: 1px solid #4a1a1a; } |
| .pill-warn { background: #2e220d; color: var(--warn); border: 1px solid #4a360d; } |
| .pill .dot { width: 5px; height: 5px; border-radius: 50%; background: currentColor; animation: pulse 2s infinite; } |
| |
| @keyframes pulse { |
| 0%, 100% { opacity: 1; } |
| 50% { opacity: 0.3; } |
| } |
| |
| .section-label { |
| font-family: var(--mono); |
| font-size: 0.62rem; |
| letter-spacing: 0.12em; |
| text-transform: uppercase; |
| color: var(--text-muted); |
| margin-bottom: 8px; |
| } |
| |
| .upload-zone { |
| border: 1px dashed var(--border); |
| border-radius: 8px; |
| padding: 20px; |
| text-align: center; |
| cursor: pointer; |
| transition: all 0.2s; |
| position: relative; |
| } |
| .upload-zone:hover { border-color: var(--accent-dim); background: #13131a; } |
| .upload-zone.dragover { border-color: var(--accent); background: #1a1730; } |
| .upload-zone input { position: absolute; inset: 0; opacity: 0; cursor: pointer; } |
| .upload-zone .icon { font-size: 1.5rem; margin-bottom: 6px; } |
| .upload-zone p { font-size: 0.78rem; color: var(--text-dim); font-family: var(--mono); } |
| .upload-zone .filename { color: var(--accent); font-size: 0.72rem; margin-top: 4px; word-break: break-all; } |
| |
| .btn { |
| width: 100%; |
| padding: 10px; |
| background: var(--accent); |
| color: white; |
| border: none; |
| border-radius: 6px; |
| font-family: var(--mono); |
| font-size: 0.72rem; |
| font-weight: 600; |
| letter-spacing: 0.08em; |
| text-transform: uppercase; |
| cursor: pointer; |
| transition: all 0.2s; |
| display: flex; |
| align-items: center; |
| justify-content: center; |
| gap: 8px; |
| } |
| .btn:hover { background: #6a59e0; transform: translateY(-1px); } |
| .btn:disabled { background: var(--border); color: var(--text-muted); cursor: not-allowed; transform: none; } |
| .btn-ghost { |
| background: transparent; |
| border: 1px solid var(--border); |
| color: var(--text-dim); |
| } |
| .btn-ghost:hover { background: var(--surface); border-color: var(--text-muted); transform: none; } |
| |
| .slider-wrap label { |
| font-family: var(--mono); |
| font-size: 0.65rem; |
| color: var(--text-dim); |
| display: flex; |
| justify-content: space-between; |
| margin-bottom: 6px; |
| } |
| .slider-wrap label span { color: var(--accent); } |
| input[type=range] { |
| width: 100%; |
| appearance: none; |
| height: 2px; |
| background: var(--border); |
| border-radius: 2px; |
| outline: none; |
| } |
| input[type=range]::-webkit-slider-thumb { |
| appearance: none; |
| width: 12px; height: 12px; |
| border-radius: 50%; |
| background: var(--accent); |
| cursor: pointer; |
| } |
| |
| .stack-info { |
| font-size: 0.72rem; |
| color: var(--text-muted); |
| font-family: var(--mono); |
| line-height: 1.8; |
| border-top: 1px solid var(--border); |
| padding-top: 16px; |
| margin-top: auto; |
| } |
| .stack-info strong { color: var(--text-dim); } |
| |
| |
| .main { |
| flex: 1; |
| display: flex; |
| flex-direction: column; |
| overflow: hidden; |
| } |
| |
| .header { |
| padding: 28px 32px 20px; |
| border-bottom: 1px solid var(--border); |
| flex-shrink: 0; |
| } |
| .header h1 { |
| font-family: var(--mono); |
| font-size: 1.4rem; |
| font-weight: 700; |
| color: var(--text); |
| letter-spacing: -0.02em; |
| } |
| .header p { |
| font-size: 0.82rem; |
| color: var(--text-muted); |
| font-style: italic; |
| margin-top: 2px; |
| } |
| |
| |
| .chat { |
| flex: 1; |
| overflow-y: auto; |
| padding: 24px 32px; |
| display: flex; |
| flex-direction: column; |
| gap: 16px; |
| scroll-behavior: smooth; |
| } |
| |
| .chat::-webkit-scrollbar { width: 4px; } |
| .chat::-webkit-scrollbar-track { background: transparent; } |
| .chat::-webkit-scrollbar-thumb { background: var(--border); border-radius: 2px; } |
| |
| .empty-state { |
| flex: 1; |
| display: flex; |
| flex-direction: column; |
| align-items: center; |
| justify-content: center; |
| gap: 12px; |
| color: var(--text-muted); |
| text-align: center; |
| } |
| .empty-state .big { font-size: 2.5rem; } |
| .empty-state p { font-family: var(--mono); font-size: 0.72rem; letter-spacing: 0.08em; text-transform: uppercase; } |
| |
| .msg { display: flex; flex-direction: column; animation: fadeUp 0.3s ease; } |
| @keyframes fadeUp { |
| from { opacity: 0; transform: translateY(8px); } |
| to { opacity: 1; transform: translateY(0); } |
| } |
| |
| .msg-user .bubble { |
| align-self: flex-end; |
| max-width: 70%; |
| background: #16161e; |
| border: 1px solid #252530; |
| border-radius: 12px 12px 3px 12px; |
| padding: 12px 16px; |
| font-size: 0.92rem; |
| } |
| |
| .msg-bot .bubble { |
| align-self: flex-start; |
| max-width: 85%; |
| background: #0e0e14; |
| border: 1px solid var(--border); |
| border-left: 2px solid var(--accent); |
| border-radius: 3px 12px 12px 12px; |
| padding: 16px 18px; |
| font-size: 0.92rem; |
| line-height: 1.7; |
| } |
| |
| .meta { |
| display: flex; |
| gap: 12px; |
| margin-top: 10px; |
| padding-top: 10px; |
| border-top: 1px solid var(--border); |
| font-family: var(--mono); |
| font-size: 0.65rem; |
| color: var(--text-muted); |
| flex-wrap: wrap; |
| } |
| .meta .pass { color: var(--pass); font-weight: 600; } |
| .meta .fail { color: var(--fail); font-weight: 600; } |
| |
| .sources { margin-top: 10px; display: flex; flex-direction: column; gap: 6px; } |
| .source-chip { |
| background: #0a0a10; |
| border: 1px solid #1a1a22; |
| border-radius: 6px; |
| padding: 8px 12px; |
| font-family: var(--mono); |
| font-size: 0.65rem; |
| color: var(--text-muted); |
| line-height: 1.5; |
| } |
| .source-chip strong { color: var(--text-dim); display: block; margin-bottom: 2px; } |
| |
| |
| .input-area { |
| padding: 16px 32px 24px; |
| border-top: 1px solid var(--border); |
| flex-shrink: 0; |
| } |
| .input-row { |
| display: flex; |
| gap: 10px; |
| align-items: center; |
| } |
| .input-row input { |
| flex: 1; |
| background: var(--surface); |
| border: 1px solid var(--border); |
| border-radius: 8px; |
| padding: 12px 16px; |
| color: var(--text); |
| font-family: var(--serif); |
| font-size: 0.92rem; |
| outline: none; |
| transition: border-color 0.2s; |
| } |
| .input-row input:focus { border-color: var(--accent); } |
| .input-row input::placeholder { color: var(--text-muted); } |
| .send-btn { |
| width: 44px; height: 44px; |
| background: var(--accent); |
| border: none; |
| border-radius: 8px; |
| color: white; |
| font-size: 1.1rem; |
| cursor: pointer; |
| transition: all 0.2s; |
| display: flex; align-items: center; justify-content: center; |
| flex-shrink: 0; |
| } |
| .send-btn:hover { background: #6a59e0; transform: translateY(-1px); } |
| .send-btn:disabled { background: var(--border); cursor: not-allowed; transform: none; } |
| |
| .toast { |
| position: fixed; |
| bottom: 24px; right: 24px; |
| padding: 10px 16px; |
| border-radius: 8px; |
| font-family: var(--mono); |
| font-size: 0.72rem; |
| font-weight: 600; |
| z-index: 1000; |
| animation: slideIn 0.3s ease; |
| max-width: 300px; |
| } |
| .toast-ok { background: #0d2e1a; color: var(--pass); border: 1px solid #1a4a2a; } |
| .toast-fail { background: #2e0d0d; color: var(--fail); border: 1px solid #4a1a1a; } |
| @keyframes slideIn { |
| from { opacity: 0; transform: translateX(20px); } |
| to { opacity: 1; transform: translateX(0); } |
| } |
| |
| .spinner { |
| display: inline-block; |
| width: 14px; height: 14px; |
| border: 2px solid rgba(255,255,255,0.2); |
| border-top-color: white; |
| border-radius: 50%; |
| animation: spin 0.7s linear infinite; |
| } |
| @keyframes spin { to { transform: rotate(360deg); } } |
| </style> |
| </head> |
| <body> |
|
|
| |
| <aside class="sidebar"> |
| <div class="logo">Corrective RAG <span>// v1.0</span></div> |
|
|
| <div class="status-row" id="statusRow"> |
| <div class="pill pill-warn"><span class="dot"></span> Checking API...</div> |
| </div> |
|
|
| <div> |
| <div class="section-label">Upload Document</div> |
| <div class="upload-zone" id="uploadZone"> |
| <input type="file" id="fileInput" accept=".pdf,.txt"> |
| <div class="icon">📄</div> |
| <p>Drop PDF or TXT here</p> |
| <div class="filename" id="fileName"></div> |
| </div> |
| </div> |
|
|
| <button class="btn" id="indexBtn" disabled> |
| <span>⚡ Index Document</span> |
| </button> |
|
|
| <div class="slider-wrap"> |
| <label>Results per query <span id="topKVal">5</span></label> |
| <input type="range" id="topK" min="1" max="10" value="5"> |
| </div> |
|
|
| <button class="btn btn-ghost" id="clearBtn">🗑 Clear Chat</button> |
|
|
| <div class="stack-info"> |
| <strong>Stack</strong><br> |
| FAISS + BM25 + RRF<br> |
| Cross-encoder reranking<br> |
| LangGraph self-correction<br> |
| LLaMA 3.3 70B via Groq |
| </div> |
| </aside> |
|
|
| |
| <main class="main"> |
| <div class="header"> |
| <h1>Corrective RAG</h1> |
| <p>Hallucination-resistant · Self-correcting · Source-grounded</p> |
| </div> |
|
|
| <div class="chat" id="chat"> |
| <div class="empty-state" id="emptyState"> |
| <div class="big">🔍</div> |
| <p>Upload a document to begin</p> |
| </div> |
| </div> |
|
|
| <div class="input-area"> |
| <div class="input-row"> |
| <input type="text" id="questionInput" placeholder="Ask a question about your document..." disabled> |
| <button class="send-btn" id="sendBtn" disabled>→</button> |
| </div> |
| </div> |
| </main> |
|
|
| <script> |
| const API = 'https://hitan2004-agentic-corrective-rag.hf.space'; |
| let sessionId = 'session_' + Date.now(); |
| let selectedFile = null; |
| let indexed = false; |
| |
| |
| async function checkHealth() { |
| try { |
| const r = await fetch(API + '/health'); |
| const d = await r.json(); |
| const apiOk = d.status === 'ok'; |
| const idxOk = d.indexes_loaded; |
| const row = document.getElementById('statusRow'); |
| row.innerHTML = ` |
| <div class="pill ${apiOk ? 'pill-ok' : 'pill-fail'}"><span class="dot"></span> API ${apiOk ? 'ONLINE' : 'OFFLINE'}</div> |
| <div class="pill ${idxOk ? 'pill-ok' : 'pill-warn'}"><span class="dot"></span> INDEX ${idxOk ? 'READY' : 'NOT LOADED'}</div> |
| `; |
| if (idxOk) enableChat(); |
| } catch { |
| document.getElementById('statusRow').innerHTML = `<div class="pill pill-fail"><span class="dot"></span> API OFFLINE</div>`; |
| } |
| } |
| |
| |
| document.getElementById('fileInput').addEventListener('change', e => { |
| selectedFile = e.target.files[0]; |
| if (selectedFile) { |
| document.getElementById('fileName').textContent = selectedFile.name; |
| document.getElementById('indexBtn').disabled = false; |
| } |
| }); |
| |
| const zone = document.getElementById('uploadZone'); |
| zone.addEventListener('dragover', e => { e.preventDefault(); zone.classList.add('dragover'); }); |
| zone.addEventListener('dragleave', () => zone.classList.remove('dragover')); |
| zone.addEventListener('drop', e => { |
| e.preventDefault(); |
| zone.classList.remove('dragover'); |
| selectedFile = e.dataTransfer.files[0]; |
| if (selectedFile) { |
| document.getElementById('fileName').textContent = selectedFile.name; |
| document.getElementById('indexBtn').disabled = false; |
| } |
| }); |
| |
| |
| document.getElementById('indexBtn').addEventListener('click', async () => { |
| if (!selectedFile) return; |
| const btn = document.getElementById('indexBtn'); |
| btn.innerHTML = '<span class="spinner"></span> Indexing...'; |
| btn.disabled = true; |
| |
| const form = new FormData(); |
| form.append('file', selectedFile); |
| |
| try { |
| const r = await fetch(API + '/upload', { method: 'POST', body: form }); |
| const d = await r.json(); |
| if (r.ok) { |
| showToast('✅ ' + selectedFile.name + ' indexed!', 'ok'); |
| enableChat(); |
| checkHealth(); |
| } else { |
| showToast('❌ ' + (d.detail || 'Upload failed'), 'fail'); |
| btn.disabled = false; |
| } |
| } catch (err) { |
| showToast('❌ Network error', 'fail'); |
| btn.disabled = false; |
| } |
| btn.innerHTML = '<span>⚡ Index Document</span>'; |
| }); |
| |
| |
| document.getElementById('topK').addEventListener('input', e => { |
| document.getElementById('topKVal').textContent = e.target.value; |
| }); |
| |
| |
| document.getElementById('clearBtn').addEventListener('click', () => { |
| fetch(API + '/session/' + sessionId, { method: 'DELETE' }).catch(() => {}); |
| sessionId = 'session_' + Date.now(); |
| document.getElementById('chat').innerHTML = '<div class="empty-state" id="emptyState"><div class="big">🔍</div><p>Upload a document to begin</p></div>'; |
| if (indexed) document.getElementById('emptyState').querySelector('p').textContent = 'Ask a question below'; |
| }); |
| |
| |
| function enableChat() { |
| indexed = true; |
| document.getElementById('questionInput').disabled = false; |
| document.getElementById('sendBtn').disabled = false; |
| document.getElementById('questionInput').placeholder = 'Ask a question about your document...'; |
| const es = document.getElementById('emptyState'); |
| if (es) es.querySelector('p').textContent = 'Ask a question below'; |
| } |
| |
| |
| document.getElementById('sendBtn').addEventListener('click', sendQuestion); |
| document.getElementById('questionInput').addEventListener('keydown', e => { |
| if (e.key === 'Enter' && !e.shiftKey) sendQuestion(); |
| }); |
| |
| async function sendQuestion() { |
| const input = document.getElementById('questionInput'); |
| const q = input.value.trim(); |
| if (!q) return; |
| |
| input.value = ''; |
| document.getElementById('sendBtn').disabled = true; |
| |
| |
| const es = document.getElementById('emptyState'); |
| if (es) es.remove(); |
| |
| |
| appendMsg('user', q); |
| |
| |
| const thinkId = 'think_' + Date.now(); |
| appendThinking(thinkId); |
| |
| const topK = document.getElementById('topK').value; |
| |
| try { |
| const r = await fetch(API + '/query', { |
| method: 'POST', |
| headers: { 'Content-Type': 'application/json' }, |
| body: JSON.stringify({ question: q, session_id: sessionId, top_k: parseInt(topK) }) |
| }); |
| const d = await r.json(); |
| removeThinking(thinkId); |
| |
| if (r.ok) { |
| appendBotMsg(d); |
| } else { |
| appendError(d.detail || 'Query failed'); |
| } |
| } catch { |
| removeThinking(thinkId); |
| appendError('Network error — is the API running?'); |
| } |
| |
| document.getElementById('sendBtn').disabled = false; |
| input.focus(); |
| } |
| |
| function appendMsg(role, text) { |
| const chat = document.getElementById('chat'); |
| const div = document.createElement('div'); |
| div.className = 'msg msg-' + role; |
| div.innerHTML = `<div class="bubble">${escHtml(text)}</div>`; |
| chat.appendChild(div); |
| chat.scrollTop = chat.scrollHeight; |
| } |
| |
| function appendThinking(id) { |
| const chat = document.getElementById('chat'); |
| const div = document.createElement('div'); |
| div.className = 'msg msg-bot'; |
| div.id = id; |
| div.innerHTML = `<div class="bubble" style="color:var(--text-muted);font-style:italic;font-size:0.85rem">Thinking<span id="dots">...</span></div>`; |
| chat.appendChild(div); |
| chat.scrollTop = chat.scrollHeight; |
| } |
| |
| function removeThinking(id) { |
| const el = document.getElementById(id); |
| if (el) el.remove(); |
| } |
| |
| function appendBotMsg(data) { |
| const chat = document.getElementById('chat'); |
| const div = document.createElement('div'); |
| div.className = 'msg msg-bot'; |
| |
| const verdict = data.validation || ''; |
| const verdictHtml = verdict === 'PASS' |
| ? `<span class="pass">PASS ✓</span>` |
| : `<span class="fail">FAIL ✗</span>`; |
| |
| const sourcesHtml = (data.sources || []).map(s => ` |
| <div class="source-chip"> |
| <strong>${escHtml(s.source || '')}</strong> |
| ${escHtml((s.chunk || '').substring(0, 180))}… |
| </div> |
| `).join(''); |
| |
| div.innerHTML = ` |
| <div class="bubble"> |
| ${escHtml(data.answer || '')} |
| <div class="meta"> |
| <span>Verdict: ${verdictHtml}</span> |
| <span>Retries: ${data.retries_used ?? 0}</span> |
| <span>Sources: ${(data.sources || []).length}</span> |
| </div> |
| ${sourcesHtml ? `<div class="sources">${sourcesHtml}</div>` : ''} |
| </div> |
| `; |
| chat.appendChild(div); |
| chat.scrollTop = chat.scrollHeight; |
| } |
| |
| function appendError(msg) { |
| const chat = document.getElementById('chat'); |
| const div = document.createElement('div'); |
| div.className = 'msg msg-bot'; |
| div.innerHTML = `<div class="bubble" style="border-left-color:var(--fail);color:var(--fail);font-family:var(--mono);font-size:0.8rem">❌ ${escHtml(msg)}</div>`; |
| chat.appendChild(div); |
| chat.scrollTop = chat.scrollHeight; |
| } |
| |
| function escHtml(str) { |
| return String(str).replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"'); |
| } |
| |
| function showToast(msg, type) { |
| const t = document.createElement('div'); |
| t.className = 'toast toast-' + type; |
| t.textContent = msg; |
| document.body.appendChild(t); |
| setTimeout(() => t.remove(), 3500); |
| } |
| |
| |
| checkHealth(); |
| setInterval(checkHealth, 30000); |
| </script> |
| </body> |
| </html> |