Spaces:
Sleeping
Sleeping
// ResearchMate Main JavaScript | |
// Global variables | |
let currentToast = null; | |
// Authentication utilities with enhanced security | |
let sessionTimeout = null; | |
let lastActivityTime = Date.now(); | |
const SESSION_TIMEOUT_MINUTES = 480; // 8 hours for prototype (less aggressive) | |
const ACTIVITY_CHECK_INTERVAL = 300000; // Check every 5 minutes | |
function getAuthToken() { | |
// Check both sessionStorage (preferred) and localStorage (fallback) | |
return sessionStorage.getItem('authToken') || localStorage.getItem('authToken'); | |
} | |
function setAuthToken(token) { | |
// Store in sessionStorage for better security (clears on browser close) | |
sessionStorage.setItem('authToken', token); | |
// Also store in localStorage for compatibility, but with shorter expiry | |
localStorage.setItem('authToken', token); | |
localStorage.setItem('tokenTimestamp', Date.now().toString()); | |
// Set cookie with Secure flag only if using HTTPS | |
let cookie = `authToken=${token}; path=/; SameSite=Strict`; | |
if (location.protocol === 'https:') { | |
cookie += '; Secure'; | |
} | |
document.cookie = cookie; | |
// Reset activity tracking | |
lastActivityTime = Date.now(); | |
startSessionTimeout(); | |
} | |
function clearAuthToken() { | |
sessionStorage.removeItem('authToken'); | |
sessionStorage.removeItem('userId'); | |
localStorage.removeItem('authToken'); | |
localStorage.removeItem('userId'); | |
localStorage.removeItem('tokenTimestamp'); | |
// Clear cookie | |
document.cookie = 'authToken=; path=/; expires=Thu, 01 Jan 1970 00:00:00 GMT; SameSite=Strict'; | |
clearTimeout(sessionTimeout); | |
} | |
function isTokenExpired() { | |
const timestamp = localStorage.getItem('tokenTimestamp'); | |
if (!timestamp) return true; | |
const tokenAge = Date.now() - parseInt(timestamp); | |
const maxAge = 24 * 60 * 60 * 1000; // 24 hours | |
return tokenAge > maxAge; | |
} | |
function startSessionTimeout() { | |
clearTimeout(sessionTimeout); | |
sessionTimeout = setTimeout(() => { | |
const inactivityTime = Date.now() - lastActivityTime; | |
if (inactivityTime >= SESSION_TIMEOUT_MINUTES * 60 * 1000) { | |
// Session expired due to inactivity | |
showToast('Session expired due to inactivity. Please log in again.', 'warning'); | |
logout(); | |
} else { | |
// Still active, reset timer | |
startSessionTimeout(); | |
} | |
}, ACTIVITY_CHECK_INTERVAL); | |
} | |
function trackActivity() { | |
lastActivityTime = Date.now(); | |
} | |
function setAuthHeaders(headers = {}) { | |
const token = getAuthToken(); | |
if (token && !isTokenExpired()) { | |
headers['Authorization'] = `Bearer ${token}`; | |
} | |
return headers; | |
} | |
function makeAuthenticatedRequest(url, options = {}) { | |
const headers = setAuthHeaders(options.headers || {}); | |
return fetch(url, { | |
...options, | |
headers: headers | |
}); | |
} | |
// Check if user is authenticated | |
function isAuthenticated() { | |
const token = getAuthToken(); | |
return !!(token && !isTokenExpired()); | |
} | |
// Redirect to login if not authenticated | |
function requireAuth() { | |
if (!isAuthenticated()) { | |
clearAuthToken(); | |
window.location.href = '/login'; | |
return false; | |
} | |
return true; | |
} | |
// Document ready with enhanced security | |
document.addEventListener('DOMContentLoaded', function() { | |
// Check authentication on protected pages | |
if (window.location.pathname !== '/login' && !isAuthenticated()) { | |
clearAuthToken(); | |
window.location.href = '/login'; | |
return; | |
} | |
// Start session timeout if authenticated | |
if (isAuthenticated()) { | |
startSessionTimeout(); | |
} | |
// Track user activity for session timeout | |
document.addEventListener('click', trackActivity); | |
document.addEventListener('keypress', trackActivity); | |
document.addEventListener('scroll', trackActivity); | |
document.addEventListener('mousemove', trackActivity); | |
// Initialize tooltips | |
initializeTooltips(); | |
// Handle page visibility changes (user switches tabs or minimizes browser) | |
document.addEventListener('visibilitychange', function() { | |
if (document.hidden) { | |
// Page is hidden, reduce activity tracking | |
clearTimeout(sessionTimeout); | |
} else { | |
// Page is visible again, resume activity tracking | |
if (isAuthenticated()) { | |
trackActivity(); | |
startSessionTimeout(); | |
} | |
} | |
}); | |
// Handle beforeunload event (browser/tab closing) | |
window.addEventListener('beforeunload', function() { | |
// Clear sessionStorage on page unload (but keep localStorage for potential restoration) | |
sessionStorage.clear(); | |
}); | |
// Periodically validate token with server (disabled for prototype) | |
// if (isAuthenticated()) { | |
// setInterval(async function() { | |
// try { | |
// const response = await makeAuthenticatedRequest('/api/user/status'); | |
// if (!response.ok) { | |
// // Token is invalid or expired | |
// showToast('Session expired. Please log in again.', 'warning'); | |
// logout(); | |
// } | |
// } catch (error) { | |
// console.log('Token validation failed:', error); | |
// } | |
// }, 5 * 60 * 1000); // Check every 5 minutes | |
// } | |
// Initialize smooth scrolling | |
initializeSmoothScrolling(); | |
// Initialize animations | |
initializeAnimations(); | |
// Initialize keyboard shortcuts | |
initializeKeyboardShortcuts(); | |
// Theme toggle removed | |
// Initialize upload | |
initializeUpload(); | |
// Initialize search page (if on search page) | |
initializeSearchPage(); | |
console.log('ResearchMate initialized successfully!'); | |
}); | |
// Initialize tooltips | |
function initializeTooltips() { | |
const tooltipTriggerList = document.querySelectorAll('[data-bs-toggle="tooltip"]'); | |
const tooltipList = [...tooltipTriggerList].map(tooltipTriggerEl => new bootstrap.Tooltip(tooltipTriggerEl)); | |
} | |
// Initialize smooth scrolling | |
function initializeSmoothScrolling() { | |
document.querySelectorAll('a[href^="#"]').forEach(anchor => { | |
anchor.addEventListener('click', function (e) { | |
const href = this.getAttribute('href'); | |
// Skip if href is just '#', which is not a valid selector | |
if (href === '#') return; | |
e.preventDefault(); | |
const target = document.querySelector(href); | |
if (target) { | |
target.scrollIntoView({ | |
behavior: 'smooth', | |
block: 'start' | |
}); | |
} | |
}); | |
}); | |
} | |
// Initialize animations | |
function initializeAnimations() { | |
const observer = new IntersectionObserver((entries) => { | |
entries.forEach(entry => { | |
if (entry.isIntersecting) { | |
entry.target.classList.add('fade-in'); | |
} | |
}); | |
}, { | |
threshold: 0.1, | |
rootMargin: '0px 0px -50px 0px' | |
}); | |
document.querySelectorAll('.card, .alert').forEach(el => { | |
observer.observe(el); | |
}); | |
} | |
// Initialize keyboard shortcuts | |
function initializeKeyboardShortcuts() { | |
document.addEventListener('keydown', function(e) { | |
// Ctrl/Cmd + K: Focus search | |
if ((e.ctrlKey || e.metaKey) && e.key === 'k') { | |
e.preventDefault(); | |
const searchInput = document.querySelector('#query, #question, #topic'); | |
if (searchInput) { | |
searchInput.focus(); | |
} | |
} | |
// Ctrl/Cmd + Enter: Submit form | |
if ((e.ctrlKey || e.metaKey) && e.key === 'Enter') { | |
e.preventDefault(); | |
const form = document.querySelector('form'); | |
if (form) { | |
form.dispatchEvent(new Event('submit')); | |
} | |
} | |
// Escape: Close modals | |
if (e.key === 'Escape') { | |
const modals = document.querySelectorAll('.modal.show'); | |
modals.forEach(modal => { | |
const bsModal = bootstrap.Modal.getInstance(modal); | |
if (bsModal) { | |
bsModal.hide(); | |
} | |
}); | |
} | |
}); | |
} | |
// Theme toggle removed: always use dark theme | |
// Enhanced Upload functionality | |
function initializeUpload() { | |
const uploadArea = document.getElementById('upload-area'); | |
const fileInput = document.getElementById('pdf-file'); | |
const uploadBtn = document.getElementById('upload-btn'); | |
if (!uploadArea || !fileInput || !uploadBtn) return; | |
// Restore previous upload results if they exist | |
restoreUploadResults(); | |
// Click to browse files | |
uploadArea.addEventListener('click', () => { | |
fileInput.click(); | |
}); | |
// Drag and drop functionality | |
uploadArea.addEventListener('dragover', (e) => { | |
e.preventDefault(); | |
uploadArea.classList.add('dragover'); | |
}); | |
uploadArea.addEventListener('dragleave', () => { | |
uploadArea.classList.remove('dragover'); | |
}); | |
uploadArea.addEventListener('drop', (e) => { | |
e.preventDefault(); | |
uploadArea.classList.remove('dragover'); | |
const files = e.dataTransfer.files; | |
if (files.length > 0 && files[0].type === 'application/pdf') { | |
fileInput.files = files; | |
handleFileSelection(files[0]); | |
} else { | |
showToast('Please select a valid PDF file', 'danger'); | |
} | |
}); | |
// File input change | |
fileInput.addEventListener('change', (e) => { | |
if (e.target.files.length > 0) { | |
handleFileSelection(e.target.files[0]); | |
} | |
}); | |
function handleFileSelection(file) { | |
uploadBtn.disabled = false; | |
uploadBtn.innerHTML = `<i class="fas fa-upload me-2"></i>Upload "${file.name}"`; | |
// Update upload area | |
uploadArea.innerHTML = ` | |
<i class="fas fa-file-pdf text-danger"></i> | |
<h5 class="mt-2 text-success">File Selected</h5> | |
<p class="text-muted">${file.name} (${formatFileSize(file.size)})</p> | |
`; | |
} | |
} | |
// Format file size | |
function formatFileSize(bytes) { | |
if (bytes === 0) return '0 Bytes'; | |
const k = 1024; | |
const sizes = ['Bytes', 'KB', 'MB', 'GB']; | |
const i = Math.floor(Math.log(bytes) / Math.log(k)); | |
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]; | |
} | |
// Toggle upload area visibility | |
function toggleUploadArea() { | |
const uploadArea = document.getElementById('upload-area'); | |
if (uploadArea) { | |
uploadArea.classList.toggle('d-none'); | |
} | |
} | |
// Upload result persistence functions | |
function saveUploadResults(data) { | |
try { | |
const currentUser = getCurrentUserId(); | |
const currentSession = getSessionId(); | |
if (!currentUser || !currentSession) { | |
console.warn('Cannot save upload results: no user or session'); | |
return; | |
} | |
const dataToSave = { | |
...data, | |
userId: currentUser, | |
sessionId: currentSession, | |
savedAt: new Date().toISOString(), | |
pageUrl: window.location.pathname | |
}; | |
saveToLocalStorage('researchmate_upload_results', dataToSave); | |
} catch (error) { | |
console.error('Failed to save upload results:', error); | |
} | |
} | |
function restoreUploadResults() { | |
try { | |
const resultsContainer = document.getElementById('results-container'); | |
if (!resultsContainer) return; | |
// Get current user from session/token | |
const currentUser = getCurrentUserId(); // You'll need to implement this | |
if (!currentUser) { | |
// No user logged in, clear any existing results | |
clearUploadResults(); | |
return; | |
} | |
const savedData = loadFromLocalStorage('researchmate_upload_results'); | |
if (savedData && savedData.pageUrl === window.location.pathname) { | |
// Check if data belongs to current user | |
if (savedData.userId !== currentUser) { | |
console.log('Upload results belong to different user, clearing'); | |
clearUploadResults(); | |
return; | |
} | |
// Check if data is from current session | |
const currentSessionId = getSessionId(); // You'll need to implement this | |
if (savedData.sessionId !== currentSessionId) { | |
console.log('Upload results from different session, clearing'); | |
clearUploadResults(); | |
return; | |
} | |
// Check if data is recent (within current session, max 1 hour) | |
const savedTime = new Date(savedData.savedAt); | |
const now = new Date(); | |
const hoursDiff = (now - savedTime) / (1000 * 60 * 60); | |
if (hoursDiff < 1) { | |
console.log('Restoring upload results from current session'); | |
displayUploadResults(savedData); | |
showToast('Previous PDF analysis restored', 'info', 3000); | |
} else { | |
// Clean up old data | |
clearUploadResults(); | |
} | |
} | |
} catch (error) { | |
console.error('Failed to restore upload results:', error); | |
} | |
} | |
// Helper function to get current user ID | |
function getCurrentUserId() { | |
try { | |
const token = localStorage.getItem('authToken') || sessionStorage.getItem('authToken'); | |
if (!token) return null; | |
// Decode JWT token to get user ID (simple base64 decode) | |
const payload = JSON.parse(atob(token.split('.')[1])); | |
return payload.user_id || payload.sub; | |
} catch (error) { | |
console.error('Failed to get current user ID:', error); | |
return null; | |
} | |
} | |
// Helper function to get session ID | |
function getSessionId() { | |
// Use browser session storage for session ID | |
let sessionId = sessionStorage.getItem('researchmate_session_id'); | |
if (!sessionId) { | |
// Generate new session ID if not exists | |
sessionId = 'session_' + Date.now() + '_' + Math.random().toString(36).substr(2, 9); | |
sessionStorage.setItem('researchmate_session_id', sessionId); | |
} | |
return sessionId; | |
} | |
function clearUploadResults() { | |
try { | |
localStorage.removeItem('researchmate_upload_results'); | |
const resultsContainer = document.getElementById('results-container'); | |
if (resultsContainer) { | |
resultsContainer.innerHTML = ''; | |
} | |
} catch (error) { | |
console.error('Failed to clear upload results:', error); | |
} | |
} | |
function displayUploadResults(data) { | |
const resultsContainer = document.getElementById('results-container'); | |
if (!resultsContainer) return; | |
const summary = data.summary || {}; | |
// Utility: Render markdown using a JS markdown parser (marked.js) | |
function renderMarkdown(text) { | |
if (typeof marked !== 'undefined') { | |
return marked.parseInline(text || ''); | |
} | |
return text || ''; | |
} | |
const html = ` | |
<div class="card"> | |
<div class="card-header d-flex justify-content-between align-items-center"> | |
<h5 class="mb-0"> | |
<i class="fas fa-file-pdf me-2"></i>PDF Analysis Results | |
</h5> | |
<button class="btn btn-sm btn-outline-secondary" onclick="clearUploadResults()" title="Clear results"> | |
<i class="fas fa-times"></i> | |
</button> | |
</div> | |
<div class="card-body"> | |
<!-- Paper Info --> | |
<div class="row mb-4"> | |
<div class="col-md-8"> | |
<h6 class="text-primary">📄 Paper Information</h6> | |
<h5 class="mb-2">${renderMarkdown(data.title || 'Unknown Title')}</h5> | |
<p class="text-muted small mb-2"> | |
<i class="fas fa-clock me-1"></i> | |
Processed: ${data.processed_at ? new Date(data.processed_at).toLocaleString() : 'N/A'} | |
</p> | |
<p class="text-muted small"> | |
<i class="fas fa-file-alt me-1"></i> | |
Text Length: ${data.text_length ? data.text_length.toLocaleString() : 'N/A'} characters | |
</p> | |
</div> | |
<div class="col-md-4 text-end"> | |
<div class="badge bg-success p-2"> | |
<i class="fas fa-check-circle me-1"></i> | |
Analysis Complete | |
</div> | |
${data.savedAt ? ` | |
<div class="badge bg-info p-2 mt-2 d-block"> | |
<i class="fas fa-history me-1"></i> | |
Restored | |
</div> | |
` : ''} | |
</div> | |
</div> | |
<!-- Abstract --> | |
<div class="mb-4"> | |
<h6 class="text-info">📝 Abstract</h6> | |
<div class="border-start border-info ps-3"> | |
<div class="mb-0">${renderMarkdown(data.abstract || 'Abstract not found')}</div> | |
</div> | |
</div> | |
<!-- AI Analysis --> | |
<div class="row"> | |
<!-- Main Summary --> | |
<div class="col-md-6 mb-4"> | |
<div class="card h-100 border-primary"> | |
<div class="card-header bg-primary text-white"> | |
<h6 class="mb-0"> | |
<i class="fas fa-brain me-2"></i>Main Summary | |
</h6> | |
</div> | |
<div class="card-body"> | |
<div class="mb-0">${renderMarkdown(summary.summary || 'Summary not available')}</div> | |
</div> | |
</div> | |
</div> | |
<!-- Key Contributions --> | |
<div class="col-md-6 mb-4"> | |
<div class="card h-100 border-success"> | |
<div class="card-header bg-success text-white"> | |
<h6 class="mb-0"> | |
<i class="fas fa-star me-2"></i>Key Contributions | |
</h6> | |
</div> | |
<div class="card-body"> | |
<div class="contributions-text"> | |
${summary.contributions ? summary.contributions.split('\n').map(line => line.trim()).filter(line => line).map(line => `<div class="mb-1">${renderMarkdown(line)}</div>`).join('') : 'Contributions not available'} | |
</div> | |
</div> | |
</div> | |
</div> | |
<!-- Methodology --> | |
<div class="col-md-6 mb-4"> | |
<div class="card h-100 border-info"> | |
<div class="card-header bg-info text-white"> | |
<h6 class="mb-0"> | |
<i class="fas fa-cogs me-2"></i>Methodology | |
</h6> | |
</div> | |
<div class="card-body"> | |
<div class="mb-0">${renderMarkdown(summary.methodology || 'Methodology not available')}</div> | |
</div> | |
</div> | |
</div> | |
<!-- Key Findings --> | |
<div class="col-md-6 mb-4"> | |
<div class="card h-100 border-warning"> | |
<div class="card-header bg-warning text-dark"> | |
<h6 class="mb-0"> | |
<i class="fas fa-lightbulb me-2"></i>Key Findings | |
</h6> | |
</div> | |
<div class="card-body"> | |
<div class="findings-text"> | |
${summary.findings ? summary.findings.split('\n').map(line => line.trim()).filter(line => line).map(line => `<div class="mb-1">${renderMarkdown(line)}</div>`).join('') : 'Findings not available'} | |
</div> | |
</div> | |
</div> | |
</div> | |
<!-- Limitations --> | |
<div class="col-12 mb-4"> | |
<div class="card border-danger"> | |
<div class="card-header bg-danger text-white"> | |
<h6 class="mb-0"> | |
<i class="fas fa-exclamation-triangle me-2"></i>Limitations | |
</h6> | |
</div> | |
<div class="card-body"> | |
<div class="mb-0">${renderMarkdown(summary.limitations || 'Limitations not identified')}</div> | |
</div> | |
</div> | |
</div> | |
</div> | |
<!-- Actions --> | |
<div class="d-flex justify-content-between align-items-center"> | |
<div> | |
<button class="btn btn-success me-2" onclick="addToKnowledgeBase()"> | |
<i class="fas fa-database me-1"></i>Add to Knowledge Base | |
</button> | |
<button class="btn btn-info" onclick="askAboutPaper()"> | |
<i class="fas fa-question-circle me-1"></i>Ask Questions | |
</button> | |
</div> | |
<div class="text-muted"> | |
<small> | |
<i class="fas fa-check me-1"></i> | |
Paper analyzed and ready for questions | |
</small> | |
</div> | |
</div> | |
</div> | |
</div> | |
`; | |
resultsContainer.innerHTML = html; | |
} | |
// Utility functions | |
function showToast(message, type = 'info', duration = 5000) { | |
// Remove existing toast | |
if (currentToast) { | |
currentToast.remove(); | |
} | |
const toast = document.createElement('div'); | |
toast.className = `toast align-items-center text-bg-${type} border-0 position-fixed top-0 end-0 m-3`; | |
toast.style.zIndex = '9999'; | |
toast.setAttribute('role', 'alert'); | |
toast.setAttribute('aria-live', 'assertive'); | |
toast.setAttribute('aria-atomic', 'true'); | |
toast.innerHTML = ` | |
<div class="d-flex"> | |
<div class="toast-body"> | |
<i class="fas fa-${getIconForType(type)} me-2"></i> | |
${message} | |
</div> | |
<button type="button" class="btn-close btn-close-white me-2 m-auto" data-bs-dismiss="toast"></button> | |
</div> | |
`; | |
document.body.appendChild(toast); | |
currentToast = toast; | |
const bsToast = new bootstrap.Toast(toast, { | |
autohide: true, | |
delay: duration | |
}); | |
bsToast.show(); | |
// Clean up after toast is hidden | |
toast.addEventListener('hidden.bs.toast', function() { | |
toast.remove(); | |
if (currentToast === toast) { | |
currentToast = null; | |
} | |
}); | |
} | |
function getIconForType(type) { | |
const icons = { | |
'success': 'check-circle', | |
'danger': 'exclamation-triangle', | |
'warning': 'exclamation-triangle', | |
'info': 'info-circle', | |
'primary': 'info-circle', | |
'secondary': 'info-circle' | |
}; | |
return icons[type] || 'info-circle'; | |
} | |
function formatDate(dateString) { | |
const date = new Date(dateString); | |
return date.toLocaleDateString('en-US', { | |
year: 'numeric', | |
month: 'short', | |
day: 'numeric', | |
hour: '2-digit', | |
minute: '2-digit' | |
}); | |
} | |
function formatNumber(num) { | |
if (num >= 1000000) { | |
return (num / 1000000).toFixed(1) + 'M'; | |
} else if (num >= 1000) { | |
return (num / 1000).toFixed(1) + 'K'; | |
} | |
return num.toString(); | |
} | |
function truncateText(text, maxLength) { | |
if (text.length <= maxLength) { | |
return text; | |
} | |
return text.substring(0, maxLength) + '...'; | |
} | |
function copyToClipboard(text) { | |
navigator.clipboard.writeText(text).then(() => { | |
showToast('Copied to clipboard!', 'success', 2000); | |
}).catch(err => { | |
showToast('Failed to copy to clipboard', 'danger', 3000); | |
}); | |
} | |
function downloadText(text, filename) { | |
const element = document.createElement('a'); | |
element.setAttribute('href', 'data:text/plain;charset=utf-8,' + encodeURIComponent(text)); | |
element.setAttribute('download', filename); | |
element.style.display = 'none'; | |
document.body.appendChild(element); | |
element.click(); | |
document.body.removeChild(element); | |
} | |
function debounce(func, wait) { | |
let timeout; | |
return function executedFunction(...args) { | |
const later = () => { | |
clearTimeout(timeout); | |
func(...args); | |
}; | |
clearTimeout(timeout); | |
timeout = setTimeout(later, wait); | |
}; | |
} | |
function throttle(func, limit) { | |
let inThrottle; | |
return function() { | |
const args = arguments; | |
const context = this; | |
if (!inThrottle) { | |
func.apply(context, args); | |
inThrottle = true; | |
setTimeout(() => inThrottle = false, limit); | |
} | |
}; | |
} | |
// API helper functions | |
async function apiRequest(url, options = {}) { | |
try { | |
const response = await fetch(url, { | |
headers: { | |
'Content-Type': 'application/json', | |
...options.headers | |
}, | |
...options | |
}); | |
if (!response.ok) { | |
throw new Error(`HTTP error! status: ${response.status}`); | |
} | |
return await response.json(); | |
} catch (error) { | |
console.error('API request failed:', error); | |
throw error; | |
} | |
} | |
// Search functionality | |
function highlightSearchTerms(text, terms) { | |
if (!terms || terms.length === 0) return text; | |
let highlightedText = text; | |
terms.forEach(term => { | |
const regex = new RegExp(`(${term})`, 'gi'); | |
highlightedText = highlightedText.replace(regex, '<mark>$1</mark>'); | |
}); | |
return highlightedText; | |
} | |
// Search page functionality | |
function initializeSearchPage() { | |
const searchForm = document.getElementById('search-form'); | |
const questionForm = document.getElementById('question-form'); | |
const resultsContainer = document.getElementById('results-container'); | |
const loadingDiv = document.getElementById('loading'); | |
console.log('Main.js: initializeSearchPage called'); | |
console.log('Search form found:', !!searchForm); | |
console.log('Results container found:', !!resultsContainer); | |
if (!searchForm || !resultsContainer) { | |
console.log('Not on search page or missing elements'); | |
return; // Not on search page | |
} | |
console.log('Main.js: Initializing search page...'); | |
// Search form handler | |
searchForm.addEventListener('submit', function(e) { | |
e.preventDefault(); | |
console.log('Main.js: Search form submitted!'); | |
const query = document.getElementById('query').value; | |
const maxResults = parseInt(document.getElementById('max_results').value); | |
console.log('Main.js: Query:', query, 'Max results:', maxResults); | |
if (!query.trim()) { | |
console.error('Main.js: Empty query'); | |
showToast('Please enter a search query', 'warning'); | |
return; | |
} | |
console.log('Main.js: Starting search...'); | |
searchPapers(query, maxResults); | |
}); | |
// Question form handler (if exists) | |
if (questionForm) { | |
questionForm.addEventListener('submit', function(e) { | |
e.preventDefault(); | |
const question = document.getElementById('question').value; | |
if (!question.trim()) { | |
showToast('Please enter a question', 'warning'); | |
return; | |
} | |
askQuestion(question); | |
}); | |
} | |
function showLoading(type = 'search') { | |
if (!loadingDiv) return; | |
const loadingTitle = document.getElementById('loading-title'); | |
const loadingMessage = document.getElementById('loading-message'); | |
const loadingProgress = document.getElementById('loading-progress'); | |
loadingDiv.style.display = 'block'; | |
resultsContainer.innerHTML = ''; | |
if (loadingTitle) { | |
loadingTitle.textContent = type === 'search' ? 'Searching Research Papers...' : 'Processing Your Question...'; | |
} | |
if (loadingMessage) { | |
loadingMessage.innerHTML = '<i class="fas fa-info-circle me-1 text-info"></i>First search may take 30-60 seconds for initialization...'; | |
} | |
if (loadingProgress) { | |
loadingProgress.style.width = '10%'; | |
} | |
} | |
function hideLoading() { | |
if (loadingDiv) { | |
loadingDiv.style.display = 'none'; | |
} | |
} | |
function searchPapers(query, maxResults) { | |
console.log('Starting search for:', query); | |
showLoading('search'); | |
// Show helpful message about first-time search | |
const loadingMessage = document.getElementById('loading-message'); | |
const loadingProgress = document.getElementById('loading-progress'); | |
if (loadingMessage) { | |
loadingMessage.innerHTML = '<i class="fas fa-info-circle me-1 text-info"></i>Searching across multiple databases...'; | |
} | |
if (loadingProgress) { | |
loadingProgress.style.width = '10%'; | |
} | |
// Update progress periodically | |
let progressInterval = setInterval(() => { | |
if (loadingProgress) { | |
const currentWidth = parseInt(loadingProgress.style.width) || 10; | |
if (currentWidth < 80) { | |
loadingProgress.style.width = (currentWidth + 2) + '%'; | |
} | |
} | |
}, 1000); | |
// Set a timeout for very long searches | |
const timeoutId = setTimeout(() => { | |
if (loadingMessage) { | |
loadingMessage.innerHTML = '<i class="fas fa-exclamation-triangle me-1 text-warning"></i>Search is taking longer than expected. Please wait...'; | |
} | |
}, 30000); // 30 seconds | |
fetch('/api/search', { | |
method: 'POST', | |
headers: { | |
'Content-Type': 'application/json', | |
}, | |
body: JSON.stringify({ query: query, max_results: maxResults }) | |
}) | |
.then(response => { | |
clearTimeout(timeoutId); | |
clearInterval(progressInterval); | |
console.log('Search response status:', response.status); | |
return response.json(); | |
}) | |
.then(data => { | |
hideLoading(); | |
console.log('Search API response:', data); | |
console.log('Response success:', data.success); | |
console.log('Response papers:', data.papers); | |
if (data.success) { | |
displaySearchResults(data); | |
} else { | |
console.error('Search failed with error:', data.error); | |
displayError(data.error || 'Search failed'); | |
} | |
}) | |
.catch(error => { | |
clearTimeout(timeoutId); | |
clearInterval(progressInterval); | |
hideLoading(); | |
console.error('Search request failed:', error); | |
displayError('Network error: ' + error.message); | |
}); | |
} | |
function askQuestion(question) { | |
showLoading('question'); | |
fetch('/api/ask', { | |
method: 'POST', | |
headers: { | |
'Content-Type': 'application/json', | |
}, | |
body: JSON.stringify({ question: question }) | |
}) | |
.then(response => response.json()) | |
.then(data => { | |
hideLoading(); | |
if (data.success) { | |
displayQuestionAnswer(data); | |
} else { | |
displayError(data.error || 'Question failed'); | |
} | |
}) | |
.catch(error => { | |
hideLoading(); | |
displayError('Network error: ' + error.message); | |
}); | |
} | |
function displaySearchResults(data) { | |
console.log('displaySearchResults called with:', data); | |
const papers = data.papers || []; | |
console.log('Papers array:', papers); | |
console.log('Papers count:', papers.length); | |
// Simple fallback for testing | |
if (papers.length === 0) { | |
const html = ` | |
<div class="alert alert-info" role="alert"> | |
<i class="fas fa-info-circle me-2"></i> | |
No papers found for your search query "${data.query}". Try different keywords. | |
</div> | |
`; | |
resultsContainer.innerHTML = html; | |
return; | |
} | |
// Try to render papers with error handling | |
try { | |
const html = ` | |
<div class="card shadow-sm"> | |
<div class="card-header"> | |
<h5 class="mb-0"> | |
<i class="fas fa-search me-2"></i>Search Results | |
<span class="badge bg-primary ms-2">${data.count || papers.length} papers found</span> | |
</h5> | |
</div> | |
<div class="card-body"> | |
<div class="row"> | |
${papers.map((paper, index) => { | |
try { | |
const title = paper.title || 'Untitled'; | |
const authors = Array.isArray(paper.authors) ? paper.authors.join(', ') : (paper.authors || 'Unknown authors'); | |
const year = paper.year || paper.published_year || 'Unknown year'; | |
const source = paper.source || 'ArXiv'; | |
const abstract = paper.abstract ? (paper.abstract.length > 200 ? paper.abstract.substring(0, 200) + '...' : paper.abstract) : 'No abstract available'; | |
const url = paper.url || paper.arxiv_url || '#'; | |
return ` | |
<div class="col-md-6 mb-3"> | |
<div class="card h-100 shadow-sm result-item"> | |
<div class="card-body"> | |
<h6 class="card-title"> | |
<a href="${url}" target="_blank" class="text-decoration-none"> | |
${title} | |
</a> | |
</h6> | |
<p class="text-muted mb-2"> | |
<small> | |
<i class="fas fa-users me-1"></i> | |
${authors} | |
</small> | |
</p> | |
<p class="text-muted mb-2"> | |
<small> | |
<i class="fas fa-calendar me-1"></i> | |
${year} | |
<i class="fas fa-database ms-2 me-1"></i> | |
${source} | |
</small> | |
</p> | |
<p class="card-text"> | |
<small>${abstract}</small> | |
</p> | |
<div class="d-flex justify-content-between align-items-center"> | |
<a href="${url}" target="_blank" class="btn btn-sm btn-outline-primary"> | |
<i class="fas fa-external-link-alt me-1"></i>View Paper | |
</a> | |
${paper.pdf_url ? ` | |
<a href="${paper.pdf_url}" target="_blank" class="btn btn-sm btn-outline-danger"> | |
<i class="fas fa-file-pdf me-1"></i>PDF | |
</a> | |
` : ''} | |
</div> | |
</div> | |
</div> | |
</div> | |
`; | |
} catch (paperError) { | |
console.error('Error rendering paper', index, paperError); | |
return ` | |
<div class="col-md-6 mb-3"> | |
<div class="card h-100 shadow-sm result-item"> | |
<div class="card-body"> | |
<p class="text-danger">Error rendering paper ${index + 1}</p> | |
</div> | |
</div> | |
</div> | |
`; | |
} | |
}).join('')} | |
</div> | |
</div> | |
</div> | |
`; | |
console.log('Generated HTML length:', html.length); | |
console.log('Results container:', resultsContainer); | |
resultsContainer.innerHTML = html; | |
console.log('Results displayed, container content length:', resultsContainer.innerHTML.length); | |
// Show success toast | |
showToast(`Found ${papers.length} papers for "${data.query}"`, 'success', 3000); | |
} catch (error) { | |
console.error('Error rendering search results:', error); | |
resultsContainer.innerHTML = ` | |
<div class="alert alert-danger" role="alert"> | |
<i class="fas fa-exclamation-triangle me-2"></i> | |
Error rendering search results: ${error.message} | |
</div> | |
`; | |
} | |
} | |
function displayQuestionAnswer(data) { | |
// Deduplicate sources based on URL to avoid showing the same source multiple times | |
const uniqueSources = data.sources ? data.sources.filter((source, index, array) => | |
array.findIndex(s => s.url === source.url) === index | |
) : []; | |
const html = ` | |
<div class="card"> | |
<div class="card-header"> | |
<h5 class="mb-0"> | |
<i class="fas fa-question-circle me-2"></i>Question & Answer | |
</h5> | |
</div> | |
<div class="card-body"> | |
<div class="mb-3"> | |
<h6 class="text-primary">Question:</h6> | |
<p class="border-start border-primary ps-3">${data.question}</p> | |
</div> | |
<div class="mb-3"> | |
<h6 class="text-success">Answer:</h6> | |
<div class="border-start border-success ps-3"> | |
${marked.parse(data.answer)} | |
</div> | |
</div> | |
${uniqueSources.length > 0 ? ` | |
<div class="mb-3"> | |
<h6 class="text-info">Sources:</h6> | |
<div class="row"> | |
${uniqueSources.map(source => ` | |
<div class="col-md-6 mb-2"> | |
<div class="card border-info"> | |
<div class="card-body p-2"> | |
<h6 class="card-title small mb-1"> | |
<a href="${source.url}" target="_blank" class="text-decoration-none"> | |
${source.title} | |
</a> | |
</h6> | |
<p class="text-muted small mb-0"> | |
<i class="fas fa-users me-1"></i>${source.authors} | |
</p> | |
</div> | |
</div> | |
</div> | |
`).join('')} | |
</div> | |
</div> | |
` : ''} | |
</div> | |
</div> | |
`; | |
resultsContainer.innerHTML = html; | |
} | |
function displayError(message) { | |
const html = ` | |
<div class="alert alert-danger" role="alert"> | |
<i class="fas fa-exclamation-triangle me-2"></i> | |
<strong>Error:</strong> ${message} | |
</div> | |
`; | |
resultsContainer.innerHTML = html; | |
showToast('Search error: ' + message, 'danger', 5000); | |
} | |
console.log('Search page initialized successfully!'); | |
} | |
// Form validation | |
function validateForm(formElement) { | |
const requiredFields = formElement.querySelectorAll('[required]'); | |
let isValid = true; | |
requiredFields.forEach(field => { | |
if (!field.value.trim()) { | |
field.classList.add('is-invalid'); | |
isValid = false; | |
} else { | |
field.classList.remove('is-invalid'); | |
} | |
}); | |
return isValid; | |
} | |
// Loading states | |
function setLoadingState(element, isLoading) { | |
if (isLoading) { | |
element.disabled = true; | |
element.dataset.originalText = element.innerHTML; | |
element.innerHTML = '<i class="fas fa-spinner fa-spin me-2"></i>Loading...'; | |
} else { | |
element.disabled = false; | |
element.innerHTML = element.dataset.originalText || element.innerHTML; | |
} | |
} | |
// Error handling | |
function handleError(error, context = '') { | |
console.error(`Error in ${context}:`, error); | |
let message = 'An unexpected error occurred'; | |
if (error.message) { | |
message = error.message; | |
} | |
showToast(message, 'danger', 8000); | |
} | |
// Local storage helpers | |
function saveToLocalStorage(key, value) { | |
try { | |
localStorage.setItem(key, JSON.stringify(value)); | |
} catch (error) { | |
console.error('Failed to save to localStorage:', error); | |
} | |
} | |
function loadFromLocalStorage(key, defaultValue = null) { | |
try { | |
const value = localStorage.getItem(key); | |
return value ? JSON.parse(value) : defaultValue; | |
} catch (error) { | |
console.error('Failed to load from localStorage:', error); | |
return defaultValue; | |
} | |
} | |
// Enhanced logout function with security cleanup | |
function logout() { | |
// Clear all authentication data | |
clearAuthToken(); | |
// Clear all session data | |
sessionStorage.clear(); | |
// Clear specific localStorage items but keep non-sensitive data | |
const keysToRemove = ['authToken', 'userId', 'tokenTimestamp', 'userSession']; | |
keysToRemove.forEach(key => localStorage.removeItem(key)); | |
// Call logout API | |
fetch('/api/auth/logout', { | |
method: 'POST', | |
headers: { | |
'Content-Type': 'application/json', | |
} | |
}) | |
.then(() => { | |
// Redirect to login page | |
window.location.href = '/login'; | |
}) | |
.catch(() => { | |
// Even if API call fails, redirect to login | |
window.location.href = '/login'; | |
}); | |
} | |
// Make logout function globally available | |
window.logout = logout; | |
// Make makeAuthenticatedRequest globally available | |
window.makeAuthenticatedRequest = makeAuthenticatedRequest; | |
// Export functions for global use | |
window.ResearchMate = { | |
showToast, | |
formatDate, | |
formatNumber, | |
truncateText, | |
copyToClipboard, | |
downloadText, | |
debounce, | |
throttle, | |
apiRequest, | |
highlightSearchTerms, | |
validateForm, | |
setLoadingState, | |
handleError, | |
saveToLocalStorage, | |
loadFromLocalStorage, | |
saveUploadResults, | |
restoreUploadResults, | |
clearUploadResults, | |
displayUploadResults | |
}; | |
// Make clearUploadResults globally available for onclick handlers | |
window.clearUploadResults = clearUploadResults; | |
console.log('ResearchMate JavaScript loaded successfully with upload persistence!'); | |