“vinit5112”
add all files
5672ed8
// 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 ...
});