Ananthakr1shnan's picture
Updated files
c8fb59f
// 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!');