CoolFace
Apppublic

Vizz17/context-aware-rag

sourceHugging Faceupdated 4mo agoView on Hugging Face
0likes
app.js677 linesDownload Raw Back to frontend
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, '&quot;');670}671 672function askQuestion(q) {673  document.getElementById('chatInput').value = q;674  document.getElementById('sendBtn').disabled = false;675  document.getElementById('chatInput').focus();676}677