/** * Iribl AI - Document Intelligence Application * With Dual Sidebars, Collapsible Sections, and Animated Dropdowns */ // ==================== App State ==================== const state = { token: localStorage.getItem('Iribl AI_token'), user: JSON.parse(localStorage.getItem('Iribl AI_user') || 'null'), documents: [], buckets: [], employees: [], messages: [], summaries: {}, // doc_id -> summary text cache selectedDocument: null, // Currently selected document for summary display selectedBucket: '', chatBucket: '', isLoading: false, currentRole: 'admin', // Chat History chatHistory: JSON.parse(localStorage.getItem('Iribl AI_chat_history') || '[]'), currentChatId: null, // Upload cancellation uploadCancelled: false, currentUploadAbortController: null, // Stream abort controller for stopping generation streamAbortController: null }; // ==================== DOM Elements ==================== const elements = { // Auth authModal: document.getElementById('authModal'), loginForm: document.getElementById('loginForm'), registerForm: document.getElementById('registerForm'), employeeLoginForm: document.getElementById('employeeLoginForm'), authTabs: document.getElementById('authTabs'), loginError: document.getElementById('loginError'), registerError: document.getElementById('registerError'), employeeLoginError: document.getElementById('employeeLoginError'), // Modals addEmployeeModal: document.getElementById('addEmployeeModal'), addEmployeeForm: document.getElementById('addEmployeeForm'), addEmployeeError: document.getElementById('addEmployeeError'), addEmployeeBtn: document.getElementById('addEmployeeBtn'), cancelAddEmployee: document.getElementById('cancelAddEmployee'), createBucketModal: document.getElementById('createBucketModal'), createBucketForm: document.getElementById('createBucketForm'), createBucketError: document.getElementById('createBucketError'), createBucketBtn: document.getElementById('createBucketBtn'), cancelCreateBucket: document.getElementById('cancelCreateBucket'), docViewerModal: document.getElementById('docViewerModal'), docViewerTitle: document.getElementById('docViewerTitle'), docViewerContent: document.getElementById('docViewerContent'), closeDocViewer: document.getElementById('closeDocViewer'), // Sidebars leftSidebar: document.getElementById('leftSidebar'), rightSidebar: document.getElementById('rightSidebar'), leftToggle: document.getElementById('leftToggle'), rightToggle: document.getElementById('rightToggle'), // App appContainer: document.getElementById('appContainer'), userName: document.getElementById('userName'), userAvatar: document.getElementById('userAvatar'), userRole: document.getElementById('userRole'), logoutBtn: document.getElementById('logoutBtn'), // Admin adminSection: document.getElementById('adminSection'), employeesList: document.getElementById('employeesList'), // Buckets bucketsList: document.getElementById('bucketsList'), // Custom Dropdowns uploadBucketWrapper: document.getElementById('uploadBucketWrapper'), uploadBucketTrigger: document.getElementById('uploadBucketTrigger'), uploadBucketOptions: document.getElementById('uploadBucketOptions'), uploadBucketSelect: document.getElementById('uploadBucketSelect'), chatBucketWrapper: document.getElementById('chatBucketWrapper'), chatBucketTrigger: document.getElementById('chatBucketTrigger'), chatBucketOptions: document.getElementById('chatBucketOptions'), chatBucketSelect: document.getElementById('chatBucketSelect'), // Upload uploadZone: document.getElementById('uploadZone'), fileInput: document.getElementById('fileInput'), uploadProgress: document.getElementById('uploadProgress'), uploadStatus: document.getElementById('uploadStatus'), progressFill: document.getElementById('progressFill'), cancelUploadBtn: document.getElementById('cancelUploadBtn'), // Documents documentsList: document.getElementById('documentsList'), docCount: document.getElementById('docCount'), // Chat chatMessages: document.getElementById('chatMessages'), welcomeScreen: document.getElementById('welcomeScreen'), chatInput: document.getElementById('chatInput'), sendBtn: document.getElementById('sendBtn'), stopBtn: document.getElementById('stopBtn'), typingIndicator: document.getElementById('typingIndicator'), toastContainer: document.getElementById('toastContainer'), // Summary Panel summaryPanel: document.getElementById('summaryPanel'), summaryTitle: document.getElementById('summaryTitle'), summaryText: document.getElementById('summaryText'), summaryClose: document.getElementById('summaryClose'), // Chat History newChatBtn: document.getElementById('newChatBtn'), clearChatBtn: document.getElementById('clearChatBtn'), clearChatBtnTop: document.getElementById('clearChatBtnTop'), chatHistoryList: document.getElementById('chatHistoryList'), chatHistoryCount: document.getElementById('chatHistoryCount'), // Mobile Navigation mobileNav: document.getElementById('mobileNav'), mobileBackdrop: document.getElementById('mobileBackdrop'), mobileLeftToggle: document.getElementById('mobileLeftToggle'), mobileChatToggle: document.getElementById('mobileChatToggle'), mobileRightToggle: document.getElementById('mobileRightToggle') }; // ==================== Toast ==================== function showToast(message, type = 'info') { const icons = { success: '✅', error: '❌', info: 'ℹ️' }; const toast = document.createElement('div'); toast.className = `toast ${type}`; toast.innerHTML = `${icons[type]}${message}`; elements.toastContainer.appendChild(toast); toast.querySelector('.toast-close').addEventListener('click', () => toast.remove()); setTimeout(() => { if (toast.parentElement) toast.remove(); }, 4000); } // ==================== Sidebar Toggle ==================== function initSidebars() { elements.leftToggle.addEventListener('click', () => { elements.leftSidebar.classList.toggle('collapsed'); const icon = elements.leftToggle.querySelector('.toggle-icon'); icon.textContent = elements.leftSidebar.classList.contains('collapsed') ? '▶' : '◀'; }); elements.rightToggle.addEventListener('click', () => { elements.rightSidebar.classList.toggle('collapsed'); const icon = elements.rightToggle.querySelector('.toggle-icon'); icon.textContent = elements.rightSidebar.classList.contains('collapsed') ? '◀' : '▶'; }); } // ==================== Mobile Navigation ==================== function initMobileNavigation() { // Check if we're on mobile const isMobile = () => window.innerWidth <= 768; // Close all sidebars on mobile function closeMobileSidebars() { elements.leftSidebar.classList.remove('mobile-open'); elements.rightSidebar.classList.remove('mobile-open'); elements.mobileBackdrop.classList.remove('active'); document.body.style.overflow = ''; // Reset nav button active states elements.mobileLeftToggle.classList.remove('active'); elements.mobileRightToggle.classList.remove('active'); elements.mobileChatToggle.classList.add('active'); } // Open left sidebar (Menu) function openLeftSidebar() { closeMobileSidebars(); elements.leftSidebar.classList.add('mobile-open'); elements.mobileBackdrop.classList.add('active'); document.body.style.overflow = 'hidden'; elements.mobileLeftToggle.classList.add('active'); elements.mobileChatToggle.classList.remove('active'); } // Open right sidebar (Docs) function openRightSidebar() { closeMobileSidebars(); elements.rightSidebar.classList.add('mobile-open'); elements.mobileBackdrop.classList.add('active'); document.body.style.overflow = 'hidden'; elements.mobileRightToggle.classList.add('active'); elements.mobileChatToggle.classList.remove('active'); } // Mobile nav button handlers elements.mobileLeftToggle.addEventListener('click', () => { if (elements.leftSidebar.classList.contains('mobile-open')) { closeMobileSidebars(); } else { openLeftSidebar(); } }); elements.mobileChatToggle.addEventListener('click', () => { closeMobileSidebars(); }); elements.mobileRightToggle.addEventListener('click', () => { if (elements.rightSidebar.classList.contains('mobile-open')) { closeMobileSidebars(); } else { openRightSidebar(); } }); // Close sidebar when backdrop is clicked elements.mobileBackdrop.addEventListener('click', closeMobileSidebars); // Close sidebar on window resize to desktop window.addEventListener('resize', () => { if (!isMobile()) { closeMobileSidebars(); // Reset any mobile-specific classes elements.leftSidebar.classList.remove('mobile-open'); elements.rightSidebar.classList.remove('mobile-open'); } }); // Close sidebar when starting a new chat or after uploading (for better UX) const originalStartNewChat = window.startNewChat; if (typeof originalStartNewChat === 'function') { window.startNewChat = function () { if (isMobile()) closeMobileSidebars(); return originalStartNewChat.apply(this, arguments); }; } // Handle swipe gestures (optional enhancement) let touchStartX = 0; let touchEndX = 0; document.addEventListener('touchstart', (e) => { touchStartX = e.changedTouches[0].screenX; }, { passive: true }); document.addEventListener('touchend', (e) => { if (!isMobile()) return; touchEndX = e.changedTouches[0].screenX; const swipeDistance = touchEndX - touchStartX; const minSwipeDistance = 80; // Swipe right from left edge - open left sidebar if (touchStartX < 30 && swipeDistance > minSwipeDistance) { openLeftSidebar(); } // Swipe left from right edge - open right sidebar if (touchStartX > window.innerWidth - 30 && swipeDistance < -minSwipeDistance) { openRightSidebar(); } // Swipe to close sidebars if (elements.leftSidebar.classList.contains('mobile-open') && swipeDistance < -minSwipeDistance) { closeMobileSidebars(); } if (elements.rightSidebar.classList.contains('mobile-open') && swipeDistance > minSwipeDistance) { closeMobileSidebars(); } }, { passive: true }); } // ==================== Collapsible Sections ==================== function initCollapsible() { document.querySelectorAll('.collapsible .section-header').forEach(header => { header.addEventListener('click', (e) => { // Don't toggle if clicking on action buttons if (e.target.closest('.btn')) return; const section = header.closest('.collapsible'); section.classList.toggle('collapsed'); }); }); } // ==================== Custom Dropdowns ==================== function initCustomDropdowns() { // Close dropdowns when clicking outside document.addEventListener('click', (e) => { document.querySelectorAll('.custom-select.open').forEach(select => { if (!select.contains(e.target)) { select.classList.remove('open'); } }); }); // Upload bucket dropdown elements.uploadBucketTrigger.addEventListener('click', (e) => { e.stopPropagation(); elements.uploadBucketWrapper.classList.toggle('open'); elements.chatBucketWrapper.classList.remove('open'); }); // Chat bucket dropdown elements.chatBucketTrigger.addEventListener('click', (e) => { e.stopPropagation(); elements.chatBucketWrapper.classList.toggle('open'); elements.uploadBucketWrapper.classList.remove('open'); }); } function updateDropdownOptions() { // Upload dropdown options let uploadOptions = `
📂 No Bucket (General)
`; uploadOptions += state.buckets.map(b => `
📁 ${b.name}
` ).join(''); elements.uploadBucketOptions.innerHTML = uploadOptions; // Chat dropdown options let chatOptions = `
📂 All Documents
`; chatOptions += state.buckets.map(b => `
📁 ${b.name}
` ).join(''); elements.chatBucketOptions.innerHTML = chatOptions; // Add click handlers elements.uploadBucketOptions.querySelectorAll('.select-option').forEach(opt => { opt.addEventListener('click', () => { const value = opt.dataset.value; elements.uploadBucketSelect.value = value; elements.uploadBucketTrigger.querySelector('.select-value').textContent = opt.textContent.trim(); elements.uploadBucketOptions.querySelectorAll('.select-option').forEach(o => o.classList.remove('active')); opt.classList.add('active'); elements.uploadBucketWrapper.classList.remove('open'); }); }); elements.chatBucketOptions.querySelectorAll('.select-option').forEach(opt => { opt.addEventListener('click', () => { const value = opt.dataset.value; elements.chatBucketSelect.value = value; state.chatBucket = value; elements.chatBucketTrigger.querySelector('.select-value').textContent = opt.textContent.trim(); elements.chatBucketOptions.querySelectorAll('.select-option').forEach(o => o.classList.remove('active')); opt.classList.add('active'); elements.chatBucketWrapper.classList.remove('open'); }); }); } // ==================== Auth ==================== function showAuthModal() { elements.authModal.classList.add('active'); elements.appContainer.style.filter = 'blur(5px)'; } function hideAuthModal() { elements.authModal.classList.remove('active'); elements.appContainer.style.filter = ''; } function updateAuthUI() { if (state.user) { elements.userName.textContent = state.user.username; elements.userAvatar.textContent = state.user.username.charAt(0).toUpperCase(); elements.userRole.textContent = state.user.role === 'admin' ? 'Admin' : 'Employee'; if (state.user.role === 'admin') { elements.adminSection.classList.remove('hidden'); loadEmployees(); } else { elements.adminSection.classList.add('hidden'); } hideAuthModal(); } else { showAuthModal(); } } // Role tabs document.querySelectorAll('.role-tab').forEach(tab => { tab.addEventListener('click', () => { document.querySelectorAll('.role-tab').forEach(t => t.classList.remove('active')); tab.classList.add('active'); state.currentRole = tab.dataset.role; if (state.currentRole === 'admin') { elements.authTabs.classList.remove('hidden'); elements.loginForm.classList.remove('hidden'); elements.registerForm.classList.add('hidden'); elements.employeeLoginForm.classList.add('hidden'); } else { elements.authTabs.classList.add('hidden'); elements.loginForm.classList.add('hidden'); elements.registerForm.classList.add('hidden'); elements.employeeLoginForm.classList.remove('hidden'); } }); }); // Auth tabs document.querySelectorAll('.auth-tab').forEach(tab => { tab.addEventListener('click', () => { document.querySelectorAll('.auth-tab').forEach(t => t.classList.remove('active')); tab.classList.add('active'); const tabName = tab.dataset.tab; elements.loginForm.classList.toggle('hidden', tabName !== 'login'); elements.registerForm.classList.toggle('hidden', tabName !== 'register'); }); }); // Admin Login elements.loginForm.addEventListener('submit', async (e) => { e.preventDefault(); const formData = new FormData(e.target); const btn = e.target.querySelector('.auth-btn'); btn.querySelector('.btn-text').classList.add('hidden'); btn.querySelector('.btn-loader').classList.remove('hidden'); elements.loginError.classList.add('hidden'); try { const response = await fetch('/api/auth/login', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ username: formData.get('username'), password: formData.get('password'), role: 'admin' }) }); const data = await response.json(); if (response.ok) { state.token = data.token; state.user = { user_id: data.user_id, username: data.username, role: data.role }; localStorage.setItem('Iribl AI_token', state.token); localStorage.setItem('Iribl AI_user', JSON.stringify(state.user)); updateAuthUI(); loadBuckets(); loadDocuments(); loadChatHistoryFromServer(); showToast('Welcome back!', 'success'); } else { elements.loginError.textContent = data.error; elements.loginError.classList.remove('hidden'); } } catch (error) { elements.loginError.textContent = 'Connection error'; elements.loginError.classList.remove('hidden'); } btn.querySelector('.btn-text').classList.remove('hidden'); btn.querySelector('.btn-loader').classList.add('hidden'); }); // Admin Register elements.registerForm.addEventListener('submit', async (e) => { e.preventDefault(); const formData = new FormData(e.target); const btn = e.target.querySelector('.auth-btn'); btn.querySelector('.btn-text').classList.add('hidden'); btn.querySelector('.btn-loader').classList.remove('hidden'); elements.registerError.classList.add('hidden'); try { const response = await fetch('/api/auth/register/admin', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ username: formData.get('username'), email: formData.get('email'), password: formData.get('password') }) }); const data = await response.json(); if (response.ok) { state.token = data.token; state.user = { user_id: data.user_id, username: data.username, role: data.role }; localStorage.setItem('Iribl AI_token', state.token); localStorage.setItem('Iribl AI_user', JSON.stringify(state.user)); updateAuthUI(); loadBuckets(); loadDocuments(); loadChatHistoryFromServer(); showToast('Account created!', 'success'); } else { elements.registerError.textContent = data.error; elements.registerError.classList.remove('hidden'); } } catch (error) { elements.registerError.textContent = 'Connection error'; elements.registerError.classList.remove('hidden'); } btn.querySelector('.btn-text').classList.remove('hidden'); btn.querySelector('.btn-loader').classList.add('hidden'); }); // Employee Login elements.employeeLoginForm.addEventListener('submit', async (e) => { e.preventDefault(); const formData = new FormData(e.target); const btn = e.target.querySelector('.auth-btn'); btn.querySelector('.btn-text').classList.add('hidden'); btn.querySelector('.btn-loader').classList.remove('hidden'); elements.employeeLoginError.classList.add('hidden'); try { const response = await fetch('/api/auth/login', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ username: formData.get('email'), password: formData.get('password'), role: 'employee' }) }); const data = await response.json(); if (response.ok) { state.token = data.token; state.user = { user_id: data.user_id, username: data.username, role: data.role }; localStorage.setItem('Iribl AI_token', state.token); localStorage.setItem('Iribl AI_user', JSON.stringify(state.user)); updateAuthUI(); loadBuckets(); loadDocuments(); loadChatHistoryFromServer(); showToast('Welcome!', 'success'); } else { elements.employeeLoginError.textContent = data.error; elements.employeeLoginError.classList.remove('hidden'); } } catch (error) { elements.employeeLoginError.textContent = 'Connection error'; elements.employeeLoginError.classList.remove('hidden'); } btn.querySelector('.btn-text').classList.remove('hidden'); btn.querySelector('.btn-loader').classList.add('hidden'); }); // Logout elements.logoutBtn.addEventListener('click', () => { state.token = null; state.user = null; state.documents = []; state.buckets = []; state.messages = []; localStorage.removeItem('Iribl AI_token'); localStorage.removeItem('Iribl AI_user'); updateAuthUI(); renderDocuments(); renderMessages(); showToast('Logged out', 'info'); }); // ==================== Employees ==================== async function loadEmployees() { if (!state.token || state.user?.role !== 'admin') return; try { const response = await fetch('/api/admin/employees', { headers: { 'Authorization': `Bearer ${state.token}` } }); if (response.ok) { const data = await response.json(); state.employees = data.employees; renderEmployees(); } } catch (error) { console.error('Failed to load employees:', error); } } function renderEmployees() { if (state.employees.length === 0) { elements.employeesList.innerHTML = `
No employees
`; return; } elements.employeesList.innerHTML = state.employees.map(emp => `
${emp.email || emp.username}
`).join(''); } elements.addEmployeeBtn.addEventListener('click', (e) => { e.stopPropagation(); elements.addEmployeeModal.classList.add('active'); elements.addEmployeeError.classList.add('hidden'); elements.addEmployeeForm.reset(); }); elements.cancelAddEmployee.addEventListener('click', () => elements.addEmployeeModal.classList.remove('active')); elements.addEmployeeForm.addEventListener('submit', async (e) => { e.preventDefault(); const formData = new FormData(e.target); const btn = e.target.querySelector('.btn-primary'); btn.querySelector('.btn-text').classList.add('hidden'); btn.querySelector('.btn-loader').classList.remove('hidden'); try { const response = await fetch('/api/admin/employees', { method: 'POST', headers: { 'Authorization': `Bearer ${state.token}`, 'Content-Type': 'application/json' }, body: JSON.stringify({ email: formData.get('email'), password: formData.get('password') }) }); const data = await response.json(); if (response.ok) { elements.addEmployeeModal.classList.remove('active'); loadEmployees(); showToast('Employee added!', 'success'); } else { elements.addEmployeeError.textContent = data.error; elements.addEmployeeError.classList.remove('hidden'); } } catch (error) { elements.addEmployeeError.textContent = 'Connection error'; elements.addEmployeeError.classList.remove('hidden'); } btn.querySelector('.btn-text').classList.remove('hidden'); btn.querySelector('.btn-loader').classList.add('hidden'); }); async function deleteEmployee(employeeId) { try { const response = await fetch(`/api/admin/employees/${employeeId}`, { method: 'DELETE', headers: { 'Authorization': `Bearer ${state.token}` } }); if (response.ok) { state.employees = state.employees.filter(e => e.user_id !== employeeId); renderEmployees(); showToast('Employee removed', 'success'); } } catch (error) { showToast('Failed to remove employee', 'error'); } } // ==================== Buckets ==================== async function loadBuckets() { if (!state.token) return; try { const response = await fetch('/api/buckets', { headers: { 'Authorization': `Bearer ${state.token}` } }); if (response.ok) { const data = await response.json(); state.buckets = data.buckets; renderBuckets(); updateDropdownOptions(); } } catch (error) { console.error('Failed to load buckets:', error); } } function renderBuckets() { let html = `
📂 All Documents
`; html += state.buckets.map(b => `
📁 ${b.name} ${b.doc_count}
`).join(''); elements.bucketsList.innerHTML = html; } function selectBucket(bucketId) { state.selectedBucket = bucketId; state.chatBucket = bucketId; // Sync chat bucket filter // Get bucket name for display const bucketName = bucketId ? (state.buckets.find(b => b.bucket_id === bucketId)?.name || 'Selected Bucket') : ''; const displayName = bucketId ? bucketName : 'All Documents'; const uploadDisplayName = bucketId ? bucketName : 'No Bucket (General)'; // Sync upload bucket dropdown elements.uploadBucketSelect.value = bucketId; elements.uploadBucketTrigger.querySelector('.select-value').textContent = uploadDisplayName; elements.uploadBucketOptions.querySelectorAll('.select-option').forEach(opt => { opt.classList.toggle('active', opt.dataset.value === bucketId); }); // Sync chat bucket dropdown elements.chatBucketSelect.value = bucketId; elements.chatBucketTrigger.querySelector('.select-value').textContent = displayName; elements.chatBucketOptions.querySelectorAll('.select-option').forEach(opt => { opt.classList.toggle('active', opt.dataset.value === bucketId); }); // Render all filtered components renderBuckets(); loadDocuments(); renderChatHistory(); // Re-render to filter by bucket } elements.createBucketBtn.addEventListener('click', (e) => { e.stopPropagation(); elements.createBucketModal.classList.add('active'); elements.createBucketError.classList.add('hidden'); elements.createBucketForm.reset(); }); elements.cancelCreateBucket.addEventListener('click', () => elements.createBucketModal.classList.remove('active')); elements.createBucketForm.addEventListener('submit', async (e) => { e.preventDefault(); const formData = new FormData(e.target); const btn = e.target.querySelector('.btn-primary'); btn.querySelector('.btn-text').classList.add('hidden'); btn.querySelector('.btn-loader').classList.remove('hidden'); try { const response = await fetch('/api/buckets', { method: 'POST', headers: { 'Authorization': `Bearer ${state.token}`, 'Content-Type': 'application/json' }, body: JSON.stringify({ name: formData.get('name'), description: formData.get('description') }) }); const data = await response.json(); if (response.ok) { elements.createBucketModal.classList.remove('active'); loadBuckets(); showToast('Bucket created!', 'success'); } else { elements.createBucketError.textContent = data.error; elements.createBucketError.classList.remove('hidden'); } } catch (error) { elements.createBucketError.textContent = 'Connection error'; elements.createBucketError.classList.remove('hidden'); } btn.querySelector('.btn-text').classList.remove('hidden'); btn.querySelector('.btn-loader').classList.add('hidden'); }); async function deleteBucket(bucketId) { try { const response = await fetch(`/api/buckets/${bucketId}`, { method: 'DELETE', headers: { 'Authorization': `Bearer ${state.token}` } }); if (response.ok) { if (state.selectedBucket === bucketId) state.selectedBucket = ''; loadBuckets(); loadDocuments(); showToast('Bucket deleted', 'success'); } } catch (error) { showToast('Failed to delete bucket', 'error'); } } // ==================== Documents ==================== async function loadDocuments() { if (!state.token) return; try { let url = '/api/documents'; if (state.selectedBucket) url += `?bucket_id=${state.selectedBucket}`; const response = await fetch(url, { headers: { 'Authorization': `Bearer ${state.token}` } }); if (response.ok) { const data = await response.json(); state.documents = data.documents; renderDocuments(); } } catch (error) { console.error('Failed to load documents:', error); } } function renderDocuments() { elements.docCount.textContent = `(${state.documents.length})`; if (state.documents.length === 0) { elements.documentsList.innerHTML = `
📭
No documents yet
`; return; } const icons = { pdf: '📕', word: '📘', powerpoint: '📙', excel: '📗', image: '🖼️', text: '📄' }; elements.documentsList.innerHTML = state.documents.map(doc => `
${icons[doc.doc_type] || '📄'}
${doc.filename}
${formatDate(doc.created_at)}
`).join(''); } function formatDate(timestamp) { const date = new Date(timestamp * 1000); const now = new Date(); const diff = now - date; if (diff < 60000) return 'Just now'; if (diff < 3600000) return `${Math.floor(diff / 60000)}m ago`; if (diff < 86400000) return `${Math.floor(diff / 3600000)}h ago`; return date.toLocaleDateString(); } async function deleteDocument(docId) { try { const response = await fetch(`/api/documents/${docId}`, { method: 'DELETE', headers: { 'Authorization': `Bearer ${state.token}` } }); if (response.ok) { state.documents = state.documents.filter(d => d.doc_id !== docId); // Clear selection if deleted doc was selected if (state.selectedDocument === docId) { state.selectedDocument = null; hideSummary(); } // Remove from summaries cache delete state.summaries[docId]; renderDocuments(); loadBuckets(); showToast('Document deleted', 'success'); } } catch (error) { showToast('Failed to delete', 'error'); } } // ==================== Document Summary ==================== function selectDocument(docId) { state.selectedDocument = docId; renderDocuments(); displaySummary(docId); } async function displaySummary(docId) { const doc = state.documents.find(d => d.doc_id === docId); if (!doc) return; // Check if summary is cached if (state.summaries[docId]) { showSummaryPanel(doc.filename, state.summaries[docId].summary); } else { // Show loading state showSummaryPanel(doc.filename, 'Generating summary...'); // Fetch summary from server await fetchSummary(docId); } } async function fetchSummary(docId) { try { const response = await fetch(`/api/documents/${docId}/summary`, { headers: { 'Authorization': `Bearer ${state.token}` } }); const data = await response.json(); if (response.ok && data.summary) { // Cache the summary state.summaries[docId] = { summary: data.summary, filename: data.filename }; // Update display if still selected if (state.selectedDocument === docId) { showSummaryPanel(data.filename, data.summary); } } else { // Show error state if (state.selectedDocument === docId) { showSummaryPanel(data.filename || 'Document', 'Unable to generate summary.'); } } } catch (error) { console.error('Failed to fetch summary:', error); if (state.selectedDocument === docId) { const doc = state.documents.find(d => d.doc_id === docId); showSummaryPanel(doc?.filename || 'Document', 'Failed to load summary.'); } } } function showSummaryPanel(filename, summaryText) { elements.summaryPanel.classList.remove('hidden'); elements.summaryTitle.textContent = filename; elements.summaryText.textContent = summaryText; } function hideSummary() { elements.summaryPanel.classList.add('hidden'); state.selectedDocument = null; renderDocuments(); } function initSummaryPanel() { elements.summaryClose.addEventListener('click', hideSummary); } // ==================== Document Viewer ==================== async function viewDocument(docId, filename) { try { // Fetch the document with proper authorization const response = await fetch(`/api/documents/${docId}/view`, { headers: { 'Authorization': `Bearer ${state.token}` } }); if (!response.ok) { showToast('Failed to load document', 'error'); return; } // Get the blob and create a URL const blob = await response.blob(); const blobUrl = URL.createObjectURL(blob); // Open in a new tab window.open(blobUrl, '_blank'); } catch (error) { console.error('Failed to view document:', error); showToast('Failed to open document', 'error'); } } elements.closeDocViewer.addEventListener('click', () => elements.docViewerModal.classList.remove('active')); // ==================== Upload ==================== let currentPollInterval = null; // Track the current polling interval for cancellation function initUpload() { elements.uploadZone.addEventListener('click', () => elements.fileInput.click()); elements.fileInput.addEventListener('change', (e) => { if (e.target.files.length > 0) uploadFiles(Array.from(e.target.files)); }); elements.uploadZone.addEventListener('dragover', (e) => { e.preventDefault(); elements.uploadZone.classList.add('dragover'); }); elements.uploadZone.addEventListener('dragleave', () => elements.uploadZone.classList.remove('dragover')); elements.uploadZone.addEventListener('drop', (e) => { e.preventDefault(); elements.uploadZone.classList.remove('dragover'); if (e.dataTransfer.files.length > 0) uploadFiles(Array.from(e.dataTransfer.files)); }); // Cancel upload button elements.cancelUploadBtn.addEventListener('click', cancelUpload); } function cancelUpload() { state.uploadCancelled = true; // Abort any ongoing fetch request if (state.currentUploadAbortController) { state.currentUploadAbortController.abort(); state.currentUploadAbortController = null; } // Clear any polling interval if (currentPollInterval) { clearInterval(currentPollInterval); currentPollInterval = null; } // Reset UI elements.uploadProgress.classList.add('hidden'); elements.uploadZone.style.pointerEvents = ''; elements.fileInput.value = ''; elements.progressFill.style.width = '0%'; showToast('Upload cancelled', 'info'); } async function uploadFiles(files) { // Reset cancellation state state.uploadCancelled = false; elements.uploadProgress.classList.remove('hidden'); elements.uploadZone.style.pointerEvents = 'none'; const bucketId = elements.uploadBucketSelect.value; let completed = 0; // Process files sequentially to avoid overwhelming the client, // but the server handles them in background. for (const file of files) { // Check if cancelled before processing each file if (state.uploadCancelled) { break; } elements.uploadStatus.textContent = `Uploading ${file.name}...`; elements.progressFill.style.width = '10%'; // Initial progress const formData = new FormData(); formData.append('file', file); formData.append('bucket_id', bucketId); // Create abort controller for this request state.currentUploadAbortController = new AbortController(); try { // Initial upload request const response = await fetch('/api/documents/upload', { method: 'POST', headers: { 'Authorization': `Bearer ${state.token}` }, body: formData, signal: state.currentUploadAbortController.signal }); if (response.status === 202) { // Async processing started const data = await response.json(); await pollUploadStatus(data.doc_id, file.name); if (!state.uploadCancelled) { completed++; } } else if (response.ok) { // Instant completion (legacy or small file) const data = await response.json(); handleUploadSuccess(data); completed++; } else { const data = await response.json(); showToast(`Failed: ${file.name} - ${data.error}`, 'error'); } } catch (e) { if (e.name === 'AbortError') { // Upload was cancelled by user break; } console.error(e); showToast(`Failed to upload ${file.name}`, 'error'); } } // Clean up abort controller state.currentUploadAbortController = null; // Only update UI if not cancelled (cancelUpload already handles UI reset) if (!state.uploadCancelled) { elements.uploadProgress.classList.add('hidden'); elements.uploadZone.style.pointerEvents = ''; elements.fileInput.value = ''; elements.progressFill.style.width = '0%'; // Load documents first, then show summary await loadDocuments(); loadBuckets(); } } async function pollUploadStatus(docId, filename) { return new Promise((resolve, reject) => { currentPollInterval = setInterval(async () => { // Check if cancelled if (state.uploadCancelled) { clearInterval(currentPollInterval); currentPollInterval = null; resolve(); return; } try { const response = await fetch(`/api/documents/${docId}/status`, { headers: { 'Authorization': `Bearer ${state.token}` } }); if (response.ok) { const statusData = await response.json(); // Update UI elements.uploadStatus.textContent = `Processing ${filename}: ${statusData.message || '...'}`; // Map 0-100 progress to UI width (keeping 10% buffer) if (statusData.progress) { elements.progressFill.style.width = `${Math.max(10, statusData.progress)}%`; } if (statusData.status === 'completed') { clearInterval(currentPollInterval); currentPollInterval = null; if (statusData.result) { handleUploadSuccess(statusData.result); } resolve(); } else if (statusData.status === 'failed') { clearInterval(currentPollInterval); currentPollInterval = null; showToast(`Processing failed: ${filename} - ${statusData.error}`, 'error'); resolve(); // Resolve anyway to continue with next file } } else { // Status check failed - might be network glitch, ignore once } } catch (e) { console.error("Polling error", e); // Continue polling despite error } }, 2000); // Check every 2 seconds }); } function handleUploadSuccess(data) { showToast(`Ready: ${data.filename}`, 'success'); // Cache the summary if (data.summary) { state.summaries[data.doc_id] = { summary: data.summary, filename: data.filename }; } // Auto-display this document state.selectedDocument = data.doc_id; // We will re-render documents shortly after this returns if (data.summary) { // Defer slightly to ensure DOM is ready if needed setTimeout(() => { showSummaryPanel(data.filename, data.summary); }, 500); } } // ==================== Chat ==================== function initChat() { elements.chatInput.addEventListener('input', () => { elements.chatInput.style.height = 'auto'; elements.chatInput.style.height = Math.min(elements.chatInput.scrollHeight, 150) + 'px'; elements.sendBtn.disabled = !elements.chatInput.value.trim(); }); elements.chatInput.addEventListener('keydown', (e) => { if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); sendMessage(); } }); elements.sendBtn.addEventListener('click', sendMessage); // Stop generation button elements.stopBtn.addEventListener('click', stopGeneration); } function stopGeneration() { if (state.streamAbortController) { state.streamAbortController.abort(); state.streamAbortController = null; } // Hide stop button, show send button elements.stopBtn.classList.add('hidden'); elements.sendBtn.classList.remove('hidden'); elements.typingIndicator.classList.add('hidden'); state.isLoading = false; // Add a note that generation was stopped if (state.messages.length > 0) { const lastMsg = state.messages[state.messages.length - 1]; if (lastMsg.role === 'assistant' && lastMsg.content) { lastMsg.content += '\n\n*[Generation stopped]*'; renderMessages(); saveCurrentChat(); } } showToast('Generation stopped', 'info'); } async function sendMessage() { const message = elements.chatInput.value.trim(); if (!message || state.isLoading) return; elements.chatInput.value = ''; elements.chatInput.style.height = 'auto'; elements.sendBtn.disabled = true; elements.welcomeScreen.classList.add('hidden'); // Create a chat ID if this is the first message if (state.messages.length === 0 && !state.currentChatId) { state.currentChatId = Date.now().toString(); } const targetChatId = state.currentChatId; addMessage('user', message); elements.typingIndicator.classList.remove('hidden'); state.isLoading = true; scrollToBottom(); // Show stop button, hide send button elements.sendBtn.classList.add('hidden'); elements.stopBtn.classList.remove('hidden'); // Create abort controller for this request state.streamAbortController = new AbortController(); try { // Use streaming endpoint for instant response const response = await fetch('/api/chat/stream', { method: 'POST', headers: { 'Authorization': `Bearer ${state.token}`, 'Content-Type': 'application/json' }, body: JSON.stringify({ message: message, bucket_id: state.chatBucket || null, chat_id: state.currentChatId }), signal: state.streamAbortController.signal }); if (!response.ok) { throw new Error('Stream request failed'); } elements.typingIndicator.classList.add('hidden'); // Create a placeholder message for streaming let streamingContent = ''; let sources = []; // Add empty assistant message and get reference to its content element state.messages.push({ role: 'assistant', content: '', sources: [] }); renderMessages(); scrollToBottom(); // Get direct reference to the streaming message element for fast updates const messageElements = elements.chatMessages.querySelectorAll('.message.assistant .message-content'); const streamingElement = messageElements[messageElements.length - 1]; const reader = response.body.getReader(); const decoder = new TextDecoder(); // Throttle DOM updates for smooth rendering (update every 50ms max) let lastUpdateTime = 0; let pendingUpdate = false; const UPDATE_INTERVAL = 50; // ms while (true) { const { done, value } = await reader.read(); if (done) break; const text = decoder.decode(value); const lines = text.split('\n'); for (const line of lines) { if (line.startsWith('data: ')) { try { const data = JSON.parse(line.slice(6)); if (data.type === 'sources') { sources = data.sources || []; } else if (data.type === 'chunk' || data.type === 'content') { // Support both 'chunk' (legacy) and 'content' (specialized queries) streamingContent += data.content; // Update state for saving later state.messages[state.messages.length - 1].content = streamingContent; state.messages[state.messages.length - 1].sources = sources; // Throttled DOM update for smooth rendering const now = Date.now(); if (now - lastUpdateTime >= UPDATE_INTERVAL) { if (streamingElement) { streamingElement.innerHTML = formatContent(streamingContent); } lastUpdateTime = now; pendingUpdate = false; } else { pendingUpdate = true; } // No auto-scroll during streaming - stay at current position } else if (data.type === 'done') { // Final update with any pending content if (pendingUpdate && streamingElement) { streamingElement.innerHTML = formatContent(streamingContent); } // Streaming complete - do final render for proper formatting renderMessages(); saveCurrentChat(); // No auto-scroll - user stays at current position } else if (data.type === 'error') { state.messages[state.messages.length - 1].content = data.content || 'Error generating response'; renderMessages(); } } catch (e) { // Skip malformed JSON } } } } } catch (err) { elements.typingIndicator.classList.add('hidden'); // Only show error if not aborted by user if (err.name !== 'AbortError') { addMessageToChat(targetChatId, 'assistant', 'Connection error. Please try again.'); } } // Cleanup: hide stop button, show send button elements.stopBtn.classList.add('hidden'); elements.sendBtn.classList.remove('hidden'); state.streamAbortController = null; state.isLoading = false; // No auto-scroll - user stays at current position } function addMessage(role, content, sources = []) { // Create a new chat ID if this is the first message if (state.messages.length === 0 && !state.currentChatId) { state.currentChatId = Date.now().toString(); } state.messages.push({ role, content, sources }); renderMessages(); // Auto-save after assistant responds (complete exchange) if (role === 'assistant') { saveCurrentChat(); } } // Add message to a specific chat (handles case where user switched chats during loading) function addMessageToChat(chatId, role, content, sources = []) { // If this is the current chat, add directly if (chatId === state.currentChatId) { state.messages.push({ role, content, sources }); renderMessages(); saveCurrentChat(); } else { // Add to the chat in history const chatIndex = state.chatHistory.findIndex(c => c.id === chatId); if (chatIndex >= 0) { state.chatHistory[chatIndex].messages.push({ role, content, sources }); saveChatHistory(); syncChatToServer(state.chatHistory[chatIndex]); renderChatHistory(); showToast('Response added to previous chat', 'info'); } } } function renderMessages() { // Preserve summary panel state before re-rendering const summaryVisible = !elements.summaryPanel.classList.contains('hidden'); const summaryTitle = elements.summaryTitle.textContent; const summaryText = elements.summaryText.textContent; if (state.messages.length === 0) { // Clear chat messages and show welcome screen elements.chatMessages.innerHTML = ''; elements.welcomeScreen.classList.remove('hidden'); elements.chatMessages.appendChild(elements.welcomeScreen); // Re-show summary if it was visible if (summaryVisible) { elements.summaryPanel.classList.remove('hidden'); } return; } elements.welcomeScreen.classList.add('hidden'); const html = state.messages.map((msg, i) => { const avatar = msg.role === 'user' ? (state.user?.username?.charAt(0).toUpperCase() || 'U') : '🧠'; const isUserMessage = msg.role === 'user'; return `
${avatar}
${formatContent(msg.content, isUserMessage)}
`; }).join(''); // Build full content with summary panel and welcome screen const summaryPanelHTML = `
📄 ${summaryTitle}
${summaryText}
`; elements.chatMessages.innerHTML = summaryPanelHTML + html + elements.welcomeScreen.outerHTML; document.getElementById('welcomeScreen')?.classList.add('hidden'); // Re-bind summary panel elements and event listener elements.summaryPanel = document.getElementById('summaryPanel'); elements.summaryTitle = document.getElementById('summaryTitle'); elements.summaryText = document.getElementById('summaryText'); elements.summaryClose = document.getElementById('summaryClose'); elements.summaryClose.addEventListener('click', hideSummary); } function formatContent(content, isUserMessage = false) { // Enhanced markdown parsing for beautiful formatting let html = content; // For user messages, escape HTML and preserve line breaks if (isUserMessage) { // Escape HTML to prevent XSS html = html .replace(/&/g, '&') .replace(//g, '>') .replace(/"/g, '"') .replace(/'/g, '''); // Convert line breaks to
html = html.replace(/\n/g, '
'); return html; } // Escape HTML special characters first (except for already parsed markdown) // Skip this if content looks like it's already HTML if (!html.includes(' { return `
${code.trim()}
`; }); // Tables: | Header | Header | html = html.replace(/(?:^|\n)(\|.+\|)\n(\|[-:\s|]+\|)\n((?:\|.+\|\n?)+)/gm, (match, headerRow, sepRow, bodyRows) => { const headers = headerRow.split('|').filter(cell => cell.trim()).map(cell => `${cell.trim()}` ).join(''); const rows = bodyRows.trim().split('\n').map(row => { const cells = row.split('|').filter(cell => cell.trim()).map(cell => `${cell.trim()}` ).join(''); return `${cells}`; }).join(''); return `
${headers}${rows}
`; }); // Headers: ### Header, ## Header, # Header html = html.replace(/^#### (.+)$/gm, '

$1

'); html = html.replace(/^### (.+)$/gm, '

$1

'); html = html.replace(/^## (.+)$/gm, '

$1

'); html = html.replace(/^# (.+)$/gm, '

$1

'); // Bold headers at start of line (NotebookLM style) html = html.replace(/^(\*\*[^*]+\*\*):?\s*$/gm, '

$1

'); // Bold text: **text** html = html.replace(/\*\*([^*]+)\*\*/g, '$1'); // Italic text: *text* html = html.replace(/(?$1'); // Inline code: `code` html = html.replace(/`([^`]+)`/g, '$1'); // Horizontal rule: --- or *** html = html.replace(/^[-*]{3,}$/gm, '
'); // Numbered lists: 1. Item, 2. Item, etc. html = html.replace(/^(\d+)\.\s+(.+)$/gm, '
  • $1. $2
  • '); // Bullet points: • Item or - Item or * Item at start of line html = html.replace(/^[\•\-\*]\s+(.+)$/gm, '
  • $1
  • '); // Sub-bullets with indentation (2+ spaces before bullet) html = html.replace(/^[\s]{2,}[\•\-\*]\s+(.+)$/gm, '
  • $1
  • '); // Wrap consecutive numbered list items html = html.replace(/(
  • [\s\S]*?<\/li>\n?)+/g, '
      $&
    '); // Wrap consecutive bullet items html = html.replace(/(
  • [\s\S]*?<\/li>\n?)+/g, ''); // Wrap consecutive sub-bullet items html = html.replace(/(
  • [\s\S]*?<\/li>\n?)+/g, ''); // Blockquotes: > text html = html.replace(/^>\s+(.+)$/gm, '
    $1
    '); // Merge consecutive blockquotes html = html.replace(/<\/blockquote>\n
    /g, '
    '); // Double newlines become paragraph breaks html = html.replace(/\n\n+/g, '

    '); // Single newlines become line breaks (but not inside lists) html = html.replace(/\n/g, '
    '); // Clean up br tags in lists, headers, tables html = html.replace(/


  • /g, '
  • '); html = html.replace(/

    /g, ''); html = html.replace(/

        /g, '
    '); html = html.replace(/<\/ol>
    /g, ''); html = html.replace(/

    /g, '
    '); html = html.replace(/

    /g, '
    '); html = html.replace(/

    /g, '
    '); html = html.replace(/

    ]*>
    /g, '
    '); html = html.replace(/

    /g, '
    '); // Wrap in paragraph html = '

    ' + html + '

    '; // Clean up empty paragraphs html = html.replace(/

    <\/p>/g, ''); html = html.replace(/

    (\s|
    )*<\/p>/g, ''); html = html.replace(/

    <(h\d|ul|ol|table|div|pre|hr|blockquote)/g, '<$1'); html = html.replace(/<\/(h\d|ul|ol|table|div|pre|blockquote)><\/p>/g, ''); html = html.replace(/


    m.role === 'user'); if (firstUserMsg) { // Truncate to first 40 chars let topic = firstUserMsg.content.substring(0, 40); if (firstUserMsg.content.length > 40) topic += '...'; return topic; } return 'New Conversation'; } function saveChatHistory() { localStorage.setItem('Iribl AI_chat_history', JSON.stringify(state.chatHistory)); } // Sync chat to server async function syncChatToServer(chatData) { if (!state.token) return; try { await fetch('/api/chats', { method: 'POST', headers: { 'Authorization': `Bearer ${state.token}`, 'Content-Type': 'application/json' }, body: JSON.stringify(chatData) }); } catch (error) { console.error('Failed to sync chat to server:', error); } } // Load chat history from server async function loadChatHistoryFromServer() { if (!state.token) return; try { const response = await fetch('/api/chats', { headers: { 'Authorization': `Bearer ${state.token}` } }); if (response.ok) { const data = await response.json(); if (data.chats && data.chats.length > 0) { // Merge server chats with local (server takes priority) state.chatHistory = data.chats; saveChatHistory(); // Update local storage renderChatHistory(); } } } catch (error) { console.error('Failed to load chats from server:', error); } } // Delete chat from server async function deleteChatFromServer(chatId) { if (!state.token) return; try { await fetch(`/api/chats/${chatId}`, { method: 'DELETE', headers: { 'Authorization': `Bearer ${state.token}` } }); } catch (error) { console.error('Failed to delete chat from server:', error); } } function saveCurrentChat() { // Only save if there are messages if (state.messages.length === 0) return null; const chatId = state.currentChatId || Date.now().toString(); const topic = generateChatTopic(state.messages); // Check if this chat already exists const existingIndex = state.chatHistory.findIndex(c => c.id === chatId); const chatData = { id: chatId, topic: topic, messages: [...state.messages], timestamp: Date.now(), bucket: state.chatBucket }; if (existingIndex >= 0) { // Update existing chat state.chatHistory[existingIndex] = chatData; } else { // Add new chat at the beginning state.chatHistory.unshift(chatData); } saveChatHistory(); renderChatHistory(); // Sync to server syncChatToServer(chatData); return chatId; } function startNewChat() { // Warn if AI is still generating if (state.isLoading) { showToast('AI is still responding - response will go to current chat', 'info'); } // Save current chat first if it has messages if (state.messages.length > 0) { saveCurrentChat(); } // Clear current chat state.messages = []; state.currentChatId = null; // Reset UI renderMessages(); elements.welcomeScreen.classList.remove('hidden'); hideSummary(); renderChatHistory(); showToast('Started new chat', 'info'); } function loadChatFromHistory(chatId) { // Warn if AI is still generating if (state.isLoading) { showToast('AI is still responding - response will go to current chat', 'info'); } // Save current chat first if it has messages if (state.messages.length > 0 && state.currentChatId !== chatId) { saveCurrentChat(); } const chat = state.chatHistory.find(c => c.id === chatId); if (!chat) return; // Load the chat state.messages = [...chat.messages]; state.currentChatId = chat.id; state.chatBucket = chat.bucket || ''; // Update bucket dropdown if (elements.chatBucketSelect) { elements.chatBucketSelect.value = state.chatBucket; const bucketName = state.chatBucket ? state.buckets.find(b => b.bucket_id === state.chatBucket)?.name || 'Selected Bucket' : 'All Documents'; elements.chatBucketTrigger.querySelector('.select-value').textContent = bucketName; } // Render messages renderMessages(); // Show/hide welcome screen based on whether chat has messages if (state.messages.length === 0) { elements.welcomeScreen.classList.remove('hidden'); } else { elements.welcomeScreen.classList.add('hidden'); } renderChatHistory(); scrollToBottom(); } function deleteChatFromHistory(chatId) { event.stopPropagation(); state.chatHistory = state.chatHistory.filter(c => c.id !== chatId); // If deleting current chat, clear it if (state.currentChatId === chatId) { state.messages = []; state.currentChatId = null; renderMessages(); elements.welcomeScreen.classList.remove('hidden'); } saveChatHistory(); renderChatHistory(); // Delete from server deleteChatFromServer(chatId); showToast('Chat deleted', 'success'); } function renderChatHistory() { // Filter chats by selected bucket let filteredChats = state.chatHistory; if (state.selectedBucket) { filteredChats = state.chatHistory.filter(chat => chat.bucket === state.selectedBucket || // Also include chats with no bucket for backwards compatibility (!chat.bucket && !state.selectedBucket) ); } const count = filteredChats.length; const totalCount = state.chatHistory.length; // Show filtered count vs total if filtering is active elements.chatHistoryCount.textContent = state.selectedBucket && count !== totalCount ? `(${count}/${totalCount})` : `(${totalCount})`; if (count === 0) { elements.chatHistoryList.innerHTML = state.selectedBucket ? `
    No chats in this bucket
    ` : `
    No chats yet
    `; return; } elements.chatHistoryList.innerHTML = filteredChats.map(chat => { const isActive = state.currentChatId === chat.id; const date = formatDate(chat.timestamp / 1000); return `
    💬
    ${chat.topic}
    ${date}
    `; }).join(''); } function clearCurrentChat() { // Warn if AI is still generating if (state.isLoading) { showToast('AI is still responding - response will go to current chat', 'info'); } // If there's a current chat, clear its messages but keep it in history if (state.currentChatId) { const chatIndex = state.chatHistory.findIndex(c => c.id === state.currentChatId); if (chatIndex >= 0) { // Clear the messages in history state.chatHistory[chatIndex].messages = []; saveChatHistory(); // Sync cleared chat to server syncChatToServer(state.chatHistory[chatIndex]); } } // Clear current chat messages state.messages = []; // Reset UI renderMessages(); elements.welcomeScreen.classList.remove('hidden'); hideSummary(); renderChatHistory(); showToast('Chat cleared', 'info'); } function initChatHistory() { // New Chat button handler elements.newChatBtn.addEventListener('click', startNewChat); // Clear Chat button handler (sidebar) elements.clearChatBtn.addEventListener('click', (e) => { e.stopPropagation(); clearCurrentChat(); }); // Clear Chat button handler (top) elements.clearChatBtnTop.addEventListener('click', clearCurrentChat); // Render existing history renderChatHistory(); // Auto-save current chat when sending messages (hook into sendMessage) // This is handled by updating currentChatId after first message } // ==================== Init ==================== function init() { initSidebars(); initMobileNavigation(); initCollapsible(); initCustomDropdowns(); initUpload(); initChat(); initSummaryPanel(); initChatHistory(); verifyToken(); } document.addEventListener('DOMContentLoaded', init);