Spaces:
Running
Running
// DOM Elements | |
const uploadBox = document.getElementById('upload-box'); | |
const pdfUpload = document.getElementById('pdf-upload'); | |
const welcomeScreen = document.getElementById('welcome-screen'); | |
const chatContainer = document.getElementById('chat-container'); | |
const chatMessages = document.getElementById('chat-messages'); | |
const chatInput = document.getElementById('chat-input'); | |
const sendButton = document.getElementById('send-button'); | |
const searchToggle = document.getElementById('search-toggle'); | |
const modelSelect = document.getElementById('model-select'); | |
const sessionInfo = document.getElementById('session-info'); | |
const currentFileName = document.getElementById('current-file-name'); | |
const clearHistoryBtn = document.getElementById('clear-history'); | |
const removePdfBtn = document.getElementById('remove-pdf'); | |
const newChatBtn = document.getElementById('new-chat'); | |
const loadingOverlay = document.getElementById('loading-overlay'); | |
const loadingText = document.getElementById('loading-text'); | |
const getStartedBtn = document.getElementById('get-started-btn'); | |
const contextContent = document.getElementById('context-content'); | |
const contextSidebar = document.getElementById('context-sidebar'); | |
const toggleContextBtn = document.getElementById('toggle-context'); | |
const menuToggle = document.getElementById('menu-toggle'); | |
const sidebar = document.querySelector('.sidebar'); | |
// App state | |
let currentSessionId = null; | |
let lastContextData = null; | |
let isMobile = window.innerWidth <= 768; | |
// Event listeners | |
uploadBox.addEventListener('click', () => pdfUpload.click()); | |
pdfUpload.addEventListener('change', handleFileUpload); | |
sendButton.addEventListener('click', sendMessage); | |
chatInput.addEventListener('keydown', (e) => { | |
if (e.key === 'Enter' && !e.shiftKey) { | |
e.preventDefault(); | |
sendMessage(); | |
} | |
}); | |
chatInput.addEventListener('input', () => { | |
sendButton.disabled = chatInput.value.trim() === ''; | |
}); | |
clearHistoryBtn.addEventListener('click', clearChatHistory); | |
removePdfBtn.addEventListener('click', removePdf); | |
newChatBtn.addEventListener('click', resetApp); | |
getStartedBtn.addEventListener('click', () => { | |
uploadBox.click(); | |
}); | |
toggleContextBtn.addEventListener('click', toggleContextSidebar); | |
// Mobile menu event listeners | |
if (menuToggle) { | |
menuToggle.addEventListener('click', () => { | |
sidebar.classList.toggle('show'); | |
}); | |
} | |
// Handle window resize | |
window.addEventListener('resize', () => { | |
const wasMobile = isMobile; | |
isMobile = window.innerWidth <= 768; | |
// If we're transitioning between mobile/desktop | |
if (wasMobile !== isMobile) { | |
updateMobileUI(); | |
} | |
}); | |
// Initialize the app | |
initializeApp(); | |
// Functions | |
function initializeApp() { | |
updateMobileUI(); | |
// Check if the page was refreshed | |
const pageWasRefreshed = ( | |
window.performance && | |
window.performance.navigation && | |
window.performance.navigation.type === 1 | |
) || document.referrer === document.location.href; | |
// If page was refreshed, clear any saved session | |
if (pageWasRefreshed) { | |
localStorage.removeItem('pdf_insight_session'); | |
return; | |
} | |
// Check if we have a session in localStorage | |
const savedSession = localStorage.getItem('pdf_insight_session'); | |
if (savedSession) { | |
try { | |
const session = JSON.parse(savedSession); | |
currentSessionId = session.id; | |
currentFileName.textContent = session.fileName; | |
// Show chat interface | |
welcomeScreen.classList.add('hidden'); | |
chatContainer.classList.remove('hidden'); | |
sessionInfo.classList.remove('hidden'); | |
// Load chat history | |
fetchChatHistory(); | |
} catch (e) { | |
console.error('Failed to load saved session:', e); | |
localStorage.removeItem('pdf_insight_session'); | |
} | |
} | |
} | |
// Update UI based on mobile/desktop view | |
function updateMobileUI() { | |
if (isMobile) { | |
if (menuToggle) menuToggle.classList.remove('hidden'); | |
contextSidebar.classList.add('collapsed'); | |
} else { | |
if (menuToggle) menuToggle.classList.add('hidden'); | |
sidebar.classList.remove('show'); | |
} | |
} | |
async function handleFileUpload(e) { | |
const file = e.target.files[0]; | |
if (!file) return; | |
try { | |
// Show loading overlay | |
showLoading('Processing Document...'); | |
const formData = new FormData(); | |
formData.append('file', file); | |
formData.append('model_name', modelSelect.value); | |
const response = await fetch('/upload-pdf', { | |
method: 'POST', | |
body: formData | |
}); | |
const data = await response.json(); | |
if (data.status === 'success') { | |
currentSessionId = data.session_id; | |
currentFileName.textContent = file.name; | |
// Save session to localStorage | |
localStorage.setItem('pdf_insight_session', JSON.stringify({ | |
id: currentSessionId, | |
fileName: file.name | |
})); | |
// Show chat interface | |
welcomeScreen.classList.add('hidden'); | |
chatContainer.classList.remove('hidden'); | |
sessionInfo.classList.remove('hidden'); | |
// Reset chat history view | |
chatMessages.innerHTML = ` | |
<div class="system-message"> | |
<p>Upload successful! You can now ask questions about "${file.name}".</p> | |
</div> | |
`; | |
// Enable input | |
chatInput.disabled = false; | |
chatInput.placeholder = 'Ask a question about the document...'; | |
// Close sidebar on mobile after uploading | |
if (isMobile) { | |
sidebar.classList.remove('show'); | |
} | |
} else { | |
// Enhanced error display | |
let errorDetails = ''; | |
if (data.detail) { | |
errorDetails = data.detail; | |
} | |
if (data.type) { | |
errorDetails = `${data.type}: ${errorDetails}`; | |
} | |
showError('Error: ' + (errorDetails || 'Failed to process document')); | |
// Add a more detailed error in the chat area if we're already in chat mode | |
if (!welcomeScreen.classList.contains('hidden')) { | |
const errorMessageDiv = document.createElement('div'); | |
errorMessageDiv.className = 'system-message error'; | |
errorMessageDiv.innerHTML = ` | |
<p><strong>Error Processing Document</strong></p> | |
<p>${errorDetails}</p> | |
<p>Please make sure you have set up all required API keys in the .env file.</p> | |
`; | |
chatMessages.appendChild(errorMessageDiv); | |
} | |
} | |
} catch (error) { | |
console.error('Error uploading file:', error); | |
showError('Failed to upload document. Please try again.'); | |
} finally { | |
hideLoading(); | |
} | |
} | |
async function sendMessage() { | |
const query = chatInput.value.trim(); | |
if (!query || !currentSessionId) return; | |
// Disable input and show typing indicator | |
chatInput.disabled = true; | |
sendButton.disabled = true; | |
// Add user message to chat | |
const userMessageElement = createMessageElement('user', query); | |
chatMessages.appendChild(userMessageElement); | |
chatMessages.scrollTop = chatMessages.scrollHeight; | |
// Clear input | |
chatInput.value = ''; | |
try { | |
// Show loading state | |
const typingIndicator = createTypingIndicator(); | |
chatMessages.appendChild(typingIndicator); | |
chatMessages.scrollTop = chatMessages.scrollHeight; | |
const response = await fetch('/chat', { | |
method: 'POST', | |
headers: { | |
'Content-Type': 'application/json' | |
}, | |
body: JSON.stringify({ | |
session_id: currentSessionId, | |
query: query, | |
use_search: searchToggle.checked, | |
model_name: modelSelect.value | |
}) | |
}); | |
const data = await response.json(); | |
// Remove typing indicator | |
chatMessages.removeChild(typingIndicator); | |
if (data.status === 'success') { | |
// Add assistant message | |
const assistantMessageElement = createMessageElement('assistant', data.answer); | |
chatMessages.appendChild(assistantMessageElement); | |
chatMessages.scrollTop = chatMessages.scrollHeight; | |
// Apply syntax highlighting to code blocks | |
applyCodeHighlighting(); | |
// Update context sidebar | |
updateContextSidebar(data.context_used); | |
lastContextData = data.context_used; | |
} else { | |
showError('Failed to get response: ' + data.detail); | |
} | |
} catch (error) { | |
console.error('Error sending message:', error); | |
showError('Failed to get response. Please try again.'); | |
// Remove typing indicator if it exists | |
const indicator = document.querySelector('.typing-indicator'); | |
if (indicator) { | |
chatMessages.removeChild(indicator); | |
} | |
} finally { | |
// Re-enable input | |
chatInput.disabled = false; | |
chatInput.focus(); | |
} | |
} | |
async function fetchChatHistory() { | |
if (!currentSessionId) return; | |
try { | |
showLoading('Loading chat history...'); | |
const response = await fetch('/chat-history', { | |
method: 'POST', | |
headers: { | |
'Content-Type': 'application/json' | |
}, | |
body: JSON.stringify({ | |
session_id: currentSessionId | |
}) | |
}); | |
const data = await response.json(); | |
if (data.status === 'success' && data.history.length > 0) { | |
// Clear chat messages and add system message | |
chatMessages.innerHTML = ` | |
<div class="system-message"> | |
<p>Continuing your conversation about "${currentFileName.textContent}".</p> | |
</div> | |
`; | |
// Add all messages from history | |
data.history.forEach(item => { | |
const userMessage = createMessageElement('user', item.user); | |
const assistantMessage = createMessageElement('assistant', item.assistant); | |
chatMessages.appendChild(userMessage); | |
chatMessages.appendChild(assistantMessage); | |
}); | |
// Apply syntax highlighting to code blocks | |
applyCodeHighlighting(); | |
// Scroll to bottom | |
chatMessages.scrollTop = chatMessages.scrollHeight; | |
} | |
} catch (error) { | |
console.error('Error fetching chat history:', error); | |
showError('Failed to load chat history.'); | |
} finally { | |
hideLoading(); | |
} | |
} | |
async function clearChatHistory() { | |
if (!currentSessionId) return; | |
try { | |
showLoading('Clearing chat history...'); | |
const response = await fetch('/clear-history', { | |
method: 'POST', | |
headers: { | |
'Content-Type': 'application/json' | |
}, | |
body: JSON.stringify({ | |
session_id: currentSessionId | |
}) | |
}); | |
const data = await response.json(); | |
if (data.status === 'success') { | |
// Reset chat messages | |
chatMessages.innerHTML = ` | |
<div class="system-message"> | |
<p>Chat history cleared. You can continue asking questions about "${currentFileName.textContent}".</p> | |
</div> | |
`; | |
// Reset context | |
contextContent.innerHTML = '<p class="no-context">No context available yet. Ask a question first.</p>'; | |
lastContextData = null; | |
} else { | |
showError('Failed to clear chat history: ' + data.detail); | |
} | |
} catch (error) { | |
console.error('Error clearing chat history:', error); | |
showError('Failed to clear chat history.'); | |
} finally { | |
hideLoading(); | |
} | |
} | |
async function removePdf() { | |
if (!currentSessionId) return; | |
try { | |
showLoading('Removing PDF from the system...'); | |
const response = await fetch('/remove-pdf', { | |
method: 'POST', | |
headers: { | |
'Content-Type': 'application/json' | |
}, | |
body: JSON.stringify({ | |
session_id: currentSessionId | |
}) | |
}); | |
const data = await response.json(); | |
if (data.status === 'success') { | |
// Reset the app | |
resetApp(); | |
showError('PDF file has been removed from the system.'); | |
} else { | |
showError('Failed to remove PDF: ' + data.detail); | |
} | |
} catch (error) { | |
console.error('Error removing PDF:', error); | |
showError('Failed to remove PDF. Please try again.'); | |
} finally { | |
hideLoading(); | |
} | |
} | |
function resetApp() { | |
// Clear current session | |
currentSessionId = null; | |
lastContextData = null; | |
localStorage.removeItem('pdf_insight_session'); | |
// Reset UI | |
welcomeScreen.classList.remove('hidden'); | |
chatContainer.classList.add('hidden'); | |
sessionInfo.classList.add('hidden'); | |
// Reset file input | |
pdfUpload.value = ''; | |
// Reset chat messages | |
chatMessages.innerHTML = ` | |
<div class="system-message"> | |
<p>Upload successful! You can now ask questions about the document.</p> | |
</div> | |
`; | |
// Reset context sidebar | |
contextContent.innerHTML = '<p class="no-context">No context available yet. Ask a question first.</p>'; | |
} | |
function createMessageElement(type, content) { | |
const div = document.createElement('div'); | |
div.className = `message ${type}-message`; | |
// For user messages, just escape HTML | |
if (type === 'user') { | |
div.innerHTML = ` | |
<div class="message-content">${escapeHTML(content)}</div> | |
<div class="message-timestamp">${formatTimestamp(new Date())}</div> | |
`; | |
} | |
// For assistant messages, render with Markdown | |
else { | |
// Configure marked.js options | |
marked.setOptions({ | |
breaks: true, // Add <br> on single line breaks | |
gfm: true, // GitHub Flavored Markdown | |
sanitize: false // Allow HTML in the input | |
}); | |
// Process the content with marked | |
const renderedContent = marked.parse(content); | |
div.innerHTML = ` | |
<div class="message-content">${renderedContent}</div> | |
<div class="message-timestamp">${formatTimestamp(new Date())}</div> | |
`; | |
} | |
return div; | |
} | |
function createTypingIndicator() { | |
const div = document.createElement('div'); | |
div.className = 'message assistant-message typing-indicator'; | |
div.innerHTML = ` | |
<div class="typing-animation"> | |
<span class="dot"></span> | |
<span class="dot"></span> | |
<span class="dot"></span> | |
</div> | |
`; | |
return div; | |
} | |
function updateContextSidebar(contextData) { | |
if (!contextData || contextData.length === 0) { | |
contextContent.innerHTML = '<p class="no-context">No context available for this response.</p>'; | |
return; | |
} | |
contextContent.innerHTML = ''; | |
contextData.forEach((item, index) => { | |
const contextItem = document.createElement('div'); | |
contextItem.className = 'context-item'; | |
const score = Math.round((1 - item.score) * 100); // Convert distance to similarity score | |
contextItem.innerHTML = ` | |
<span class="context-score">Relevance: ${score}%</span> | |
<div class="context-text">${truncateText(item.text, 300)}</div> | |
`; | |
contextContent.appendChild(contextItem); | |
}); | |
} | |
function toggleContextSidebar() { | |
contextSidebar.classList.toggle('collapsed'); | |
// Update icon | |
const icon = toggleContextBtn.querySelector('i'); | |
if (contextSidebar.classList.contains('collapsed')) { | |
icon.className = 'fas fa-angle-left'; | |
} else { | |
icon.className = 'fas fa-angle-right'; | |
} | |
} | |
function applyCodeHighlighting() { | |
// Find all code blocks in the assistant messages | |
document.querySelectorAll('.assistant-message pre code').forEach(block => { | |
hljs.highlightElement(block); | |
}); | |
} | |
function showLoading(message) { | |
loadingText.textContent = message || 'Loading...'; | |
loadingOverlay.classList.remove('hidden'); | |
} | |
function hideLoading() { | |
loadingOverlay.classList.add('hidden'); | |
} | |
function showError(message) { | |
// Add error message to chat | |
const errorDiv = document.createElement('div'); | |
errorDiv.className = 'system-message error'; | |
errorDiv.innerHTML = `<p>${message}</p>`; | |
chatMessages.appendChild(errorDiv); | |
chatMessages.scrollTop = chatMessages.scrollHeight; | |
// Also show an alert for critical errors | |
if (message.includes('API_KEY') || message.includes('environment variables')) { | |
alert('Configuration Error: ' + message); | |
} | |
// Remove after 10 seconds | |
setTimeout(() => { | |
if (errorDiv.parentNode === chatMessages) { | |
chatMessages.removeChild(errorDiv); | |
} | |
}, 10000); | |
} | |
// Helper functions | |
function formatTimestamp(date) { | |
return date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }); | |
} | |
function truncateText(text, maxLength) { | |
if (text.length <= maxLength) return text; | |
return text.substring(0, maxLength) + '...'; | |
} | |
function escapeHTML(text) { | |
const div = document.createElement('div'); | |
div.textContent = text; | |
return div.innerHTML; | |
} |