Vizz17/context-aware-rag
0
1// ── ContextAware RAG — Frontend App Logic (Document-Scoped Chat) ─────────────2const API = '';3 4// ── State ──────────────────────────────────────────────────5let messages = [];6let sessionId = null;7let documents = [];8let selectedDocId = null;9let selectedDocName = null;10let isLoading = false;11 12// ── Auth State ──────────────────────────────────────────────13let token = localStorage.getItem('token') || sessionStorage.getItem('token');14let username = localStorage.getItem('username') || sessionStorage.getItem('username');15let isGuest = localStorage.getItem('isGuest') === 'true' || sessionStorage.getItem('isGuest') === 'true';16 17// ── DOM Ready ──────────────────────────────────────────────18document.addEventListener('DOMContentLoaded', () => {19 setupInput();20 setupSidebarUpload();21 setupGatewayUpload();22 23 if (window.innerWidth < 768) {24 document.getElementById('sidebar').classList.add('collapsed');25 }26 27 // Check auth28 if (token) {29 showApp();30 } else {31 document.getElementById('authOverlay').classList.remove('hidden');32 }33});34 35// ── Auth Wrapper for Fetch ──────────────────────────────────36async function authFetch(url, options = {}) {37 options.headers = options.headers || {};38 if (token) {39 options.headers['Authorization'] = `Bearer ${token}`;40 }41 const resp = await fetch(url, options);42 if (resp.status === 401) {43 logout();44 }45 return resp;46}47 48// ── Auth UI & Handlers ──────────────────────────────────────49let currentAuthTab = 'login';50 51function switchAuthTab(tab) {52 currentAuthTab = tab;53 const tabLogin = document.getElementById('tabLogin');54 const tabRegister = document.getElementById('tabRegister');55 const submitBtn = document.getElementById('authSubmitBtn');56 const errorEl = document.getElementById('authError');57 58 errorEl.classList.add('hidden');59 60 if (tab === 'login') {61 tabLogin.className = 'flex-1 py-2 text-sm font-bold rounded-lg text-[#00e5ff] bg-[var(--glass-bg)] transition-all';62 tabRegister.className = 'flex-1 py-2 text-sm font-semibold rounded-lg text-[var(--text-muted)] hover:text-white transition-all';63 submitBtn.textContent = 'Sign In';64 } else {65 tabRegister.className = 'flex-1 py-2 text-sm font-bold rounded-lg text-[#b388ff] bg-[var(--glass-bg)] transition-all';66 tabLogin.className = 'flex-1 py-2 text-sm font-semibold rounded-lg text-[var(--text-muted)] hover:text-white transition-all';67 submitBtn.textContent = 'Create Account';68 }69}70 71async function handleAuthSubmit(e) {72 e.preventDefault();73 const usernameInput = document.getElementById('authUsername').value.trim();74 const passwordInput = document.getElementById('authPassword').value;75 const errorEl = document.getElementById('authError');76 77 errorEl.classList.add('hidden');78 79 const endpoint = currentAuthTab === 'login' ? '/api/auth/login' : '/api/auth/register';80 try {81 const resp = await fetch(`${API}${endpoint}`, {82 method: 'POST',83 headers: { 'Content-Type': 'application/json' },84 body: JSON.stringify({ username: usernameInput, password: passwordInput })85 });86 87 const data = await resp.json();88 if (resp.ok) {89 if (currentAuthTab === 'login') {90 token = data.token;91 username = data.username;92 isGuest = false;93 94 localStorage.setItem('token', token);95 localStorage.setItem('username', username);96 localStorage.setItem('isGuest', 'false');97 98 showApp();99 } else {100 switchAuthTab('login');101 errorEl.textContent = 'Registration successful! Please sign in.';102 errorEl.className = 'text-[#00e676] text-xs font-semibold mt-4 h-4 block';103 errorEl.classList.remove('hidden');104 }105 } else {106 errorEl.textContent = data.detail || 'Authentication failed';107 errorEl.className = 'text-red-400 text-xs font-semibold mt-4 h-4 block';108 errorEl.classList.remove('hidden');109 }110 } catch (err) {111 errorEl.textContent = 'Network error. Cannot reach backend.';112 errorEl.className = 'text-red-400 text-xs font-semibold mt-4 h-4 block';113 errorEl.classList.remove('hidden');114 }115}116 117async function handleGuestLogin() {118 const errorEl = document.getElementById('authError');119 errorEl.classList.add('hidden');120 121 try {122 const resp = await fetch(`${API}/api/auth/guest`, { method: 'POST' });123 const data = await resp.json();124 if (resp.ok) {125 token = data.token;126 username = data.username;127 isGuest = true;128 129 sessionStorage.setItem('token', token);130 sessionStorage.setItem('username', username);131 sessionStorage.setItem('isGuest', 'true');132 133 showApp();134 } else {135 errorEl.textContent = 'Failed to start guest session';136 errorEl.className = 'text-red-400 text-xs font-semibold mt-4 h-4 block';137 errorEl.classList.remove('hidden');138 }139 } catch (err) {140 errorEl.textContent = 'Network error. Cannot reach backend.';141 errorEl.className = 'text-red-400 text-xs font-semibold mt-4 h-4 block';142 errorEl.classList.remove('hidden');143 }144}145 146function showApp() {147 const authOverlay = document.getElementById('authOverlay');148 if (authOverlay) authOverlay.classList.add('hidden');149 const userDisp = document.getElementById('userDisplay');150 if (userDisp) userDisp.textContent = isGuest ? 'Guest' : username;151 loadDocuments();152}153 154function handleLogout() {155 if (token) {156 fetch(`${API}/api/auth/logout`, {157 method: 'POST',158 headers: { 'Authorization': `Bearer ${token}` }159 }).catch(() => {});160 }161 logout();162}163 164function logout() {165 token = null;166 username = null;167 isGuest = false;168 169 localStorage.removeItem('token');170 localStorage.removeItem('username');171 localStorage.removeItem('isGuest');172 173 sessionStorage.removeItem('token');174 sessionStorage.removeItem('username');175 sessionStorage.removeItem('isGuest');176 177 const authOverlay = document.getElementById('authOverlay');178 if (authOverlay) authOverlay.classList.remove('hidden');179 exitDoc();180}181 182// Clean up guest sessions when closing the tab183window.addEventListener('beforeunload', () => {184 if (token && isGuest) {185 fetch(`${API}/api/auth/logout`, {186 method: 'POST',187 headers: { 'Authorization': `Bearer ${token}` },188 keepalive: true189 });190 }191});192 193// ═══════════════════════════════════════════════════════════194// DOCUMENT-FIRST FLOW195// ═══════════════════════════════════════════════════════════196 197/**198 * Activate a document — enters chat mode scoped to this doc.199 */200function activateDocument(docId, docName) {201 selectedDocId = docId;202 selectedDocName = docName;203 204 // Update header badge with exit logic inside205 document.getElementById('activeDocBadge').innerHTML = `206 <span class="text-xs font-bold text-[#b388ff] uppercase tracking-wider">Querying:</span>207 <span class="text-sm font-semibold text-white truncate max-w-[200px]">📄 ${escapeHtml(docName)}</span>208 <button onclick="exitDoc()" class="ml-2 w-5 h-5 flex items-center justify-center rounded bg-[rgba(255,82,82,0.1)] text-[#ff5252] hover:bg-[#ff5252] hover:text-white transition-colors" title="Change Document">✕</button>209 `;210 document.getElementById('optimizerBadge').style.display = '';211 212 // Hide gateway, set chat layout to welcome mode213 document.getElementById('docGateway').style.display = 'none';214 document.getElementById('chatInputArea').style.display = 'none';215 document.getElementById('chatHistorySection').style.display = '';216 217 // Show welcome with doc name218 const welcomeEl = document.getElementById('welcome');219 welcomeEl.style.display = 'flex';220 document.getElementById('welcomeDocName').textContent = `Querying: ${docName}`;221 222 // Clear messages for a fresh start223 messages = [];224 sessionId = null;225 document.getElementById('chatMessages').innerHTML = '';226 227 // Highlight active doc in sidebar228 highlightActiveDoc();229 230 // Load chat history for this doc231 loadChatHistory();232}233 234/**235 * Exit document mode — go back to the gateway.236 */237function exitDoc() {238 selectedDocId = null;239 selectedDocName = null;240 sessionId = null;241 messages = [];242 243 document.getElementById('activeDocBadge').innerHTML = `244 <span class="text-xs font-bold text-[var(--text-muted)] uppercase tracking-wider">No document selected</span>245 `;246 document.getElementById('optimizerBadge').style.display = 'none';247 248 document.getElementById('docGateway').style.display = 'flex';249 document.getElementById('welcome').style.display = 'none';250 document.getElementById('chatInputArea').style.display = 'none';251 document.getElementById('chatHistorySection').style.display = 'none';252 document.getElementById('chatMessages').innerHTML = '';253 254 highlightActiveDoc();255 renderDocSearchList();256}257 258function highlightActiveDoc() {259 document.querySelectorAll('.doc-card').forEach(el => {260 el.classList.toggle('doc-active', el.dataset.docId === selectedDocId);261 });262}263 264// ═══════════════════════════════════════════════════════════265// CHAT266// ═══════════════════════════════════════════════════════════267 268function setupInput() {269 const bindTextarea = (inputId, btnId) => {270 const textarea = document.getElementById(inputId);271 const sendBtn = document.getElementById(btnId);272 if (!textarea || !sendBtn) return;273 274 textarea.addEventListener('keydown', (e) => {275 if (e.key === 'Enter' && !e.shiftKey) {276 e.preventDefault();277 sendMessage(inputId, btnId);278 }279 });280 281 textarea.addEventListener('input', () => {282 textarea.style.height = 'auto';283 textarea.style.height = Math.min(textarea.scrollHeight, 150) + 'px';284 sendBtn.disabled = !textarea.value.trim();285 });286 287 sendBtn.addEventListener('click', () => sendMessage(inputId, btnId));288 };289 290 bindTextarea('chatInput', 'sendBtn');291 bindTextarea('welcomeInput', 'welcomeSendBtn');292}293 294async function sendMessage(inputId = 'chatInput', btnId = 'sendBtn') {295 const textarea = document.getElementById(inputId);296 if(!textarea) return;297 const query = textarea.value.trim();298 if (!query || isLoading || !selectedDocId) return;299 300 hideWelcome();301 document.getElementById('chatInputArea').style.display = '';302 303 addMessage('user', query);304 textarea.value = '';305 textarea.style.height = 'auto';306 document.getElementById('sendBtn').disabled = true;307 const wBtn = document.getElementById('welcomeSendBtn');308 if(wBtn) wBtn.disabled = true;309 310 isLoading = true;311 const typingEl = showTypingIndicator();312 313 try {314 const payload = {315 query,316 session_id: sessionId,317 history: messages.slice(0, -1),318 filters: { doc_id: selectedDocId },319 };320 321 const resp = await authFetch(`${API}/api/chat`, {322 method: 'POST',323 headers: { 'Content-Type': 'application/json' },324 body: JSON.stringify(payload),325 });326 327 typingEl.remove();328 329 if (resp.ok) {330 const data = await resp.json();331 sessionId = data.session_id;332 addMessage('assistant', data.answer, data.sources, data.latency_ms, data.model, data.retrieval_mode);333 loadChatHistory();334 } else {335 // Show actual error detail from the API336 let errMsg = '❌ Error occurred while processing request.';337 try {338 const errData = await resp.json();339 if (errData.detail) errMsg = `❌ ${errData.detail}`;340 } catch (_) {}341 addMessage('assistant', errMsg);342 }343 } catch (e) {344 typingEl.remove();345 addMessage('assistant', '🔌 Cannot connect to API server. Ensure FastAPI is running.');346 } finally {347 isLoading = false;348 }349}350 351function addMessage(role, content, sources = null, latency = null, model = null, retrievalMode = null) {352 messages.push({ role, content, sources, latency, model });353 renderMessage(role, content, sources, latency, model, retrievalMode);354 scrollToBottom();355}356 357function renderMessage(role, content, sources, latency, model, retrievalMode) {358 const container = document.getElementById('chatMessages');359 const div = document.createElement('div');360 div.className = `message ${role}`;361 362 const avatarText = role === 'user' ? 'U' : '🧠';363 let html = `364 <div class="message-avatar">${avatarText}</div>365 <div class="message-content">366 <div class="message-text">${formatMarkdown(content)}</div>367 `;368 369 if (sources && sources.length > 0) {370 const srcId = 'src-' + Date.now();371 html += `372 <button class="sources-toggle mt-3" onclick="toggleSources('${srcId}')">373 📎 ${sources.length} Cited Sources374 </button>375 <div class="sources-panel" id="${srcId}">376 `;377 sources.forEach(s => {378 html += `379 <div class="source-chip mt-2">380 <div class="source-meta">📄 ${s.document} · Page ${s.page} · Score: ${s.score.toFixed(3)}</div>381 <div class="text-[0.8rem] text-[var(--text-muted)] line-clamp-3">${escapeHtml(s.text.substring(0, 300))}...</div>382 </div>383 `;384 });385 html += '</div>';386 }387 388 if (latency != null) {389 html += `<div class="latency-badge">⚡ ${Math.round(latency)}ms generation</div>`;390 }391 392 html += '</div>';393 div.innerHTML = html;394 container.appendChild(div);395}396 397function toggleSources(id) {398 document.getElementById(id).classList.toggle('open');399}400 401function showTypingIndicator() {402 const container = document.getElementById('chatMessages');403 const div = document.createElement('div');404 div.className = 'message assistant';405 div.innerHTML = `406 <div class="message-avatar">🧠</div>407 <div class="message-content" style="max-width:100px;">408 <div class="typing-indicator"><span></span><span></span><span></span></div>409 </div>410 `;411 container.appendChild(div);412 scrollToBottom();413 return div;414}415 416function hideWelcome() {417 const w = document.getElementById('welcome');418 if (w) w.style.display = 'none';419}420 421function scrollToBottom() {422 const c = document.getElementById('chatMessages');423 requestAnimationFrame(() => { c.scrollTop = c.scrollHeight; });424}425 426// ═══════════════════════════════════════════════════════════427// DOCUMENTS428// ═══════════════════════════════════════════════════════════429 430async function loadDocuments() {431 try {432 const resp = await authFetch(`${API}/api/documents`);433 if (!resp.ok) throw new Error('API error');434 const data = await resp.json();435 documents = data.documents || [];436 437 highlightActiveDoc();438 renderDocSearchList();439 } catch (e) {440 console.error('Failed to load documents:', e);441 }442}443 444function refreshGatewayDocList() {445 // Logic moved to renderDocSearchList for the new pop-up modal446}447 448// ═══════════════════════════════════════════════════════════449// DOCUMENT SEARCH MODAL (GATEWAY)450// ═══════════════════════════════════════════════════════════451 452function openDocSearchModal() {453 document.getElementById('docSearchModal').classList.remove('hidden');454 document.getElementById('modalSearchInput').value = '';455 document.getElementById('modalSearchInput').focus();456 renderDocSearchList();457}458 459function closeDocSearchModal(eventOrForce) {460 if (eventOrForce === true || eventOrForce?.target?.id === 'docSearchModal') {461 document.getElementById('docSearchModal').classList.add('hidden');462 }463}464 465function filterModalDocs(query) {466 renderDocSearchList(query.toLowerCase());467}468 469function renderDocSearchList(query = '') {470 const container = document.getElementById('modalDocList');471 if (!container) return;472 473 if (documents.length === 0) {474 container.innerHTML = `<div class="text-sm text-center py-8 text-[var(--text-muted)]">Knowledge base is empty. Upload a PDF first!</div>`;475 return;476 }477 478 const filtered = documents.filter(d => d.filename.toLowerCase().includes(query));479 480 if (filtered.length === 0) {481 container.innerHTML = `<div class="text-sm text-center py-8 text-[var(--text-muted)]">No documents match "${escapeHtml(query)}"</div>`;482 return;483 }484 485 container.innerHTML = filtered.map(d => `486 <div class="gateway-doc-item mb-2 group" onclick="closeDocSearchModal(true); activateDocument('${d.doc_id}', '${escapeAttr(d.filename)}')">487 <div class="flex items-center gap-4">488 <span class="text-2xl">📄</span>489 <div class="min-w-0 flex-1">490 <div class="text-sm font-bold text-[var(--text-main)] truncate">${escapeHtml(d.filename)}</div>491 <div class="text-xs font-semibold text-[var(--text-muted)] uppercase tracking-wider mt-0.5">${d.page_count} pages • ${d.chunk_count} chunks</div>492 </div>493 </div>494 <span class="text-xs px-3 py-1.5 rounded-lg bg-[rgba(0,229,255,0.12)] text-[#00e5ff] font-bold transition-opacity border border-[rgba(0,229,255,0.15)]">Launch</span>495 </div>496 `).join('');497}498 499async function deleteDoc(docId) {500 if (!confirm('Remove this document from the knowledge base?')) return;501 try {502 await authFetch(`${API}/api/documents/${docId}`, { method: 'DELETE' });503 if (selectedDocId === docId) exitDoc();504 loadDocuments();505 } catch (e) { /* ignore */ }506}507 508// ═══════════════════════════════════════════════════════════509// UPLOAD (sidebar + gateway)510// ═══════════════════════════════════════════════════════════511 512function setupSidebarUpload() {513 const zone = document.getElementById('uploadZone');514 const input = document.getElementById('fileInput');515 if (!zone || !input) return;516 517 zone.addEventListener('dragover', (e) => { e.preventDefault(); zone.classList.add('dragover'); });518 zone.addEventListener('dragleave', () => zone.classList.remove('dragover'));519 zone.addEventListener('drop', (e) => {520 e.preventDefault();521 zone.classList.remove('dragover');522 if (e.dataTransfer.files.length) uploadFile(e.dataTransfer.files[0], 'sidebar');523 });524 input.addEventListener('change', () => { if (input.files.length) uploadFile(input.files[0], 'sidebar'); });525}526 527function setupGatewayUpload() {528 const zone = document.getElementById('gatewayUploadZone');529 const input = document.getElementById('gatewayFileInput');530 if (!zone || !input) return;531 532 zone.addEventListener('dragover', (e) => { e.preventDefault(); zone.classList.add('dragover'); });533 zone.addEventListener('dragleave', () => zone.classList.remove('dragover'));534 zone.addEventListener('drop', (e) => {535 e.preventDefault();536 zone.classList.remove('dragover');537 if (e.dataTransfer.files.length) uploadFile(e.dataTransfer.files[0], 'gateway');538 });539 input.addEventListener('change', () => { if (input.files.length) uploadFile(input.files[0], 'gateway'); });540}541 542async function uploadFile(file, source = 'sidebar') {543 if (!file.name.toLowerCase().endsWith('.pdf')) { alert('Only PDFs Supported.'); return; }544 545 const statusId = source === 'gateway' ? 'gatewayUploadStatus' : 'uploadStatus';546 const status = document.getElementById(statusId);547 status.innerHTML = `<div class="mt-2 text-xs font-semibold text-[#00e5ff] animate-pulse text-center">Processing ${escapeHtml(file.name)}...</div>`;548 549 try {550 const form = new FormData();551 form.append('file', file);552 const resp = await authFetch(`${API}/api/upload`, { method: 'POST', body: form });553 if (resp.ok) {554 const data = await resp.json();555 status.innerHTML = `<div class="mt-2 text-xs font-bold text-[#00e676] text-center">✅ Indexed successfully</div>`;556 557 // Reload docs, then auto-activate the uploaded doc558 await loadDocuments();559 setTimeout(() => {560 status.innerHTML = '';561 activateDocument(data.doc_id, data.filename);562 }, 1000);563 } else {564 let errMsg = 'Upload failed';565 try { const errData = await resp.json(); if (errData.detail) errMsg = errData.detail; } catch (_) {}566 status.innerHTML = `<div class="mt-2 text-xs font-bold text-[#ff5252] text-center">❌ ${escapeHtml(errMsg)}</div>`;567 }568 } catch (e) {569 status.innerHTML = '<div class="mt-2 text-xs text-[#ff5252] text-center">Connection error</div>';570 }571}572 573// ═══════════════════════════════════════════════════════════574// CHAT HISTORY (scoped to active doc)575// ═══════════════════════════════════════════════════════════576 577async function loadChatHistory() {578 const list = document.getElementById('chatList');579 if (!selectedDocId) return [];580 581 try {582 const resp = await authFetch(`${API}/api/chats?doc_id=${encodeURIComponent(selectedDocId)}`);583 if (!resp.ok) return [];584 const data = await resp.json();585 const chats = data.sessions || [];586 587 if (chats.length) {588 list.innerHTML = chats.map(c => `589 <div class="chat-card p-2.5 flex justify-between items-center group" onclick="loadChat('${c.session_id}')">590 <div class="min-w-0 flex-1 truncate text-sm font-medium text-[var(--text-main)]">${escapeHtml(c.title)}</div>591 <button class="w-6 h-6 rounded bg-[rgba(255,82,82,0.1)] text-[#ff5252] hover:bg-[#ff5252] hover:text-white transition-colors flex items-center justify-center opacity-0 group-hover:opacity-100 flex-shrink-0 ml-2" onclick="event.stopPropagation();deleteChat('${c.session_id}')">✕</button>592 </div>593 `).join('');594 } else {595 list.innerHTML = '<div class="text-xs text-center py-2 text-[var(--text-muted)]">No past chats</div>';596 }597 598 return chats;599 } catch (e) {600 return [];601 }602}603 604async function loadChat(id) {605 try {606 const resp = await authFetch(`${API}/api/chats/${id}`);607 if (!resp.ok) return;608 const data = await resp.json();609 sessionId = id;610 messages = data.messages || [];611 612 hideWelcome();613 document.getElementById('chatInputArea').style.display = '';614 const container = document.getElementById('chatMessages');615 container.innerHTML = '';616 messages.forEach(m => renderMessage(m.role, m.content, m.sources, m.latency, m.model));617 scrollToBottom();618 } catch (e) {}619}620 621async function deleteChat(id) {622 try {623 await authFetch(`${API}/api/chats/${id}`, { method: 'DELETE' });624 if (sessionId === id) newChat();625 loadChatHistory();626 } catch (e) {}627}628 629function newChat() {630 messages = [];631 sessionId = null;632 document.getElementById('chatMessages').innerHTML = '';633 document.getElementById('chatInputArea').style.display = 'none';634 const w = document.getElementById('welcome');635 if (w && selectedDocId) {636 w.style.display = 'flex';637 document.getElementById('welcomeDocName').textContent = `Querying: ${selectedDocName}`;638 const wInput = document.getElementById('welcomeInput');639 if (wInput) wInput.focus();640 }641}642 643function toggleSidebar() { document.getElementById('sidebar').classList.toggle('collapsed'); }644 645// ═══════════════════════════════════════════════════════════646// MARKDOWN HELPERS647// ═══════════════════════════════════════════════════════════648function formatMarkdown(text) {649 if (!text) return '';650 let html = escapeHtml(text);651 html = html.replace(/```(\w*)\n([\s\S]*?)```/g, '<pre><code>$2</code></pre>');652 html = html.replace(/`([^`]+)`/g, '<code>$1</code>');653 html = html.replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>');654 html = html.replace(/\*(.+?)\*/g, '<em>$1</em>');655 html = html.split('\n\n').map(p => `<p>${p}</p>`).join('');656 html = html.replace(/\n/g, '<br>');657 html = html.replace(/<br>- /g, '</p><ul><li>');658 html = html.replace(/<br>(\d+)\. /g, '</p><ol><li>');659 return html;660}661 662function escapeHtml(text) {663 const div = document.createElement('div');664 div.textContent = text;665 return div.innerHTML;666}667 668function escapeAttr(text) {669 return text.replace(/'/g, "\\'").replace(/"/g, '"');670}671 672function askQuestion(q) {673 document.getElementById('chatInput').value = q;674 document.getElementById('sendBtn').disabled = false;675 document.getElementById('chatInput').focus();676}677 