// 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 = `Upload "${file.name}"`;
// Update upload area
uploadArea.innerHTML = `
File Selected
${file.name} (${formatFileSize(file.size)})
`;
}
}
// 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 = `
PDF Analysis Results
📄 Paper Information
${renderMarkdown(data.title || 'Unknown Title')}
Processed: ${data.processed_at ? new Date(data.processed_at).toLocaleString() : 'N/A'}
Text Length: ${data.text_length ? data.text_length.toLocaleString() : 'N/A'} characters
Analysis Complete
${data.savedAt ? `
Restored
` : ''}
📝 Abstract
${renderMarkdown(data.abstract || 'Abstract not found')}
Main Summary
${renderMarkdown(summary.summary || 'Summary not available')}
`;
}
}
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 = `