Spaces:
Running
Running
| // Research Radar - Enhanced Interactive JavaScript | |
| class ResearchRadar { | |
| constructor() { | |
| this.currentSection = 'search'; | |
| this.currentPage = 'landing'; | |
| this.isLoading = false; | |
| this.chatHistory = []; | |
| this.currentDocumentId = null; | |
| this.uploadProgress = 0; | |
| this.searchCache = new Map(); | |
| this.init(); | |
| } | |
| init() { | |
| this.setupEventListeners(); | |
| this.setupDragAndDrop(); | |
| this.initializeChat(); | |
| this.setupChatInput(); | |
| this.setupSearchSuggestions(); | |
| this.updateStatusIndicator(); | |
| this.setupPageNavigation(); | |
| this.setupEnhancedChat(); | |
| this.setupMobileNav(); | |
| this.setupVhUnit(); | |
| } | |
| setupEventListeners() { | |
| console.log('Setting up event listeners...'); | |
| // Landing page CTA buttons - Critical buttons that need to work | |
| this.setupLandingPageButtons(); | |
| // Navigation for app page | |
| const navLinks = document.querySelectorAll('.app-nav .nav-link'); | |
| console.log(`Found ${navLinks.length} navigation links`); | |
| navLinks.forEach(link => { | |
| link.addEventListener('click', (e) => { | |
| e.preventDefault(); | |
| const section = link.dataset.section; | |
| console.log(`Navigation clicked: ${section}`); | |
| if (section) { | |
| this.switchSection(section); | |
| } | |
| }); | |
| }); | |
| // Quick search cards | |
| const quickSearchCards = document.querySelectorAll('.quick-search-card'); | |
| console.log(`Found ${quickSearchCards.length} quick search cards`); | |
| quickSearchCards.forEach(card => { | |
| card.addEventListener('click', (e) => { | |
| const query = card.dataset.query; | |
| console.log(`Quick search card clicked: ${query}`); | |
| if (query) { | |
| const searchInput = document.getElementById('searchInput'); | |
| if (searchInput) { | |
| searchInput.value = query; | |
| this.searchPapers(); | |
| } | |
| } | |
| }); | |
| }); | |
| // Chat suggestions | |
| document.querySelectorAll('.suggestion-chip').forEach(chip => { | |
| chip.addEventListener('click', (e) => { | |
| const question = chip.dataset.question; | |
| if (question) { | |
| document.getElementById('chatInput').value = question; | |
| this.sendChatMessage(); | |
| } | |
| }); | |
| }); | |
| // Example URLs | |
| document.querySelectorAll('.example-url').forEach(btn => { | |
| btn.addEventListener('click', (e) => { | |
| const url = btn.dataset.url; | |
| if (url) { | |
| document.getElementById('paperUrl').value = url; | |
| } | |
| }); | |
| }); | |
| // Clear chat button | |
| const clearBtn = document.getElementById('chatClearBtn'); | |
| if (clearBtn) { | |
| clearBtn.addEventListener('click', () => this.clearChat()); | |
| } | |
| // Search functionality | |
| const searchBtn = document.getElementById('searchBtn'); | |
| const searchInput = document.getElementById('searchInput'); | |
| console.log('Search button found:', !!searchBtn); | |
| console.log('Search input found:', !!searchInput); | |
| if (searchBtn) { | |
| searchBtn.addEventListener('click', () => { | |
| console.log('Search button clicked!'); | |
| this.searchPapers(); | |
| }); | |
| } | |
| if (searchInput) { | |
| searchInput.addEventListener('keypress', (e) => { | |
| if (e.key === 'Enter') { | |
| console.log('Search input Enter pressed!'); | |
| this.searchPapers(); | |
| } | |
| }); | |
| } | |
| // File upload | |
| const fileInput = document.getElementById('fileInput'); | |
| console.log('File input found:', !!fileInput); | |
| if (fileInput) { | |
| fileInput.addEventListener('change', (e) => { | |
| console.log('File input changed!'); | |
| this.handleFileUpload(e); | |
| }); | |
| } | |
| // URL analysis | |
| const analyzeUrlBtn = document.getElementById('analyzeUrlBtn'); | |
| const paperUrlInput = document.getElementById('paperUrl'); | |
| console.log('Analyze URL button found:', !!analyzeUrlBtn); | |
| console.log('Paper URL input found:', !!paperUrlInput); | |
| if (analyzeUrlBtn) { | |
| analyzeUrlBtn.addEventListener('click', () => { | |
| console.log('Analyze URL button clicked!'); | |
| this.analyzePaperUrl(); | |
| }); | |
| } | |
| if (paperUrlInput) { | |
| paperUrlInput.addEventListener('keypress', (e) => { | |
| if (e.key === 'Enter') { | |
| console.log('Paper URL input Enter pressed!'); | |
| this.analyzePaperUrl(); | |
| } | |
| }); | |
| } | |
| // Chat functionality | |
| const chatSendBtn = document.getElementById('chatSendBtn'); | |
| const chatInput = document.getElementById('chatInput'); | |
| const chatSendBtnPanel = document.getElementById('chatSendBtnPanel'); | |
| const chatInputPanel = document.getElementById('chatInputPanel'); | |
| if (chatSendBtn) { | |
| chatSendBtn.addEventListener('click', () => this.sendChatMessage()); | |
| } | |
| if (chatInput) { | |
| chatInput.addEventListener('keypress', (e) => { | |
| if (e.key === 'Enter') { | |
| this.sendChatMessage(); | |
| } | |
| }); | |
| } | |
| // Panel chat functionality | |
| if (chatSendBtnPanel) { | |
| chatSendBtnPanel.addEventListener('click', () => this.sendChatMessagePanel()); | |
| } | |
| if (chatInputPanel) { | |
| chatInputPanel.addEventListener('keypress', (e) => { | |
| if (e.key === 'Enter' && !e.shiftKey) { | |
| e.preventDefault(); | |
| this.sendChatMessagePanel(); | |
| } | |
| }); | |
| } | |
| } | |
| setupLandingPageButtons() { | |
| console.log('Setting up landing page buttons...'); | |
| // Get Started button (nav) | |
| const getStartedBtn = document.querySelector('.nav-cta-btn'); | |
| console.log('Get Started button found:', !!getStartedBtn); | |
| if (getStartedBtn) { | |
| // Remove existing onclick to prevent conflicts | |
| getStartedBtn.removeAttribute('onclick'); | |
| getStartedBtn.addEventListener('click', (e) => { | |
| e.preventDefault(); | |
| console.log('Get Started button clicked!'); | |
| this.navigateToApp('search'); | |
| }); | |
| } | |
| // Start Exploring button | |
| const startExploringBtn = document.querySelector('.cta-button.primary'); | |
| console.log('Start Exploring button found:', !!startExploringBtn); | |
| if (startExploringBtn) { | |
| // Remove existing onclick to prevent conflicts | |
| startExploringBtn.removeAttribute('onclick'); | |
| startExploringBtn.addEventListener('click', (e) => { | |
| e.preventDefault(); | |
| console.log('Start Exploring button clicked!'); | |
| this.navigateToApp('search'); | |
| }); | |
| } | |
| // Upload Paper button | |
| const uploadPaperBtn = document.querySelector('.cta-button.secondary'); | |
| console.log('Upload Paper button found:', !!uploadPaperBtn); | |
| if (uploadPaperBtn) { | |
| // Remove existing onclick to prevent conflicts | |
| uploadPaperBtn.removeAttribute('onclick'); | |
| uploadPaperBtn.addEventListener('click', (e) => { | |
| e.preventDefault(); | |
| console.log('Upload Paper button clicked!'); | |
| this.navigateToApp('upload'); | |
| }); | |
| } | |
| // Back to Landing button | |
| const backToLandingBtn = document.querySelector('.back-to-landing'); | |
| console.log('Back to Landing button found:', !!backToLandingBtn); | |
| if (backToLandingBtn) { | |
| // Remove existing onclick to prevent conflicts | |
| backToLandingBtn.removeAttribute('onclick'); | |
| backToLandingBtn.addEventListener('click', (e) => { | |
| e.preventDefault(); | |
| console.log('Back to Landing button clicked!'); | |
| this.navigateToLanding(); | |
| }); | |
| } | |
| // Brand logo that navigates to landing | |
| const brandLogo = document.querySelector('.app-nav .nav-brand'); | |
| console.log('Brand logo found:', !!brandLogo); | |
| if (brandLogo) { | |
| // Remove existing onclick to prevent conflicts | |
| brandLogo.removeAttribute('onclick'); | |
| brandLogo.addEventListener('click', (e) => { | |
| e.preventDefault(); | |
| console.log('Brand logo clicked!'); | |
| this.navigateToLanding(); | |
| }); | |
| } | |
| } | |
| setupDragAndDrop() { | |
| const uploadZone = document.getElementById('uploadZone'); | |
| if (!uploadZone) return; | |
| ['dragenter', 'dragover', 'dragleave', 'drop'].forEach(eventName => { | |
| uploadZone.addEventListener(eventName, this.preventDefaults, false); | |
| }); | |
| ['dragenter', 'dragover'].forEach(eventName => { | |
| uploadZone.addEventListener(eventName, () => { | |
| uploadZone.classList.add('drag-over'); | |
| }, false); | |
| }); | |
| ['dragleave', 'drop'].forEach(eventName => { | |
| uploadZone.addEventListener(eventName, () => { | |
| uploadZone.classList.remove('drag-over'); | |
| }, false); | |
| }); | |
| uploadZone.addEventListener('drop', (e) => { | |
| const files = e.dataTransfer.files; | |
| if (files.length > 0) { | |
| this.processFile(files[0]); | |
| } | |
| }, false); | |
| } | |
| preventDefaults(e) { | |
| e.preventDefault(); | |
| e.stopPropagation(); | |
| } | |
| switchSection(sectionName) { | |
| // Update navigation | |
| document.querySelectorAll('.nav-link').forEach(link => { | |
| link.classList.remove('active'); | |
| }); | |
| document.querySelector(`[data-section="${sectionName}"]`).classList.add('active'); | |
| // Update sections | |
| document.querySelectorAll('.section').forEach(section => { | |
| section.classList.remove('active'); | |
| }); | |
| document.getElementById(sectionName).classList.add('active'); | |
| this.currentSection = sectionName; | |
| } | |
| showLoading(show = true) { | |
| const overlay = document.getElementById('loadingOverlay'); | |
| if (overlay) { | |
| if (show) { | |
| overlay.classList.add('active'); | |
| this.isLoading = true; | |
| } else { | |
| overlay.classList.remove('active'); | |
| this.isLoading = false; | |
| } | |
| } | |
| } | |
| showToast(message, type = 'info') { | |
| const container = document.getElementById('toastContainer'); | |
| if (!container) return; | |
| const toast = document.createElement('div'); | |
| toast.className = `toast ${type}`; | |
| toast.innerHTML = ` | |
| <div style="display: flex; align-items: center; gap: 0.5rem;"> | |
| <i class="fas fa-${this.getToastIcon(type)}"></i> | |
| <span>${message}</span> | |
| </div> | |
| `; | |
| container.appendChild(toast); | |
| // Auto remove after 5 seconds | |
| setTimeout(() => { | |
| if (toast.parentNode) { | |
| toast.style.animation = 'toastSlideOut 0.3s ease-out forwards'; | |
| setTimeout(() => { | |
| container.removeChild(toast); | |
| }, 300); | |
| } | |
| }, 5000); | |
| } | |
| getToastIcon(type) { | |
| const icons = { | |
| 'success': 'check-circle', | |
| 'error': 'exclamation-circle', | |
| 'warning': 'exclamation-triangle', | |
| 'info': 'info-circle' | |
| }; | |
| return icons[type] || 'info-circle'; | |
| } | |
| async searchPapers() { | |
| const searchInput = document.getElementById('searchInput'); | |
| const query = searchInput.value.trim(); | |
| if (!query) { | |
| this.showToast('Please enter a search query', 'warning'); | |
| return; | |
| } | |
| this.showLoading(true); | |
| try { | |
| const response = await fetch('/search', { | |
| method: 'POST', | |
| headers: { | |
| 'Content-Type': 'application/json', | |
| }, | |
| body: JSON.stringify({ query }) | |
| }); | |
| const data = await response.json(); | |
| if (response.ok) { | |
| this.displaySearchResults(data.papers); | |
| this.showToast(`Found ${data.papers.length} papers`, 'success'); | |
| } else { | |
| throw new Error(data.error || 'Search failed'); | |
| } | |
| } catch (error) { | |
| console.error('Search error:', error); | |
| this.showToast(error.message, 'error'); | |
| } finally { | |
| this.showLoading(false); | |
| } | |
| } | |
| displaySearchResults(papers) { | |
| const container = document.getElementById('searchResults'); | |
| if (!container) return; | |
| // Keep papers for later access (e.g., openSummaryChat after ingest/summarize) | |
| this.lastSearchResults = papers; | |
| if (papers.length === 0) { | |
| container.innerHTML = ` | |
| <div class="result-card"> | |
| <p style="text-align: center; color: var(--text-secondary);"> | |
| <i class="fas fa-search" style="font-size: 2rem; margin-bottom: 1rem; display: block;"></i> | |
| No papers found. Try different keywords. | |
| </p> | |
| </div> | |
| `; | |
| return; | |
| } | |
| container.innerHTML = papers.map((paper, index) => ` | |
| <div class="paper-card"> | |
| <h3 class="paper-title">${this.escapeHtml(paper.title)}</h3> | |
| <div class="paper-authors"> | |
| <i class="fas fa-users"></i> | |
| ${paper.authors.slice(0, 3).map(author => this.escapeHtml(author)).join(', ')} | |
| ${paper.authors.length > 3 ? ` and ${paper.authors.length - 3} others` : ''} | |
| </div> | |
| <div class="paper-meta"> | |
| <span><i class="fas fa-calendar"></i> ${paper.published}</span> | |
| <span><i class="fas fa-tag"></i> ${this.escapeHtml(paper.category)}</span> | |
| </div> | |
| <div class="paper-summary"> | |
| ${this.truncateText(this.escapeHtml(paper.summary), 300)} | |
| </div> | |
| <div class="paper-actions"> | |
| <button class="btn-primary generate-summary-btn" data-paper-url="${paper.url}" data-paper-pdf-url="${paper.pdf_url}" data-paper-index="${index}"> | |
| <i class="fas fa-magic"></i> Generate Summary | |
| </button> | |
| <a href="${paper.pdf_url}" target="_blank" class="btn-secondary"> | |
| <i class="fas fa-file-pdf"></i> View PDF | |
| </a> | |
| <a href="${paper.url}" target="_blank" class="btn-secondary"> | |
| <i class="fas fa-external-link-alt"></i> arXiv Page | |
| </a> | |
| </div> | |
| </div> | |
| `).join(''); | |
| // Add event listeners to Generate Summary buttons | |
| this.setupGenerateSummaryButtons(); | |
| } | |
| setupGenerateSummaryButtons() { | |
| console.log('Setting up Generate Summary buttons...'); | |
| const generateButtons = document.querySelectorAll('.generate-summary-btn'); | |
| console.log(`Found ${generateButtons.length} Generate Summary buttons`); | |
| generateButtons.forEach(button => { | |
| button.addEventListener('click', async (e) => { | |
| e.preventDefault(); | |
| const paperUrl = button.dataset.paperUrl; | |
| const paperIndex = button.dataset.paperIndex; | |
| const pdfUrl = button.dataset.paperPdfUrl; | |
| const paper = this.lastSearchResults && typeof Number(paperIndex) === 'number' | |
| ? this.lastSearchResults[Number(paperIndex)] | |
| : null; | |
| console.log(`Generate Summary button clicked for paper: ${paperUrl}`); | |
| if (paperUrl) { | |
| // Disable button and show loading state | |
| button.disabled = true; | |
| button.innerHTML = '<i class="fas fa-spinner fa-spin"></i> Generating...'; | |
| try { | |
| // Ingest the paper PDF first, then summarize from doc_id | |
| const ingestRes = await fetch('/ingest-paper', { | |
| method: 'POST', | |
| headers: { 'Content-Type': 'application/json' }, | |
| body: JSON.stringify({ pdf_url: pdfUrl, url: paperUrl }) | |
| }); | |
| const ingestData = await ingestRes.json(); | |
| if (!ingestRes.ok || !ingestData.success) { | |
| throw new Error(ingestData.error || 'Failed to ingest paper'); | |
| } | |
| const docId = ingestData.doc_id; | |
| // Persist active document in UI | |
| this.setActiveDocument(docId); | |
| // Now summarize using doc_id (backend will fetch all chunks from Qdrant) | |
| const sumRes = await fetch('/summarize-paper', { | |
| method: 'POST', | |
| headers: { 'Content-Type': 'application/json' }, | |
| body: JSON.stringify({ doc_id: docId }) | |
| }); | |
| const sumData = await sumRes.json(); | |
| if (!sumRes.ok || !sumData.success) { | |
| throw new Error(sumData.error || 'Failed to generate summary'); | |
| } | |
| // Open summary + chat view | |
| const paperData = paper || { title: '', authors: [], published: '', category: '' }; | |
| this.openSummaryChat({ title: paperData.title, authors: paperData.authors, published: paperData.published, category: paperData.category }, sumData.summary); | |
| } catch (error) { | |
| console.error('Error generating summary:', error); | |
| this.showToast('Failed to generate summary. Please try again.', 'error'); | |
| } finally { | |
| // Re-enable button | |
| button.disabled = false; | |
| button.innerHTML = '<i class="fas fa-magic"></i> Generate Summary'; | |
| } | |
| } else { | |
| console.error('No paper URL found for Generate Summary button'); | |
| this.showToast('Error: Paper URL not found', 'error'); | |
| } | |
| }); | |
| }); | |
| } | |
| async handleFileUpload(event) { | |
| const file = event.target.files[0]; | |
| if (file) { | |
| await this.processFile(file); | |
| } | |
| } | |
| async processFile(file) { | |
| // Validate file | |
| const allowedTypes = ['application/pdf', 'text/plain', 'application/vnd.openxmlformats-officedocument.wordprocessingml.document']; | |
| const maxSize = 16 * 1024 * 1024; // 16MB | |
| if (!allowedTypes.includes(file.type)) { | |
| this.showToast('Invalid file type. Please upload PDF, TXT, or DOCX files.', 'error'); | |
| return; | |
| } | |
| if (file.size > maxSize) { | |
| this.showToast('File too large. Maximum size is 16MB.', 'error'); | |
| return; | |
| } | |
| this.showLoading(true); | |
| try { | |
| const formData = new FormData(); | |
| formData.append('file', file); | |
| const response = await fetch('/upload', { | |
| method: 'POST', | |
| body: formData | |
| }); | |
| const data = await response.json(); | |
| if (response.ok) { | |
| this.displayUploadResult(data); | |
| this.showToast('File processed successfully!', 'success'); | |
| } else { | |
| throw new Error(data.error || 'Upload failed'); | |
| } | |
| } catch (error) { | |
| console.error('Upload error:', error); | |
| this.showToast(error.message, 'error'); | |
| } finally { | |
| this.showLoading(false); | |
| // Reset file input | |
| document.getElementById('fileInput').value = ''; | |
| } | |
| } | |
| displayUploadResult(data) { | |
| // Create paper data object for the uploaded file | |
| const paperData = { | |
| title: data.filename || 'Uploaded Document', | |
| authors: ['Uploaded by User'], | |
| published: new Date().toLocaleDateString(), | |
| category: 'Uploaded Document', | |
| filename: data.filename, | |
| word_count: data.word_count | |
| }; | |
| // Store the document ID for chat functionality | |
| this.currentDocumentId = data.doc_id; | |
| // Open the Summary + Chat panel with the uploaded document | |
| this.openSummaryChat(paperData, data.summary); | |
| } | |
| async analyzePaperUrl() { | |
| const urlInput = document.getElementById('paperUrl'); | |
| const url = urlInput.value.trim(); | |
| if (!url) { | |
| this.showToast('Please enter a paper URL', 'warning'); | |
| return; | |
| } | |
| // Basic URL validation | |
| if (!url.includes('arxiv.org')) { | |
| this.showToast('Please enter a valid arXiv URL', 'warning'); | |
| return; | |
| } | |
| this.showLoading(true); | |
| try { | |
| const response = await fetch('/summarize-paper', { | |
| method: 'POST', | |
| headers: { | |
| 'Content-Type': 'application/json', | |
| }, | |
| body: JSON.stringify({ url }) | |
| }); | |
| const data = await response.json(); | |
| if (response.ok) { | |
| this.displayPaperAnalysis(data); | |
| this.showToast('Paper analyzed successfully!', 'success'); | |
| urlInput.value = ''; // Clear input | |
| } else { | |
| throw new Error(data.error || 'Analysis failed'); | |
| } | |
| } catch (error) { | |
| console.error('Analysis error:', error); | |
| this.showToast(error.message, 'error'); | |
| } finally { | |
| this.showLoading(false); | |
| } | |
| } | |
| displayPaperAnalysis(data) { | |
| // Store the document ID for chat functionality | |
| this.currentDocumentId = data.doc_id; | |
| // Open the Summary + Chat panel with the analyzed paper | |
| this.openSummaryChat(data.paper, data.summary); | |
| } | |
| async summarizePaper(paperUrl) { | |
| // Deprecated in favor of ingest -> summarize flow | |
| return this.showToast('Summarization flow updated. Please use Generate Summary button.', 'info'); | |
| } | |
| openSummaryChat(paperData, summaryText) { | |
| // Hide current section and show summary-chat | |
| document.querySelectorAll('.section').forEach(section => { | |
| section.style.display = 'none'; | |
| }); | |
| const summarySection = document.getElementById('summary-chat'); | |
| summarySection.style.display = 'block'; | |
| // Update paper information | |
| document.getElementById('paperTitle').textContent = paperData.title || 'Research Paper'; | |
| document.getElementById('paperAuthor').textContent = paperData.authors ? paperData.authors.join(', ') : 'Unknown Author'; | |
| document.getElementById('paperDate').textContent = paperData.published || new Date().getFullYear(); | |
| document.getElementById('paperCategory').textContent = paperData.category || 'Research'; | |
| // Show summary | |
| this.displaySummaryInPanel(summaryText); | |
| // Setup chat panel | |
| this.setupChatPanel(); | |
| // Store current paper data for chat context | |
| this.currentPaper = paperData; | |
| // Default to Chat tab for immediate Q&A after tabs are in DOM | |
| setTimeout(() => { | |
| try { switchTab('chat'); } catch (_) {} | |
| const chatInput = document.getElementById('chatInputPanel'); | |
| if (chatInput) chatInput.focus(); | |
| }, 150); | |
| } | |
| displaySummaryInPanel(summaryText) { | |
| const summaryLoading = document.getElementById('summaryLoading'); | |
| const summaryTextEl = document.getElementById('summaryText'); | |
| // Hide loading and show summary | |
| summaryLoading.style.display = 'none'; | |
| summaryTextEl.style.display = 'block'; | |
| summaryTextEl.innerHTML = this.formatSummaryText(summaryText); | |
| // Update stats | |
| this.updateSummaryStats(summaryText); | |
| } | |
| formatSummaryText(text) { | |
| // Convert plain text to formatted HTML | |
| return text | |
| .replace(/\n\n/g, '</p><p>') | |
| .replace(/\n/g, '<br>') | |
| .replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>') | |
| .replace(/\*(.*?)\*/g, '<em>$1</em>') | |
| .replace(/^/, '<p>') | |
| .replace(/$/, '</p>'); | |
| } | |
| updateSummaryStats(text) { | |
| const wordCount = text.split(/\s+/).length; | |
| const readingTime = Math.ceil(wordCount / 200); // Average reading speed | |
| const compressionRatio = Math.round((1 - (text.length / (text.length * 3))) * 100); // Estimate | |
| const wc = document.getElementById('wordCount'); | |
| const rt = document.getElementById('readingTime'); | |
| const cr = document.getElementById('compressionRatio'); | |
| if (wc) wc.textContent = wordCount.toLocaleString(); | |
| if (rt) rt.textContent = `${readingTime} min`; | |
| if (cr) cr.textContent = `${compressionRatio}%`; | |
| } | |
| setupChatPanel() { | |
| const chatInput = document.getElementById('chatInputPanel'); | |
| const sendBtn = document.getElementById('chatSendBtnPanel'); | |
| // Clear any existing event listeners | |
| const newChatInput = chatInput.cloneNode(true); | |
| chatInput.parentNode.replaceChild(newChatInput, chatInput); | |
| const newSendBtn = sendBtn.cloneNode(true); | |
| sendBtn.parentNode.replaceChild(newSendBtn, sendBtn); | |
| // Add new event listeners | |
| newChatInput.addEventListener('keydown', (e) => { | |
| if (e.key === 'Enter' && !e.shiftKey) { | |
| e.preventDefault(); | |
| this.sendChatMessagePanel(); | |
| } | |
| }); | |
| newChatInput.addEventListener('input', () => { | |
| this.autoResizeTextarea(newChatInput); | |
| }); | |
| newSendBtn.addEventListener('click', () => { | |
| this.sendChatMessagePanel(); | |
| }); | |
| } | |
| sendChatMessagePanel() { | |
| const chatInput = document.getElementById('chatInputPanel'); | |
| const message = chatInput.value.trim(); | |
| if (!message) return; | |
| // Add user message | |
| this.addChatMessagePanel(message, 'user'); | |
| // Clear input | |
| chatInput.value = ''; | |
| this.autoResizeTextarea(chatInput); | |
| // Show typing indicator and send to backend | |
| this.showChatTypingPanel(); | |
| this.sendChatToBackend(message); | |
| } | |
| addChatMessagePanel(message, sender) { | |
| const chatPanel = document.getElementById('chatMessagesPanel'); | |
| // Remove welcome message if it exists | |
| const welcome = chatPanel.querySelector('.chat-welcome'); | |
| if (welcome && sender === 'user') { | |
| welcome.remove(); | |
| } | |
| const messageDiv = document.createElement('div'); | |
| messageDiv.className = `chat-message-panel ${sender}`; | |
| const currentTime = new Date().toLocaleTimeString([], { | |
| hour: '2-digit', | |
| minute: '2-digit' | |
| }); | |
| messageDiv.innerHTML = ` | |
| <div class="message-avatar-panel ${sender}"> | |
| <i class="fas fa-${sender === 'user' ? 'user' : 'robot'}"></i> | |
| </div> | |
| <div class="message-content-panel ${sender}"> | |
| <div class="message-bubble ${sender}"> | |
| ${sender === 'bot' ? message : this.escapeHtml(message)} | |
| </div> | |
| <div class="message-time-panel">${currentTime}</div> | |
| </div> | |
| `; | |
| chatPanel.appendChild(messageDiv); | |
| chatPanel.scrollTop = chatPanel.scrollHeight; | |
| } | |
| showChatTypingPanel() { | |
| const chatPanel = document.getElementById('chatMessagesPanel'); | |
| const typingDiv = document.createElement('div'); | |
| typingDiv.className = 'chat-message-panel bot'; | |
| typingDiv.id = 'typingIndicatorPanel'; | |
| typingDiv.innerHTML = ` | |
| <div class="message-avatar-panel bot"> | |
| <i class="fas fa-robot"></i> | |
| </div> | |
| <div class="message-content-panel bot"> | |
| <div class="message-bubble bot"> | |
| <div class="typing-dots"> | |
| <div class="dot"></div> | |
| <div class="dot"></div> | |
| <div class="dot"></div> | |
| </div> | |
| <span style="margin-left: 0.5rem; font-style: italic;">AI is thinking...</span> | |
| </div> | |
| </div> | |
| `; | |
| chatPanel.appendChild(typingDiv); | |
| chatPanel.scrollTop = chatPanel.scrollHeight; | |
| } | |
| hideChatTypingPanel() { | |
| const typingIndicator = document.getElementById('typingIndicatorPanel'); | |
| if (typingIndicator) { | |
| typingIndicator.remove(); | |
| } | |
| } | |
| sendChatToBackend(message) { | |
| fetch('/chat', { | |
| method: 'POST', | |
| headers: { | |
| 'Content-Type': 'application/json', | |
| }, | |
| body: JSON.stringify({ message: message }) | |
| }) | |
| .then(response => response.json()) | |
| .then(data => { | |
| this.hideChatTypingPanel(); | |
| if (data.success) { | |
| this.addChatMessagePanel(data.response, 'bot'); | |
| } else { | |
| this.addChatMessagePanel( | |
| `I apologize, but I encountered an error: ${data.error || 'Unknown error'}. Please try again.`, | |
| 'bot' | |
| ); | |
| } | |
| }) | |
| .catch(error => { | |
| console.error('Chat error:', error); | |
| this.hideChatTypingPanel(); | |
| this.addChatMessagePanel( | |
| 'I apologize, but I\'m having trouble connecting right now. Please check your connection and try again.', | |
| 'bot' | |
| ); | |
| }); | |
| } | |
| initializeChat() { | |
| this.chatHistory = []; | |
| } | |
| async sendChatMessage() { | |
| // Redirect to the enhanced chat functionality | |
| this.sendMessage(); | |
| } | |
| addChatMessage(message, sender) { | |
| // Redirect to the enhanced chat message functionality | |
| this.addMessageToChat(message, sender); | |
| } | |
| // Utility functions | |
| escapeHtml(text) { | |
| const div = document.createElement('div'); | |
| div.textContent = text; | |
| return div.innerHTML; | |
| } | |
| truncateText(text, maxLength) { | |
| if (text.length <= maxLength) return text; | |
| return text.substr(0, maxLength) + '...'; | |
| } | |
| // Enhanced UI Methods (Legacy - now handled by setupEnhancedChat) | |
| setupChatInput() { | |
| // This is now handled by the enhanced chat setup | |
| this.setupEnhancedChat(); | |
| } | |
| setupSearchSuggestions() { | |
| const searchInput = document.getElementById('searchInput'); | |
| const suggestions = document.getElementById('searchSuggestions'); | |
| // Initialize enhanced search features | |
| this.initializeQuickSearchCards(); | |
| this.initializeSearchTips(); | |
| this.initializeRecentSearches(); | |
| this.initializeAdvancedFilters(); | |
| if (searchInput && suggestions) { | |
| searchInput.addEventListener('input', (e) => this.handleSearchInput(e)); | |
| searchInput.addEventListener('focus', () => this.showSearchSuggestions()); | |
| searchInput.addEventListener('blur', () => this.hideSearchSuggestions()); | |
| // Handle suggestion clicks | |
| suggestions.querySelectorAll('.suggestion-item').forEach(item => { | |
| item.addEventListener('click', () => { | |
| const query = item.dataset.query; | |
| if (query) { | |
| searchInput.value = query; | |
| suggestions.classList.remove('show'); | |
| this.searchPapers(); | |
| this.addToRecentSearches(query); | |
| } | |
| }); | |
| }); | |
| } | |
| } | |
| initializeQuickSearchCards() { | |
| const quickSearchCards = document.querySelectorAll('.quick-search-card'); | |
| quickSearchCards.forEach(card => { | |
| card.addEventListener('click', () => { | |
| const query = card.dataset.query; | |
| if (query) { | |
| const searchInput = document.getElementById('searchInput'); | |
| if (searchInput) { | |
| searchInput.value = query; | |
| this.searchPapers(); | |
| this.addToRecentSearches(query); | |
| } | |
| } | |
| }); | |
| }); | |
| } | |
| initializeSearchTips() { | |
| const tipsToggle = document.querySelector('.tips-toggle'); | |
| const tipsContent = document.querySelector('.tips-content'); | |
| if (tipsToggle && tipsContent) { | |
| tipsToggle.addEventListener('click', () => { | |
| tipsContent.classList.toggle('show'); | |
| const icon = tipsToggle.querySelector('i'); | |
| if (icon) { | |
| icon.classList.toggle('fa-chevron-down'); | |
| icon.classList.toggle('fa-chevron-up'); | |
| } | |
| }); | |
| } | |
| } | |
| initializeRecentSearches() { | |
| this.loadRecentSearches(); | |
| } | |
| initializeAdvancedFilters() { | |
| // Advanced filters toggle is handled by global function | |
| } | |
| handleSearchInput(e) { | |
| const query = e.target.value.trim(); | |
| if (query.length > 2) { | |
| this.updateSearchSuggestions(query); | |
| this.showSearchSuggestions(); | |
| } else if (query.length === 0) { | |
| this.showSearchSuggestions(); | |
| } else { | |
| this.hideSearchSuggestions(); | |
| } | |
| } | |
| updateSearchSuggestions(query) { | |
| const searchSuggestions = document.getElementById('searchSuggestions'); | |
| if (!searchSuggestions) return; | |
| // Generate dynamic suggestions based on the query | |
| const suggestions = [ | |
| { text: query, count: '~1.2k papers' }, | |
| { text: query + ' applications', count: '~800 papers' }, | |
| { text: query + ' algorithms', count: '~650 papers' }, | |
| { text: query + ' recent advances', count: '~420 papers' } | |
| ]; | |
| const suggestionsSection = searchSuggestions.querySelector('.suggestions-section'); | |
| if (suggestionsSection) { | |
| const suggestionItems = suggestions.map(s => ` | |
| <div class="suggestion-item" data-query="${s.text}"> | |
| <i class="fas fa-search"></i> | |
| <span>${s.text}</span> | |
| <small>${s.count}</small> | |
| </div> | |
| `).join(''); | |
| suggestionsSection.innerHTML = ` | |
| <h4>Suggestions</h4> | |
| ${suggestionItems} | |
| `; | |
| // Re-attach event listeners | |
| suggestionsSection.querySelectorAll('.suggestion-item').forEach(item => { | |
| item.addEventListener('click', () => { | |
| const query = item.dataset.query; | |
| if (query) { | |
| const searchInput = document.getElementById('searchInput'); | |
| if (searchInput) { | |
| searchInput.value = query; | |
| this.searchPapers(); | |
| this.hideSearchSuggestions(); | |
| this.addToRecentSearches(query); | |
| } | |
| } | |
| }); | |
| }); | |
| } | |
| } | |
| showSearchSuggestions() { | |
| const searchSuggestions = document.getElementById('searchSuggestions'); | |
| if (searchSuggestions) { | |
| searchSuggestions.classList.add('show'); | |
| } | |
| } | |
| hideSearchSuggestions() { | |
| setTimeout(() => { | |
| const searchSuggestions = document.getElementById('searchSuggestions'); | |
| if (searchSuggestions) { | |
| searchSuggestions.classList.remove('show'); | |
| } | |
| }, 200); | |
| } | |
| addToRecentSearches(query) { | |
| let recentSearches = JSON.parse(localStorage.getItem('recentSearches') || '[]'); | |
| // Remove if already exists | |
| recentSearches = recentSearches.filter(search => search !== query); | |
| // Add to beginning | |
| recentSearches.unshift(query); | |
| // Keep only last 5 | |
| recentSearches = recentSearches.slice(0, 5); | |
| localStorage.setItem('recentSearches', JSON.stringify(recentSearches)); | |
| this.updateRecentSearchesDisplay(); | |
| } | |
| loadRecentSearches() { | |
| const recentSearches = JSON.parse(localStorage.getItem('recentSearches') || '[]'); | |
| if (recentSearches.length > 0) { | |
| this.updateRecentSearchesDisplay(); | |
| } | |
| } | |
| updateRecentSearchesDisplay() { | |
| const recentSearches = JSON.parse(localStorage.getItem('recentSearches') || '[]'); | |
| const recentSearchesContainer = document.getElementById('recentSearches'); | |
| const recentSearchItems = document.getElementById('recentSearchItems'); | |
| if (recentSearches.length > 0 && recentSearchesContainer && recentSearchItems) { | |
| const recentHTML = recentSearches.map(search => ` | |
| <div class="suggestion-item" data-query="${search}"> | |
| <i class="fas fa-history"></i> | |
| <span>${search}</span> | |
| </div> | |
| `).join(''); | |
| recentSearchItems.innerHTML = recentHTML; | |
| recentSearchesContainer.style.display = 'block'; | |
| // Attach event listeners | |
| recentSearchItems.querySelectorAll('.suggestion-item').forEach(item => { | |
| item.addEventListener('click', () => { | |
| const query = item.dataset.query; | |
| if (query) { | |
| const searchInput = document.getElementById('searchInput'); | |
| if (searchInput) { | |
| searchInput.value = query; | |
| this.searchPapers(); | |
| } | |
| } | |
| }); | |
| }); | |
| } | |
| } | |
| updateStatusIndicator() { | |
| const indicator = document.getElementById('statusIndicator'); | |
| const chatStatus = document.getElementById('chatStatus'); | |
| if (indicator) { | |
| const statusText = indicator.querySelector('.status-text'); | |
| const statusDot = indicator.querySelector('.status-dot'); | |
| if (this.currentDocumentId) { | |
| statusText.textContent = 'Document Active'; | |
| statusDot.style.background = 'var(--success-color)'; | |
| } else { | |
| statusText.textContent = 'Ready'; | |
| statusDot.style.background = 'var(--warning-color)'; | |
| } | |
| } | |
| if (chatStatus) { | |
| const statusIndicator = chatStatus.querySelector('.status-indicator'); | |
| if (this.currentDocumentId) { | |
| statusIndicator.classList.remove('offline'); | |
| statusIndicator.classList.add('online'); | |
| statusIndicator.querySelector('span').textContent = 'Document loaded'; | |
| } else { | |
| statusIndicator.classList.remove('online'); | |
| statusIndicator.classList.add('offline'); | |
| statusIndicator.querySelector('span').textContent = 'No document selected'; | |
| } | |
| } | |
| } | |
| updateUploadProgress(percentage, step) { | |
| const progressContainer = document.getElementById('uploadProgress'); | |
| const progressFill = document.getElementById('progressFill'); | |
| const progressPercentage = document.getElementById('progressPercentage'); | |
| const progressSubtitle = document.getElementById('progressSubtitle'); | |
| const progressTime = document.getElementById('progressTime'); | |
| if (progressContainer && progressFill && progressPercentage) { | |
| progressContainer.style.display = 'block'; | |
| progressFill.style.width = `${percentage}%`; | |
| progressPercentage.textContent = `${percentage}%`; | |
| // Update subtitle and time estimate | |
| const subtitles = [ | |
| 'Preparing your document for analysis...', | |
| 'Uploading your document securely...', | |
| 'Extracting text and content...', | |
| 'AI analyzing document structure...', | |
| 'Analysis complete! Ready for questions.' | |
| ]; | |
| if (progressSubtitle && step <= subtitles.length) { | |
| progressSubtitle.textContent = subtitles[step - 1] || subtitles[0]; | |
| } | |
| if (progressTime) { | |
| if (percentage < 100) { | |
| const remainingTime = Math.max(1, Math.round((100 - percentage) / 10)); | |
| progressTime.textContent = `~${remainingTime}s remaining`; | |
| } else { | |
| progressTime.textContent = 'Complete!'; | |
| } | |
| } | |
| // Update steps with enhanced system | |
| const steps = progressContainer.querySelectorAll('.progress-step'); | |
| steps.forEach((stepEl, index) => { | |
| const stepNumber = parseInt(stepEl.dataset.step); | |
| if (stepNumber <= step) { | |
| stepEl.classList.add('active'); | |
| } else { | |
| stepEl.classList.remove('active'); | |
| } | |
| }); | |
| if (percentage === 100) { | |
| setTimeout(() => { | |
| progressContainer.style.display = 'none'; | |
| }, 3000); | |
| } | |
| } | |
| } | |
| autoResizeTextarea(event) { | |
| const textarea = event.target; | |
| textarea.style.height = 'auto'; | |
| const newHeight = Math.min(textarea.scrollHeight, 120); | |
| textarea.style.height = newHeight + 'px'; | |
| } | |
| clearChat() { | |
| const chatMessages = document.getElementById('chatMessages'); | |
| if (chatMessages) { | |
| // Keep the welcome message | |
| const welcomeMessage = chatMessages.querySelector('.welcome-message'); | |
| chatMessages.innerHTML = ''; | |
| if (welcomeMessage) { | |
| chatMessages.appendChild(welcomeMessage); | |
| } | |
| } | |
| this.chatHistory = []; | |
| this.showToast('Chat cleared', 'success'); | |
| } | |
| setActiveDocument(docId) { | |
| this.currentDocumentId = docId; | |
| this.updateStatusIndicator(); | |
| // Show chat indicator | |
| const chatIndicator = document.getElementById('chatIndicator'); | |
| if (chatIndicator) { | |
| chatIndicator.classList.add('active'); | |
| } | |
| } | |
| // Override the original methods to include enhanced functionality | |
| async processFile(file) { | |
| if (!file || !this.allowed_file(file.name)) { | |
| this.showToast('Please select a valid file (PDF, TXT, or DOCX)', 'error'); | |
| return; | |
| } | |
| const formData = new FormData(); | |
| formData.append('file', file); | |
| this.showLoading(true); | |
| this.updateUploadProgress(25, 1); | |
| try { | |
| this.updateUploadProgress(50, 2); | |
| const response = await fetch('/upload', { | |
| method: 'POST', | |
| body: formData | |
| }); | |
| this.updateUploadProgress(75, 3); | |
| const data = await response.json(); | |
| if (response.ok) { | |
| this.updateUploadProgress(100, 4); | |
| this.displayUploadResult(data); | |
| this.setActiveDocument(data.doc_id); | |
| this.showToast('File uploaded and analyzed successfully!', 'success'); | |
| } else { | |
| throw new Error(data.error || 'Upload failed'); | |
| } | |
| } catch (error) { | |
| console.error('Upload error:', error); | |
| this.showToast(error.message, 'error'); | |
| this.updateUploadProgress(0, 0); | |
| } finally { | |
| this.showLoading(false); | |
| } | |
| } | |
| allowed_file(filename) { | |
| const allowedExtensions = ['pdf', 'txt', 'docx']; | |
| const extension = filename.split('.').pop().toLowerCase(); | |
| return allowedExtensions.includes(extension); | |
| } | |
| // Page Navigation Methods | |
| setupPageNavigation() { | |
| // Handle smooth scrolling for landing page links | |
| document.querySelectorAll('a[href^="#"]').forEach(anchor => { | |
| anchor.addEventListener('click', function (e) { | |
| e.preventDefault(); | |
| const target = document.querySelector(this.getAttribute('href')); | |
| if (target && target.closest('#landingPage')) { | |
| target.scrollIntoView({ | |
| behavior: 'smooth', | |
| block: 'start' | |
| }); | |
| } | |
| }); | |
| }); | |
| } | |
| setupMobileNav() { | |
| // Toggle landing nav links | |
| document.querySelectorAll('.navbar .mobile-nav-toggle').forEach(toggle => { | |
| toggle.addEventListener('click', () => { | |
| const container = toggle.closest('.navbar').querySelector('.landing-nav-links, .nav-links'); | |
| if (container) { | |
| container.classList.toggle('show'); | |
| } | |
| }); | |
| }); | |
| // Close menu on link click (mobile) | |
| document.querySelectorAll('.landing-nav-links .nav-link').forEach(link => { | |
| link.addEventListener('click', () => { | |
| const links = document.querySelector('.landing-nav-links'); | |
| links && links.classList.remove('show'); | |
| }); | |
| }); | |
| } | |
| setupVhUnit() { | |
| const setVh = () => { | |
| const vh = window.innerHeight * 0.01; | |
| document.documentElement.style.setProperty('--vh', `${vh}px`); | |
| }; | |
| setVh(); | |
| window.addEventListener('resize', setVh); | |
| window.addEventListener('orientationchange', setVh); | |
| } | |
| // Removed split-mode; tabs-only behavior | |
| setupViewToggle() { | |
| // Strict tabs: only one panel visible | |
| document.querySelectorAll('.tab-btn').forEach(btn => { | |
| btn.classList.remove('active'); | |
| }); | |
| document.querySelectorAll('.tab-content').forEach(content => { | |
| content.classList.remove('active'); | |
| }); | |
| document.querySelector(`[data-tab="summary"]`)?.classList.add('active'); | |
| document.getElementById('summary-tab')?.classList.add('active'); | |
| history.replaceState(null, null, `#summary`); | |
| const tabDisplayName = 'Summary'; | |
| showToast(`Focused ${tabDisplayName}`, 'info'); | |
| } | |
| navigateToApp(section = 'search') { | |
| // Hide landing page | |
| const landingPage = document.getElementById('landingPage'); | |
| const appPage = document.getElementById('appPage'); | |
| if (landingPage && appPage) { | |
| landingPage.classList.remove('active'); | |
| appPage.classList.add('active'); | |
| this.currentPage = 'app'; | |
| // Switch to the specified section | |
| this.switchSection(section); | |
| // Show toast message | |
| this.showToast(`Welcome to Research Radar! ${section === 'search' ? 'Start searching for papers' : 'Upload your documents'}`, 'success'); | |
| // Focus on the relevant input | |
| setTimeout(() => { | |
| if (section === 'search') { | |
| const searchInput = document.getElementById('searchInput'); | |
| if (searchInput) searchInput.focus(); | |
| } else if (section === 'upload') { | |
| // Auto-focus on upload section | |
| document.getElementById('upload').scrollIntoView({ behavior: 'smooth' }); | |
| } | |
| }, 500); | |
| } | |
| } | |
| navigateToLanding() { | |
| const landingPage = document.getElementById('landingPage'); | |
| const appPage = document.getElementById('appPage'); | |
| if (landingPage && appPage) { | |
| appPage.classList.remove('active'); | |
| landingPage.classList.add('active'); | |
| this.currentPage = 'landing'; | |
| // Scroll to top | |
| window.scrollTo({ top: 0, behavior: 'smooth' }); | |
| this.showToast('Welcome back to the homepage!', 'info'); | |
| } | |
| } | |
| // Enhanced section switching for app page | |
| switchSection(sectionName) { | |
| if (this.currentPage !== 'app') return; | |
| // Update navigation | |
| document.querySelectorAll('.app-nav .nav-link').forEach(link => { | |
| link.classList.remove('active'); | |
| }); | |
| const activeLink = document.querySelector(`.app-nav [data-section="${sectionName}"]`); | |
| if (activeLink) { | |
| activeLink.classList.add('active'); | |
| } | |
| // Update sections | |
| document.querySelectorAll('#appPage .section').forEach(section => { | |
| section.classList.remove('active'); | |
| }); | |
| const targetSection = document.getElementById(sectionName); | |
| if (targetSection) { | |
| targetSection.classList.add('active'); | |
| } | |
| this.currentSection = sectionName; | |
| // Add section-specific functionality | |
| this.onSectionChange(sectionName); | |
| } | |
| onSectionChange(sectionName) { | |
| switch(sectionName) { | |
| case 'search': | |
| // Focus search input after animation | |
| setTimeout(() => { | |
| const searchInput = document.getElementById('searchInput'); | |
| if (searchInput) searchInput.focus(); | |
| }, 300); | |
| break; | |
| case 'upload': | |
| // Reset upload progress | |
| this.updateUploadProgress(0, 0); | |
| break; | |
| case 'mypapers': | |
| // Load papers when section is accessed | |
| this.loadMyPapers(); | |
| break; | |
| case 'chat': | |
| // Focus chat input | |
| setTimeout(() => { | |
| const chatInput = document.getElementById('chatInput'); | |
| if (chatInput) chatInput.focus(); | |
| }, 300); | |
| break; | |
| } | |
| } | |
| // Enhanced Chat Functionality | |
| setupEnhancedChat() { | |
| this.messageCount = 0; | |
| this.sessionStartTime = Date.now(); | |
| this.updateChatStats(); | |
| this.setupChatSuggestions(); | |
| this.setupChatInput(); | |
| this.setupQuickActions(); | |
| this.startSessionTimer(); | |
| } | |
| setupChatSuggestions() { | |
| // Handle suggestion chips | |
| document.querySelectorAll('.suggestion-chip-enhanced').forEach(chip => { | |
| chip.addEventListener('click', (e) => { | |
| const question = e.currentTarget.dataset.question; | |
| if (question) { | |
| const chatInput = document.getElementById('chatInput'); | |
| chatInput.value = question; | |
| chatInput.focus(); | |
| this.autoResizeTextarea(chatInput); | |
| } | |
| }); | |
| }); | |
| } | |
| setupChatInput() { | |
| const chatInput = document.getElementById('chatInput'); | |
| const sendBtn = document.getElementById('chatSendBtn'); | |
| const clearBtn = document.getElementById('chatClearBtn'); | |
| // Auto-resize textarea | |
| chatInput.addEventListener('input', () => { | |
| this.autoResizeTextarea(chatInput); | |
| }); | |
| // Handle Enter key | |
| chatInput.addEventListener('keydown', (e) => { | |
| if (e.key === 'Enter' && !e.shiftKey) { | |
| e.preventDefault(); | |
| this.sendMessage(); | |
| } else if (e.key === 'Enter' && e.shiftKey) { | |
| // Allow new line | |
| } | |
| }); | |
| // Send button | |
| sendBtn.addEventListener('click', () => { | |
| this.sendMessage(); | |
| }); | |
| // Clear chat | |
| clearBtn.addEventListener('click', () => { | |
| this.clearChat(); | |
| }); | |
| // Attach button (placeholder) | |
| document.getElementById('attachBtn')?.addEventListener('click', () => { | |
| this.showNotification('File attachment coming soon!', 'info'); | |
| }); | |
| // Emoji button (placeholder) | |
| document.getElementById('emojiBtn')?.addEventListener('click', () => { | |
| this.showNotification('Emoji picker coming soon!', 'info'); | |
| }); | |
| } | |
| setupQuickActions() { | |
| // Quick action buttons in status card | |
| document.querySelectorAll('.quick-action-btn').forEach(btn => { | |
| btn.addEventListener('click', (e) => { | |
| e.stopPropagation(); | |
| }); | |
| }); | |
| } | |
| autoResizeTextarea(textarea) { | |
| textarea.style.height = 'auto'; | |
| textarea.style.height = Math.min(textarea.scrollHeight, 120) + 'px'; | |
| } | |
| sendMessage() { | |
| const chatInput = document.getElementById('chatInput'); | |
| const message = chatInput.value.trim(); | |
| if (!message) return; | |
| // Add user message to chat | |
| this.addMessageToChat(message, 'user'); | |
| // Clear input | |
| chatInput.value = ''; | |
| this.autoResizeTextarea(chatInput); | |
| // Show typing indicator | |
| this.showTypingIndicator(); | |
| // Update stats | |
| this.messageCount++; | |
| this.updateChatStats(); | |
| // Send to backend | |
| this.sendChatMessage(message); | |
| } | |
| addMessageToChat(message, sender = 'user') { | |
| const chatMessages = document.getElementById('chatMessages'); | |
| const messageDiv = document.createElement('div'); | |
| const currentTime = new Date().toLocaleTimeString([], { | |
| hour: '2-digit', | |
| minute: '2-digit' | |
| }); | |
| if (sender === 'user') { | |
| messageDiv.className = 'chat-message user-message'; | |
| messageDiv.innerHTML = ` | |
| <div class="message-avatar-enhanced"> | |
| <div class="avatar-icon user-avatar"> | |
| <i class="fas fa-user"></i> | |
| </div> | |
| </div> | |
| <div class="message-content-enhanced"> | |
| <div class="message-header"> | |
| <span class="message-sender">You</span> | |
| <span class="message-time">${currentTime}</span> | |
| </div> | |
| <div class="message-text"> | |
| <p>${this.escapeHtml(message)}</p> | |
| </div> | |
| </div> | |
| `; | |
| } else { | |
| messageDiv.className = 'chat-message bot-message'; | |
| messageDiv.innerHTML = ` | |
| <div class="message-avatar-enhanced"> | |
| <div class="avatar-icon"> | |
| <i class="fas fa-robot"></i> | |
| </div> | |
| <div class="avatar-status online"></div> | |
| </div> | |
| <div class="message-content-enhanced"> | |
| <div class="message-header"> | |
| <span class="message-sender">Research Assistant</span> | |
| <span class="message-time">${currentTime}</span> | |
| <span class="message-badge ai">AI</span> | |
| </div> | |
| <div class="message-text"> | |
| <div class="message-content">${message}</div> | |
| </div> | |
| </div> | |
| `; | |
| } | |
| // Remove welcome message if it's the first user message | |
| const welcomeMessage = chatMessages.querySelector('.welcome-message-enhanced'); | |
| if (welcomeMessage && sender === 'user') { | |
| welcomeMessage.style.animation = 'fadeOut 0.3s ease-out forwards'; | |
| setTimeout(() => { | |
| welcomeMessage.remove(); | |
| }, 300); | |
| } | |
| chatMessages.appendChild(messageDiv); | |
| // Scroll to bottom | |
| const container = document.querySelector('.chat-messages-container'); | |
| container.scrollTop = container.scrollHeight; | |
| // Update chat status | |
| this.updateChatStatus('active'); | |
| } | |
| showTypingIndicator() { | |
| const loadingIndicator = document.getElementById('chatLoading'); | |
| loadingIndicator.style.display = 'flex'; | |
| // Scroll to show typing indicator | |
| const container = document.querySelector('.chat-messages-container'); | |
| setTimeout(() => { | |
| container.scrollTop = container.scrollHeight; | |
| }, 100); | |
| } | |
| hideTypingIndicator() { | |
| const loadingIndicator = document.getElementById('chatLoading'); | |
| loadingIndicator.style.display = 'none'; | |
| } | |
| clearChat() { | |
| if (confirm('Are you sure you want to clear the chat history?')) { | |
| const chatMessages = document.getElementById('chatMessages'); | |
| chatMessages.innerHTML = ` | |
| <div class="chat-message bot-message welcome-message-enhanced"> | |
| <div class="message-avatar-enhanced"> | |
| <div class="avatar-icon"> | |
| <i class="fas fa-robot"></i> | |
| </div> | |
| <div class="avatar-status online"></div> | |
| </div> | |
| <div class="message-content-enhanced"> | |
| <div class="message-header"> | |
| <span class="message-sender">Research Assistant</span> | |
| <span class="message-time">Just now</span> | |
| <span class="message-badge ai">AI</span> | |
| </div> | |
| <div class="message-text"> | |
| <div class="welcome-intro"> | |
| <h3>👋 Welcome back to Research Radar!</h3> | |
| <p>Chat cleared. I'm ready to help you with your research questions.</p> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| `; | |
| this.messageCount = 0; | |
| this.updateChatStats(); | |
| this.updateChatStatus('ready'); | |
| this.showNotification('Chat cleared successfully', 'success'); | |
| } | |
| } | |
| updateChatStats() { | |
| const messageCountEl = document.getElementById('messageCount'); | |
| const sessionTimeEl = document.getElementById('sessionTime'); | |
| if (messageCountEl) { | |
| messageCountEl.textContent = this.messageCount; | |
| } | |
| } | |
| startSessionTimer() { | |
| setInterval(() => { | |
| const sessionTimeEl = document.getElementById('sessionTime'); | |
| if (sessionTimeEl) { | |
| const elapsed = Math.floor((Date.now() - this.sessionStartTime) / 1000); | |
| const minutes = Math.floor(elapsed / 60); | |
| const seconds = elapsed % 60; | |
| sessionTimeEl.textContent = `${minutes}:${seconds.toString().padStart(2, '0')}`; | |
| } | |
| }, 1000); | |
| } | |
| updateChatStatus(status) { | |
| const statusTitle = document.getElementById('statusTitle'); | |
| const statusDescription = document.getElementById('statusDescription'); | |
| switch (status) { | |
| case 'ready': | |
| statusTitle.textContent = 'Ready to Chat'; | |
| statusDescription.textContent = 'Upload a document or search for papers to get started'; | |
| break; | |
| case 'active': | |
| statusTitle.textContent = 'Chat Active'; | |
| statusDescription.textContent = 'AI assistant is ready to answer your questions'; | |
| break; | |
| case 'processing': | |
| statusTitle.textContent = 'Processing...'; | |
| statusDescription.textContent = 'AI is analyzing your question'; | |
| break; | |
| } | |
| } | |
| sendChatMessage(message) { | |
| this.updateChatStatus('processing'); | |
| fetch('/chat', { | |
| method: 'POST', | |
| headers: { | |
| 'Content-Type': 'application/json', | |
| }, | |
| body: JSON.stringify({ message: message }) | |
| }) | |
| .then(response => response.json()) | |
| .then(data => { | |
| this.hideTypingIndicator(); | |
| if (data.success) { | |
| this.addMessageToChat(data.response, 'bot'); | |
| this.messageCount++; | |
| this.updateChatStats(); | |
| } else { | |
| this.addMessageToChat( | |
| `I apologize, but I encountered an error: ${data.error || 'Unknown error'}. Please try again.`, | |
| 'bot' | |
| ); | |
| } | |
| this.updateChatStatus('active'); | |
| }) | |
| .catch(error => { | |
| console.error('Chat error:', error); | |
| this.hideTypingIndicator(); | |
| this.addMessageToChat( | |
| 'I apologize, but I\'m having trouble connecting right now. Please check your connection and try again.', | |
| 'bot' | |
| ); | |
| this.updateChatStatus('ready'); | |
| }); | |
| } | |
| escapeHtml(text) { | |
| const div = document.createElement('div'); | |
| div.textContent = text; | |
| return div.innerHTML; | |
| } | |
| // Global functions for HTML onclick handlers | |
| toggleSuggestions() { | |
| const content = document.getElementById('suggestionsContent'); | |
| const toggle = document.querySelector('.suggestions-toggle i'); | |
| if (content.classList.contains('collapsed')) { | |
| content.classList.remove('collapsed'); | |
| toggle.className = 'fas fa-chevron-up'; | |
| } else { | |
| content.classList.add('collapsed'); | |
| toggle.className = 'fas fa-chevron-down'; | |
| } | |
| } | |
| showQuickActions() { | |
| const quickActions = document.getElementById('chatQuickActions'); | |
| quickActions.style.display = 'block'; | |
| quickActions.style.animation = 'slideUp 0.3s ease-out'; | |
| } | |
| hideQuickActions() { | |
| const quickActions = document.getElementById('chatQuickActions'); | |
| quickActions.style.animation = 'slideDown 0.3s ease-out'; | |
| setTimeout(() => { | |
| quickActions.style.display = 'none'; | |
| }, 300); | |
| } | |
| generateSummary() { | |
| this.addMessageToChat('Please generate a summary of the current document.', 'user'); | |
| this.sendChatMessage('Please generate a summary of the current document.'); | |
| this.hideQuickActions(); | |
| } | |
| extractKeyPoints() { | |
| this.addMessageToChat('What are the key points from this paper?', 'user'); | |
| this.sendChatMessage('What are the key points from this paper?'); | |
| this.hideQuickActions(); | |
| } | |
| findRelatedPapers() { | |
| this.addMessageToChat('Can you suggest related papers to this research?', 'user'); | |
| this.sendChatMessage('Can you suggest related papers to this research?'); | |
| this.hideQuickActions(); | |
| } | |
| exportChat() { | |
| // Get all messages | |
| const messages = document.querySelectorAll('.chat-message'); | |
| let chatText = 'Research Radar Chat Export\n'; | |
| chatText += '='.repeat(50) + '\n\n'; | |
| messages.forEach(message => { | |
| const sender = message.querySelector('.message-sender')?.textContent || 'Unknown'; | |
| const time = message.querySelector('.message-time')?.textContent || ''; | |
| const text = message.querySelector('.message-text')?.textContent || ''; | |
| if (text.trim()) { | |
| chatText += `[${time}] ${sender}:\n${text.trim()}\n\n`; | |
| } | |
| }); | |
| // Create and download file | |
| const blob = new Blob([chatText], { type: 'text/plain' }); | |
| const url = URL.createObjectURL(blob); | |
| const a = document.createElement('a'); | |
| a.href = url; | |
| a.download = `research-radar-chat-${new Date().toISOString().split('T')[0]}.txt`; | |
| document.body.appendChild(a); | |
| a.click(); | |
| document.body.removeChild(a); | |
| URL.revokeObjectURL(url); | |
| this.showNotification('Chat exported successfully!', 'success'); | |
| this.hideQuickActions(); | |
| } | |
| // Summary + Chat Functionality | |
| openSummaryChat(paperData, summaryText) { | |
| console.log('openSummaryChat called with:', { paperData, summaryText }); | |
| // Hide current section and show summary-chat | |
| document.querySelectorAll('.section').forEach(section => { | |
| section.style.display = 'none'; | |
| }); | |
| const summarySection = document.getElementById('summary-chat'); | |
| if (!summarySection) { | |
| console.error('summary-chat section not found!'); | |
| this.showToast('Error: Summary section not found', 'error'); | |
| return; | |
| } | |
| summarySection.style.display = 'block'; | |
| // Update paper information | |
| document.getElementById('paperTitle').textContent = paperData.title || 'Research Paper'; | |
| document.getElementById('paperAuthor').textContent = paperData.authors ? paperData.authors.join(', ') : 'Unknown Author'; | |
| document.getElementById('paperDate').textContent = paperData.published || new Date().getFullYear(); | |
| document.getElementById('paperCategory').textContent = paperData.category || 'Research'; | |
| // Show summary | |
| this.displaySummaryInPanel(summaryText); | |
| // Setup chat panel | |
| this.setupChatPanel(); | |
| // Store current paper data for chat context | |
| this.currentPaper = paperData; | |
| // Default to Chat tab for immediate Q&A after tabs are in DOM | |
| setTimeout(() => { | |
| try { switchTab('chat'); } catch (_) {} | |
| const chatInput = document.getElementById('chatInputPanel'); | |
| if (chatInput) chatInput.focus(); | |
| }, 150); | |
| } | |
| displaySummaryInPanel(summaryText) { | |
| const summaryLoading = document.getElementById('summaryLoading'); | |
| const summaryTextEl = document.getElementById('summaryText'); | |
| // Hide loading and show summary | |
| summaryLoading.style.display = 'none'; | |
| summaryTextEl.style.display = 'block'; | |
| summaryTextEl.innerHTML = this.formatSummaryText(summaryText); | |
| // Update stats | |
| this.updateSummaryStats(summaryText); | |
| } | |
| formatSummaryText(text) { | |
| // Convert plain text to formatted HTML | |
| return text | |
| .replace(/\n\n/g, '</p><p>') | |
| .replace(/\n/g, '<br>') | |
| .replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>') | |
| .replace(/\*(.*?)\*/g, '<em>$1</em>') | |
| .replace(/^/, '<p>') | |
| .replace(/$/, '</p>'); | |
| } | |
| updateSummaryStats(text) { | |
| const wordCount = text.split(/\s+/).length; | |
| const readingTime = Math.ceil(wordCount / 200); // Average reading speed | |
| const compressionRatio = Math.round((1 - (text.length / (text.length * 3))) * 100); // Estimate | |
| const wc = document.getElementById('wordCount'); | |
| const rt = document.getElementById('readingTime'); | |
| const cr = document.getElementById('compressionRatio'); | |
| if (wc) wc.textContent = wordCount.toLocaleString(); | |
| if (rt) rt.textContent = `${readingTime} min`; | |
| if (cr) cr.textContent = `${compressionRatio}%`; | |
| } | |
| setupChatPanel() { | |
| const chatInput = document.getElementById('chatInputPanel'); | |
| const sendBtn = document.getElementById('chatSendBtnPanel'); | |
| if (!chatInput || !sendBtn) return; | |
| // Clear any existing event listeners by cloning | |
| const newChatInput = chatInput.cloneNode(true); | |
| chatInput.parentNode.replaceChild(newChatInput, chatInput); | |
| const newSendBtn = sendBtn.cloneNode(true); | |
| sendBtn.parentNode.replaceChild(newSendBtn, sendBtn); | |
| // Add new event listeners | |
| newChatInput.addEventListener('keydown', (e) => { | |
| if (e.key === 'Enter' && !e.shiftKey) { | |
| e.preventDefault(); | |
| this.sendChatMessagePanel(); | |
| } | |
| }); | |
| newChatInput.addEventListener('input', () => { | |
| this.autoResizeTextarea(newChatInput); | |
| }); | |
| newSendBtn.addEventListener('click', () => { | |
| this.sendChatMessagePanel(); | |
| }); | |
| } | |
| sendChatMessagePanel() { | |
| const chatInput = document.getElementById('chatInputPanel'); | |
| const message = chatInput.value.trim(); | |
| if (!message) return; | |
| // Add user message | |
| this.addChatMessagePanel(message, 'user'); | |
| // Clear input | |
| chatInput.value = ''; | |
| this.autoResizeTextarea(chatInput); | |
| // Show typing indicator and send to backend | |
| this.showChatTypingPanel(); | |
| this.sendChatToBackend(message); | |
| } | |
| addChatMessagePanel(message, sender) { | |
| const chatPanel = document.getElementById('chatMessagesPanel'); | |
| // Remove welcome message if it exists | |
| const welcome = chatPanel.querySelector('.chat-welcome'); | |
| if (welcome && sender === 'user') { | |
| welcome.remove(); | |
| } | |
| const messageDiv = document.createElement('div'); | |
| messageDiv.className = `chat-message-panel ${sender}`; | |
| const currentTime = new Date().toLocaleTimeString([], { | |
| hour: '2-digit', | |
| minute: '2-digit' | |
| }); | |
| messageDiv.innerHTML = ` | |
| <div class="message-avatar-panel ${sender}"> | |
| <i class="fas fa-${sender === 'user' ? 'user' : 'robot'}"></i> | |
| </div> | |
| <div class="message-content-panel ${sender}"> | |
| <div class="message-bubble ${sender}"> | |
| ${sender === 'bot' ? message : this.escapeHtml(message)} | |
| </div> | |
| <div class="message-time-panel">${currentTime}</div> | |
| </div> | |
| `; | |
| chatPanel.appendChild(messageDiv); | |
| chatPanel.scrollTop = chatPanel.scrollHeight; | |
| } | |
| showChatTypingPanel() { | |
| const chatPanel = document.getElementById('chatMessagesPanel'); | |
| const typingDiv = document.createElement('div'); | |
| typingDiv.className = 'chat-message-panel bot'; | |
| typingDiv.id = 'typingIndicatorPanel'; | |
| typingDiv.innerHTML = ` | |
| <div class="message-avatar-panel bot"> | |
| <i class="fas fa-robot"></i> | |
| </div> | |
| <div class="message-content-panel bot"> | |
| <div class="message-bubble bot"> | |
| <div class="typing-dots"> | |
| <div class="dot"></div> | |
| <div class="dot"></div> | |
| <div class="dot"></div> | |
| </div> | |
| <span style="margin-left: 0.5rem; font-style: italic;">AI is thinking...</span> | |
| </div> | |
| </div> | |
| `; | |
| chatPanel.appendChild(typingDiv); | |
| chatPanel.scrollTop = chatPanel.scrollHeight; | |
| } | |
| hideChatTypingPanel() { | |
| const typingIndicator = document.getElementById('typingIndicatorPanel'); | |
| if (typingIndicator) { | |
| typingIndicator.remove(); | |
| } | |
| } | |
| sendChatToBackend(message) { | |
| fetch('/chat', { | |
| method: 'POST', | |
| headers: { | |
| 'Content-Type': 'application/json', | |
| }, | |
| body: JSON.stringify({ message: message }) | |
| }) | |
| .then(response => response.json()) | |
| .then(data => { | |
| this.hideChatTypingPanel(); | |
| if (data.success) { | |
| this.addChatMessagePanel(data.response, 'bot'); | |
| } else { | |
| this.addChatMessagePanel( | |
| `I apologize, but I encountered an error: ${data.error || 'Unknown error'}. Please try again.`, | |
| 'bot' | |
| ); | |
| } | |
| }) | |
| .catch(error => { | |
| console.error('Chat error:', error); | |
| this.hideChatTypingPanel(); | |
| this.addChatMessagePanel( | |
| 'I apologize, but I\'m having trouble connecting right now. Please check your connection and try again.', | |
| 'bot' | |
| ); | |
| }); | |
| } | |
| // My Papers functionality | |
| async loadMyPapers() { | |
| const loadingEl = document.getElementById('mypapersLoading'); | |
| const emptyEl = document.getElementById('mypapersEmpty'); | |
| const gridEl = document.getElementById('papersGrid'); | |
| if (loadingEl) loadingEl.style.display = 'block'; | |
| if (emptyEl) emptyEl.style.display = 'none'; | |
| if (gridEl) gridEl.innerHTML = ''; | |
| try { | |
| const response = await fetch('/documents'); | |
| const data = await response.json(); | |
| if (response.ok && data.success) { | |
| this.displayMyPapers(data.documents); | |
| } else { | |
| throw new Error(data.error || 'Failed to load papers'); | |
| } | |
| } catch (error) { | |
| console.error('Error loading papers:', error); | |
| this.showToast('Failed to load papers', 'error'); | |
| if (emptyEl) emptyEl.style.display = 'block'; | |
| } finally { | |
| if (loadingEl) loadingEl.style.display = 'none'; | |
| } | |
| } | |
| displayMyPapers(documents) { | |
| const emptyEl = document.getElementById('mypapersEmpty'); | |
| const gridEl = document.getElementById('papersGrid'); | |
| if (!documents || documents.length === 0) { | |
| if (emptyEl) emptyEl.style.display = 'block'; | |
| if (gridEl) gridEl.innerHTML = ''; | |
| return; | |
| } | |
| if (emptyEl) emptyEl.style.display = 'none'; | |
| if (!gridEl) return; | |
| gridEl.innerHTML = documents.map(doc => this.createPaperCard(doc)).join(''); | |
| // Add event listeners to the buttons after creating them | |
| this.setupMyPapersButtons(); | |
| } | |
| setupMyPapersButtons() { | |
| console.log('Setting up My Papers buttons...'); | |
| // Add event listeners to Open buttons | |
| const openButtons = document.querySelectorAll('.paper-action-btn.primary'); | |
| console.log(`Found ${openButtons.length} Open buttons`); | |
| openButtons.forEach(button => { | |
| button.addEventListener('click', (e) => { | |
| e.preventDefault(); | |
| const docId = button.getAttribute('data-doc-id'); | |
| console.log('Open button clicked for docId:', docId); | |
| this.openPaperFromMyPapers(docId); | |
| }); | |
| }); | |
| // Add event listeners to Delete buttons | |
| const deleteButtons = document.querySelectorAll('.paper-action-btn.secondary'); | |
| console.log(`Found ${deleteButtons.length} Delete buttons`); | |
| deleteButtons.forEach(button => { | |
| button.addEventListener('click', (e) => { | |
| e.preventDefault(); | |
| const docId = button.getAttribute('data-doc-id'); | |
| console.log('Delete button clicked for docId:', docId); | |
| this.deletePaper(docId); | |
| }); | |
| }); | |
| } | |
| createPaperCard(doc) { | |
| const icon = this.getDocumentIcon(doc.type); | |
| const authors = Array.isArray(doc.authors) ? doc.authors.join(', ') : doc.authors || 'Unknown'; | |
| const date = doc.upload_date || doc.published || 'Unknown Date'; | |
| return ` | |
| <div class="paper-card-mypapers" data-doc-id="${doc.document_id}"> | |
| <div class="paper-card-header"> | |
| <div class="paper-card-icon"> | |
| <i class="${icon}"></i> | |
| </div> | |
| <div class="paper-card-content"> | |
| <div class="paper-card-title">${this.escapeHtml(doc.title)}</div> | |
| <div class="paper-card-meta"> | |
| <span><i class="fas fa-users"></i> ${this.escapeHtml(authors)}</span> | |
| <span><i class="fas fa-calendar"></i> ${this.escapeHtml(date)}</span> | |
| ${doc.word_count ? `<span><i class="fas fa-file-text"></i> ${doc.word_count.toLocaleString()} words</span>` : ''} | |
| </div> | |
| </div> | |
| </div> | |
| <div class="paper-card-actions"> | |
| <button class="paper-action-btn primary" data-doc-id="${doc.document_id}"> | |
| <i class="fas fa-eye"></i> | |
| <span>Open</span> | |
| </button> | |
| <button class="paper-action-btn secondary" data-doc-id="${doc.document_id}"> | |
| <i class="fas fa-trash"></i> | |
| <span>Delete</span> | |
| </button> | |
| </div> | |
| </div> | |
| `; | |
| } | |
| getDocumentIcon(type) { | |
| switch (type) { | |
| case 'arxiv_paper': return 'fas fa-graduation-cap'; | |
| case 'uploaded_document': return 'fas fa-file-upload'; | |
| default: return 'fas fa-file-alt'; | |
| } | |
| } | |
| async openPaperFromMyPapers(docId) { | |
| console.log('ResearchRadar.openPaperFromMyPapers called with docId:', docId); | |
| try { | |
| // Set the document as active for chat | |
| this.currentDocumentId = docId; | |
| // Fetch document summary | |
| console.log('Fetching document summary...'); | |
| const response = await fetch(`/documents/${docId}/summary`); | |
| const data = await response.json(); | |
| console.log('Summary response:', data); | |
| if (response.ok && data.success) { | |
| // Activate the document for chat | |
| await fetch(`/documents/${docId}/activate`, { | |
| method: 'POST', | |
| headers: { 'Content-Type': 'application/json' } | |
| }); | |
| // Redirect to summary and chat page | |
| console.log('Redirecting to summary and chat...'); | |
| this.openSummaryChat(data.document, data.summary); | |
| this.showToast('Paper loaded successfully', 'success'); | |
| } else { | |
| throw new Error(data.error || 'Failed to load paper'); | |
| } | |
| } catch (error) { | |
| console.error('Error opening paper:', error); | |
| this.showToast('Failed to open paper', 'error'); | |
| } | |
| } | |
| async deletePaper(docId) { | |
| console.log('ResearchRadar.deletePaper called with docId:', docId); | |
| if (!confirm('Are you sure you want to delete this paper? This action cannot be undone.')) { | |
| return; | |
| } | |
| try { | |
| console.log('Deleting document...'); | |
| const response = await fetch(`/documents/${docId}`, { | |
| method: 'DELETE', | |
| headers: { 'Content-Type': 'application/json' } | |
| }); | |
| const data = await response.json(); | |
| console.log('Delete response:', data); | |
| if (response.ok && data.success) { | |
| const card = document.querySelector(`[data-doc-id="${docId}"]`); | |
| if (card) { | |
| card.remove(); | |
| } | |
| this.showToast('Paper deleted successfully', 'success'); | |
| } else { | |
| throw new Error(data.error || 'Failed to delete paper'); | |
| } | |
| } catch (error) { | |
| console.error('Error deleting paper:', error); | |
| this.showToast('Failed to delete paper', 'error'); | |
| } | |
| } | |
| async clearAllPapers() { | |
| if (!confirm('Are you sure you want to clear all papers? This action cannot be undone.')) { | |
| return; | |
| } | |
| try { | |
| const response = await fetch('/documents', { | |
| method: 'DELETE', | |
| headers: { 'Content-Type': 'application/json' } | |
| }); | |
| const data = await response.json(); | |
| if (response.ok && data.success) { | |
| const gridEl = document.getElementById('papersGrid'); | |
| if (gridEl) { | |
| gridEl.innerHTML = ''; | |
| document.getElementById('mypapersEmpty').style.display = 'block'; | |
| } | |
| this.showToast('All papers cleared successfully', 'success'); | |
| } else { | |
| throw new Error(data.error || 'Failed to clear papers'); | |
| } | |
| } catch (error) { | |
| console.error('Error clearing papers:', error); | |
| this.showToast('Failed to clear papers', 'error'); | |
| } | |
| } | |
| } | |
| // Global navigation functions | |
| function navigateToApp(section = 'search') { | |
| console.log(`Global navigateToApp called with section: ${section}`); | |
| if (window.researchRadar) { | |
| console.log('Using ResearchRadar instance'); | |
| window.researchRadar.navigateToApp(section); | |
| } else { | |
| console.log('ResearchRadar instance not ready, using fallback navigation'); | |
| // Fallback navigation if ResearchRadar isn't ready yet | |
| const landingPage = document.getElementById('landingPage'); | |
| const appPage = document.getElementById('appPage'); | |
| if (landingPage && appPage) { | |
| landingPage.classList.remove('active'); | |
| appPage.classList.add('active'); | |
| // Switch to the requested section | |
| setTimeout(() => { | |
| const sections = document.querySelectorAll('.section'); | |
| sections.forEach(s => s.classList.remove('active')); | |
| const targetSection = document.getElementById(section); | |
| if (targetSection) { | |
| targetSection.classList.add('active'); | |
| } | |
| // Update navigation | |
| const navLinks = document.querySelectorAll('.nav-link'); | |
| navLinks.forEach(link => link.classList.remove('active')); | |
| const activeNavLink = document.querySelector(`[data-section="${section}"]`); | |
| if (activeNavLink) { | |
| activeNavLink.classList.add('active'); | |
| } | |
| }, 50); | |
| } | |
| } | |
| } | |
| function navigateToLanding() { | |
| console.log('Global navigateToLanding called'); | |
| if (window.researchRadar) { | |
| console.log('Using ResearchRadar instance'); | |
| window.researchRadar.navigateToLanding(); | |
| } else { | |
| console.log('ResearchRadar instance not ready, using fallback navigation'); | |
| // Fallback navigation if ResearchRadar isn't ready yet | |
| const landingPage = document.getElementById('landingPage'); | |
| const appPage = document.getElementById('appPage'); | |
| if (landingPage && appPage) { | |
| appPage.classList.remove('active'); | |
| landingPage.classList.add('active'); | |
| } | |
| } | |
| } | |
| // Global chat functions | |
| function toggleSuggestions() { | |
| if (window.researchRadar) { | |
| window.researchRadar.toggleSuggestions(); | |
| } | |
| } | |
| function showQuickActions() { | |
| if (window.researchRadar) { | |
| window.researchRadar.showQuickActions(); | |
| } | |
| } | |
| function hideQuickActions() { | |
| if (window.researchRadar) { | |
| window.researchRadar.hideQuickActions(); | |
| } | |
| } | |
| function generateSummary() { | |
| if (window.researchRadar) { | |
| window.researchRadar.generateSummary(); | |
| } | |
| } | |
| function extractKeyPoints() { | |
| if (window.researchRadar) { | |
| window.researchRadar.extractKeyPoints(); | |
| } | |
| } | |
| function findRelatedPapers() { | |
| if (window.researchRadar) { | |
| window.researchRadar.findRelatedPapers(); | |
| } | |
| } | |
| // Global functions for My Papers buttons | |
| function openPaperFromMyPapers(docId) { | |
| console.log('Global openPaperFromMyPapers called with docId:', docId); | |
| // Wait for ResearchRadar to be available | |
| const waitForResearchRadar = () => { | |
| if (window.researchRadar) { | |
| window.researchRadar.openPaperFromMyPapers(docId); | |
| } else { | |
| console.log('ResearchRadar not ready, waiting...'); | |
| setTimeout(waitForResearchRadar, 100); | |
| } | |
| }; | |
| waitForResearchRadar(); | |
| } | |
| function deletePaperFromMyPapers(docId) { | |
| console.log('Global deletePaperFromMyPapers called with docId:', docId); | |
| // Wait for ResearchRadar to be available | |
| const waitForResearchRadar = () => { | |
| if (window.researchRadar) { | |
| window.researchRadar.deletePaper(docId); | |
| } else { | |
| console.log('ResearchRadar not ready, waiting...'); | |
| setTimeout(waitForResearchRadar, 100); | |
| } | |
| }; | |
| waitForResearchRadar(); | |
| } | |
| function exportChat() { | |
| if (window.researchRadar) { | |
| window.researchRadar.exportChat(); | |
| } | |
| } | |
| // Enhanced Search Functions | |
| function toggleAdvancedSearch() { | |
| const advancedFilters = document.getElementById('advancedFilters'); | |
| const toggleBtn = document.querySelector('.advanced-search-btn'); | |
| if (advancedFilters) { | |
| const isShowing = advancedFilters.classList.toggle('show'); | |
| if (toggleBtn) { | |
| const icon = toggleBtn.querySelector('i'); | |
| if (icon) { | |
| if (isShowing) { | |
| icon.classList.remove('fa-sliders-h'); | |
| icon.classList.add('fa-times'); | |
| toggleBtn.classList.add('active'); | |
| } else { | |
| icon.classList.remove('fa-times'); | |
| icon.classList.add('fa-sliders-h'); | |
| toggleBtn.classList.remove('active'); | |
| } | |
| } | |
| } | |
| } | |
| } | |
| function toggleSearchTips() { | |
| const tipsContent = document.querySelector('.tips-content'); | |
| const tipsToggle = document.querySelector('.tips-toggle'); | |
| if (tipsContent) { | |
| tipsContent.classList.toggle('show'); | |
| if (tipsToggle) { | |
| const icon = tipsToggle.querySelector('i'); | |
| if (icon) { | |
| icon.classList.toggle('fa-chevron-down'); | |
| icon.classList.toggle('fa-chevron-up'); | |
| } | |
| } | |
| } | |
| } | |
| function clearSearchHistory() { | |
| localStorage.removeItem('recentSearches'); | |
| const recentSearchesContainer = document.getElementById('recentSearches'); | |
| if (recentSearchesContainer) { | |
| recentSearchesContainer.style.display = 'none'; | |
| } | |
| if (window.researchRadar) { | |
| window.researchRadar.showToast('Search history cleared', 'success'); | |
| } | |
| } | |
| // Enhanced Upload Functions | |
| function toggleUploadTips() { | |
| const tipsContent = document.getElementById('uploadTipsContent'); | |
| const tipsToggle = document.querySelector('.upload-tips .tips-toggle'); | |
| if (tipsContent) { | |
| tipsContent.classList.toggle('show'); | |
| if (tipsToggle) { | |
| const icon = tipsToggle.querySelector('i'); | |
| if (icon) { | |
| icon.classList.toggle('fa-chevron-down'); | |
| icon.classList.toggle('fa-chevron-up'); | |
| } | |
| } | |
| } | |
| } | |
| // Additional missing functions for summary-chat functionality | |
| function goBackToSearch() { | |
| console.log('Global goBackToSearch called'); | |
| if (window.researchRadar) { | |
| console.log('Using ResearchRadar instance for goBackToSearch'); | |
| // Hide summary-chat section | |
| const summarySection = document.getElementById('summary-chat'); | |
| if (summarySection) { | |
| summarySection.classList.remove('active'); | |
| summarySection.style.display = 'none'; | |
| } | |
| // Show search section and restore navigation | |
| window.researchRadar.switchSection('search'); | |
| console.log('Successfully returned to search section'); | |
| } else { | |
| console.log('ResearchRadar instance not ready, using fallback navigation'); | |
| // Fallback navigation | |
| const summarySection = document.getElementById('summary-chat'); | |
| const searchSection = document.getElementById('search'); | |
| if (summarySection) { | |
| summarySection.classList.remove('active'); | |
| summarySection.style.display = 'none'; | |
| } | |
| if (searchSection) { | |
| searchSection.classList.add('active'); | |
| searchSection.style.display = 'block'; | |
| } | |
| // Update navigation | |
| const navLinks = document.querySelectorAll('.nav-link'); | |
| navLinks.forEach(link => link.classList.remove('active')); | |
| const searchNavLink = document.querySelector('[data-section="search"]'); | |
| if (searchNavLink) { | |
| searchNavLink.classList.add('active'); | |
| } | |
| } | |
| } | |
| function exportSummaryChat() { | |
| console.log('Exporting summary and chat...'); | |
| if (window.researchRadar) { | |
| window.researchRadar.showToast('Export feature coming soon!', 'info'); | |
| } | |
| } | |
| function shareSummary() { | |
| console.log('Sharing summary...'); | |
| if (window.researchRadar) { | |
| window.researchRadar.showToast('Share feature coming soon!', 'info'); | |
| } | |
| } | |
| function regenerateSummary() { | |
| console.log('Regenerating summary...'); | |
| if (window.researchRadar) { | |
| window.researchRadar.showToast('Regenerating summary...', 'info'); | |
| } | |
| } | |
| function copySummary() { | |
| const summaryText = document.getElementById('summaryText'); | |
| if (summaryText) { | |
| navigator.clipboard.writeText(summaryText.textContent).then(() => { | |
| if (window.researchRadar) { | |
| window.researchRadar.showToast('Summary copied to clipboard!', 'success'); | |
| } | |
| }); | |
| } | |
| } | |
| function askQuickQuestion(question) { | |
| const chatInput = document.getElementById('chatInputPanel'); | |
| if (chatInput) { | |
| chatInput.value = question; | |
| chatInput.focus(); | |
| } | |
| } | |
| // Global summarize paper function (fallback for any remaining onclick handlers) | |
| function summarizePaper(paperUrl) { | |
| console.log(`Global summarizePaper called with URL: ${paperUrl}`); | |
| if (window.researchRadar) { | |
| console.log('Using ResearchRadar instance for summarizePaper'); | |
| window.researchRadar.summarizePaper(paperUrl); | |
| } else { | |
| console.error('ResearchRadar instance not available for summarizePaper'); | |
| alert('Application not ready. Please try again in a moment.'); | |
| } | |
| } | |
| // Immediate setup for critical buttons (before full initialization) | |
| function setupCriticalButtons() { | |
| console.log('Setting up critical buttons immediately...'); | |
| // Set up the main navigation buttons with fallback functions | |
| const buttons = [ | |
| { selector: '.nav-cta-btn', action: () => navigateToApp('search'), name: 'Get Started' }, | |
| { selector: '.cta-button.primary', action: () => navigateToApp('search'), name: 'Start Exploring' }, | |
| { selector: '.cta-button.secondary', action: () => navigateToApp('upload'), name: 'Upload Paper' }, | |
| { selector: '.back-to-landing', action: () => navigateToLanding(), name: 'Back to Landing' }, | |
| { selector: '.app-nav .nav-brand', action: () => navigateToLanding(), name: 'Brand Logo' } | |
| ]; | |
| buttons.forEach(({ selector, action, name }) => { | |
| const button = document.querySelector(selector); | |
| if (button) { | |
| console.log(`✅ Setting up ${name} button`); | |
| button.removeAttribute('onclick'); | |
| button.addEventListener('click', (e) => { | |
| e.preventDefault(); | |
| console.log(`${name} button clicked!`); | |
| action(); | |
| }); | |
| } else { | |
| console.log(`❌ ${name} button not found`); | |
| } | |
| }); | |
| // Also setup summary page buttons if they exist | |
| setupSummaryPageButtonsGlobal(); | |
| } | |
| // Global function to setup summary page buttons | |
| function setupSummaryPageButtonsGlobal() { | |
| console.log('Setting up summary page buttons globally...'); | |
| // Summary page buttons with fallback functions | |
| const summaryButtons = [ | |
| { selector: '.back-btn', action: () => goBackToSearch(), name: 'Back to Search' }, | |
| { selector: '.summary-action-btn[title*="Copy"]', action: () => copySummary(), name: 'Copy Summary' }, | |
| { selector: '.summary-action-btn[title*="Regenerate"]', action: () => regenerateSummary(), name: 'Regenerate Summary' }, | |
| { selector: '.action-btn-header[title*="Export"]', action: () => exportSummaryChat(), name: 'Export Summary' }, | |
| { selector: '.action-btn-header[title*="Share"]', action: () => shareSummary(), name: 'Share Summary' } | |
| ]; | |
| summaryButtons.forEach(({ selector, action, name }) => { | |
| const button = document.querySelector(selector); | |
| if (button) { | |
| console.log(`✅ Setting up ${name} button`); | |
| button.removeAttribute('onclick'); | |
| // Clone button to remove all existing event listeners | |
| const newButton = button.cloneNode(true); | |
| button.parentNode.replaceChild(newButton, button); | |
| newButton.addEventListener('click', (e) => { | |
| e.preventDefault(); | |
| console.log(`${name} button clicked!`); | |
| action(); | |
| }); | |
| } else { | |
| console.log(`❌ ${name} button not found`); | |
| } | |
| }); | |
| // Setup quick question buttons | |
| const quickQuestionBtns = document.querySelectorAll('.quick-question-btn'); | |
| console.log(`Found ${quickQuestionBtns.length} quick question buttons`); | |
| quickQuestionBtns.forEach((btn, index) => { | |
| btn.removeAttribute('onclick'); | |
| // Clone button to remove existing listeners | |
| const newBtn = btn.cloneNode(true); | |
| btn.parentNode.replaceChild(newBtn, btn); | |
| newBtn.addEventListener('click', (e) => { | |
| e.preventDefault(); | |
| const question = newBtn.textContent.trim(); | |
| console.log(`Quick question button ${index + 1} clicked: ${question}`); | |
| askQuickQuestion(question); | |
| }); | |
| }); | |
| } | |
| // Run critical setup immediately if DOM is already loaded | |
| if (document.readyState === 'loading') { | |
| document.addEventListener('DOMContentLoaded', setupCriticalButtons); | |
| } else { | |
| setupCriticalButtons(); | |
| } | |
| // Initialize the application when DOM is loaded | |
| document.addEventListener('DOMContentLoaded', () => { | |
| console.log('DOM Content Loaded - Initializing Research Radar...'); | |
| // Small delay to ensure all elements are rendered | |
| setTimeout(() => { | |
| window.researchRadar = new ResearchRadar(); | |
| console.log('🚀 Research Radar - Application initialized successfully!'); | |
| // Test if critical elements exist | |
| const testElements = [ | |
| 'searchInput', | |
| 'searchBtn', | |
| 'analyzeUrlBtn', | |
| 'fileInput', | |
| 'searchResults' | |
| ]; | |
| testElements.forEach(id => { | |
| const element = document.getElementById(id); | |
| console.log(`Element ${id}:`, element ? 'Found' : 'Not found'); | |
| }); | |
| // Test if Generate Summary buttons exist (they might be created dynamically) | |
| setTimeout(() => { | |
| const generateButtons = document.querySelectorAll('.generate-summary-btn'); | |
| console.log(`Dynamic Generate Summary buttons found: ${generateButtons.length}`); | |
| }, 1000); | |
| }, 100); | |
| }); | |
| // Add some additional CSS for animations | |
| const additionalCSS = ` | |
| @keyframes toastSlideOut { | |
| to { | |
| opacity: 0; | |
| transform: translateX(100%); | |
| } | |
| } | |
| `; | |
| const styleSheet = document.createElement('style'); | |
| styleSheet.textContent = additionalCSS; | |
| document.head.appendChild(styleSheet); | |
| // Tab switching functionality | |
| function switchTab(tabName) { | |
| // Strict tabs: only one panel visible | |
| document.querySelectorAll('.tab-btn').forEach(btn => { | |
| btn.classList.remove('active'); | |
| }); | |
| document.querySelectorAll('.tab-content').forEach(content => { | |
| content.classList.remove('active'); | |
| }); | |
| document.querySelector(`[data-tab="${tabName}"]`)?.classList.add('active'); | |
| document.getElementById(`${tabName}-tab`)?.classList.add('active'); | |
| if (tabName === 'chat') { | |
| setTimeout(() => { | |
| const chatInput = document.getElementById('chatInputPanel'); | |
| if (chatInput) chatInput.focus(); | |
| }, 100); | |
| } | |
| history.replaceState(null, null, `#${tabName}`); | |
| const tabDisplayName = tabName === 'summary' ? 'Summary' : 'Chat'; | |
| showToast(`Switched to ${tabDisplayName} tab`, 'info'); | |
| } | |
| // Initialize tab from URL hash | |
| function initializeTabFromHash() { | |
| const hash = window.location.hash.substring(1); | |
| if (hash === 'summary' || hash === 'chat') { | |
| switchTab(hash); | |
| } | |
| } | |
| // Quick question functionality | |
| function askQuickQuestion(question) { | |
| const chatInput = document.getElementById('chatInputPanel'); | |
| if (chatInput) { | |
| chatInput.value = question; | |
| chatInput.focus(); | |
| } | |
| } | |
| // Enhanced chat input functionality | |
| function initializeChatInput() { | |
| const chatInput = document.getElementById('chatInputPanel'); | |
| const sendBtn = document.getElementById('chatSendBtnPanel'); | |
| if (chatInput && sendBtn) { | |
| // Auto-resize textarea | |
| chatInput.addEventListener('input', function() { | |
| this.style.height = 'auto'; | |
| this.style.height = Math.min(this.scrollHeight, 120) + 'px'; | |
| }); | |
| // Handle Enter key | |
| chatInput.addEventListener('keydown', function(e) { | |
| if (e.key === 'Enter') { | |
| if (e.ctrlKey || e.metaKey) { | |
| // Ctrl+Enter or Cmd+Enter to send | |
| e.preventDefault(); | |
| sendChatMessage(); | |
| } else if (!e.shiftKey) { | |
| // Enter to send (unless Shift+Enter for new line) | |
| e.preventDefault(); | |
| sendChatMessage(); | |
| } | |
| } | |
| }); | |
| // Send button click | |
| sendBtn.addEventListener('click', sendChatMessage); | |
| } | |
| } | |
| // Send chat message functionality | |
| function sendChatMessage() { | |
| const chatInput = document.getElementById('chatInputPanel'); | |
| const message = chatInput.value.trim(); | |
| if (!message) { | |
| if (window.researchRadar) { | |
| window.researchRadar.showToast('Please enter a message', 'warning'); | |
| } | |
| return; | |
| } | |
| // Add user message to chat | |
| addMessageToChat('user', message); | |
| // Clear input | |
| chatInput.value = ''; | |
| chatInput.style.height = 'auto'; | |
| // Show typing indicator | |
| showTypingIndicator(); | |
| // Call backend chat API | |
| fetch('/chat', { | |
| method: 'POST', | |
| headers: { 'Content-Type': 'application/json' }, | |
| body: JSON.stringify({ message }) | |
| }) | |
| .then(res => res.json()) | |
| .then(data => { | |
| hideTypingIndicator(); | |
| if (data && data.success) { | |
| addMessageToChat('assistant', data.response || ''); | |
| } else { | |
| addMessageToChat('assistant', `Error: ${data?.error || 'Unknown error'}`); | |
| } | |
| }) | |
| .catch(err => { | |
| console.error('Chat error:', err); | |
| hideTypingIndicator(); | |
| addMessageToChat('assistant', 'Network error. Please try again.'); | |
| }); | |
| } | |
| // Add message to chat | |
| function addMessageToChat(sender, message) { | |
| const chatContainer = document.getElementById('chatMessagesPanel'); | |
| const messageElement = document.createElement('div'); | |
| messageElement.className = `chat-message ${sender}`; | |
| const timestamp = new Date().toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }); | |
| const avatarIcon = sender === 'user' ? 'fa-user' : 'fa-robot'; | |
| messageElement.innerHTML = ` | |
| <div class="message-avatar"> | |
| <i class="fas ${avatarIcon}"></i> | |
| </div> | |
| <div class="message-content"> | |
| <div class="message-bubble"> | |
| <p>${message}</p> | |
| </div> | |
| <div class="message-meta"> | |
| <span>${sender === 'user' ? 'You' : 'AI Assistant'} • ${timestamp}</span> | |
| </div> | |
| </div> | |
| `; | |
| // Remove welcome message if it exists | |
| const welcomeMessage = chatContainer.querySelector('.chat-welcome'); | |
| if (welcomeMessage) { | |
| welcomeMessage.style.display = 'none'; | |
| } | |
| chatContainer.appendChild(messageElement); | |
| // Scroll to bottom | |
| chatContainer.scrollTop = chatContainer.scrollHeight; | |
| } | |
| // Show typing indicator | |
| function showTypingIndicator() { | |
| const chatContainer = document.getElementById('chatMessagesPanel'); | |
| // Prevent adding multiple indicators | |
| if (document.getElementById('typingIndicator')) return; | |
| const typingIndicator = document.createElement('div'); | |
| typingIndicator.className = 'typing-indicator chat-message assistant'; | |
| typingIndicator.id = 'typingIndicator'; | |
| typingIndicator.innerHTML = ` | |
| <div class="message-avatar"> | |
| <i class="fas fa-robot"></i> | |
| </div> | |
| <div class="message-content"> | |
| <div class="message-bubble"> | |
| <div class="typing-dots"> | |
| <div class="dot"></div> | |
| <div class="dot"></div> | |
| <div class="dot"></div> | |
| </div> | |
| </div> | |
| </div> | |
| `; | |
| chatContainer.appendChild(typingIndicator); | |
| chatContainer.scrollTop = chatContainer.scrollHeight; | |
| } | |
| // Hide typing indicator | |
| function hideTypingIndicator() { | |
| const typingIndicator = document.getElementById('typingIndicator'); | |
| if (typingIndicator) { | |
| typingIndicator.remove(); | |
| } | |
| } | |
| // Initialize when DOM is loaded | |
| document.addEventListener('DOMContentLoaded', function() { | |
| // ... existing code ... | |
| // Initialize tab functionality | |
| initializeTabFromHash(); | |
| initializeChatInput(); | |
| // Listen for hash changes | |
| window.addEventListener('hashchange', initializeTabFromHash); | |
| // ... existing code ... | |
| }); | |