// ────────────────────────────── static/script.js ────────────────────────────── (function() { // DOM elements const fileDropZone = document.getElementById('file-drop-zone'); const fileInput = document.getElementById('files'); const fileList = document.getElementById('file-list'); const fileItems = document.getElementById('file-items'); const uploadBtn = document.getElementById('upload-btn'); const uploadProgress = document.getElementById('upload-progress'); const progressStatus = document.getElementById('progress-status'); const progressFill = document.getElementById('progress-fill'); const progressLog = document.getElementById('progress-log'); const questionInput = document.getElementById('question'); const sendBtn = document.getElementById('send-btn'); const chatHint = document.getElementById('chat-hint'); const messages = document.getElementById('messages'); const reportLink = document.getElementById('report-link'); const searchLink = document.getElementById('search-link'); const loadingOverlay = document.getElementById('loading-overlay'); const loadingMessage = document.getElementById('loading-message'); // State let selectedFiles = []; let isUploading = false; let isProcessing = false; // Initialize init(); function init() { setupFileDropZone(); setupEventListeners(); checkUserAuth(); // Listen for project changes document.addEventListener('projectChanged', () => { updateUploadButton(); }); // Initial button state update updateUploadButton(); } function setupFileDropZone() { // Click to browse fileDropZone.addEventListener('click', () => fileInput.click()); // Drag and drop fileDropZone.addEventListener('dragover', (e) => { e.preventDefault(); fileDropZone.classList.add('dragover'); }); fileDropZone.addEventListener('dragleave', () => { fileDropZone.classList.remove('dragover'); }); fileDropZone.addEventListener('drop', (e) => { e.preventDefault(); fileDropZone.classList.remove('dragover'); const files = Array.from(e.dataTransfer.files); handleFileSelection(files); }); // File input change fileInput.addEventListener('change', (e) => { const files = Array.from(e.target.files); handleFileSelection(files); }); } function setupEventListeners() { // Upload form document.getElementById('upload-form').addEventListener('submit', handleUpload); // Chat sendBtn.addEventListener('click', handleAsk); // Convert to textarea behavior: Enter submits, Shift+Enter for newline questionInput.addEventListener('keydown', (e) => { if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); handleAsk(); } }); questionInput.addEventListener('input', autoGrowTextarea); // Clear chat history const clearBtn = document.getElementById('clear-chat-btn'); if (clearBtn) { clearBtn.addEventListener('click', async () => { const user = window.__sb_get_user(); const currentProject = window.__sb_get_current_project && window.__sb_get_current_project(); if (!user || !currentProject) return; if (!confirm('Clear chat history for this project?')) return; try { const res = await fetch(`/chat/history?user_id=${encodeURIComponent(user.user_id)}&project_id=${encodeURIComponent(currentProject.project_id)}`, { method: 'DELETE' }); if (res.ok) { document.getElementById('messages').innerHTML = ''; } else { alert('Failed to clear history'); } } catch {} }); } // Report link toggle if (reportLink) { reportLink.addEventListener('click', (e) => { e.preventDefault(); toggleReportMode(); }); } // Search link toggle (enables web search augmentation) if (searchLink) { searchLink.addEventListener('click', (e) => { e.preventDefault(); // Visual toggle; can be active concurrently with report mode searchLink.classList.toggle('active'); }); } } function handleFileSelection(files) { const validFiles = files.filter(file => { const isValid = file.type === 'application/pdf' || file.type === 'application/vnd.openxmlformats-officedocument.wordprocessingml.document'; if (!isValid) { alert(`Unsupported file type: ${file.name}. Please upload PDF or DOCX files only.`); } return isValid; }); if (validFiles.length === 0) return; selectedFiles = validFiles; updateFileList(); updateUploadButton(); } function updateFileList() { if (selectedFiles.length === 0) { fileList.style.display = 'none'; return; } fileList.style.display = 'block'; fileItems.innerHTML = ''; selectedFiles.forEach((file, index) => { const fileItem = document.createElement('div'); fileItem.className = 'file-item'; const icon = document.createElement('span'); icon.className = 'file-item-icon'; icon.textContent = file.type.includes('pdf') ? '📄' : '📝'; const name = document.createElement('span'); name.className = 'file-item-name'; name.textContent = file.name; const size = document.createElement('span'); size.className = 'file-item-size'; size.textContent = formatFileSize(file.size); const remove = document.createElement('button'); remove.className = 'file-item-remove'; remove.textContent = '×'; remove.title = 'Remove file'; remove.addEventListener('click', () => removeFile(index)); fileItem.appendChild(icon); fileItem.appendChild(name); fileItem.appendChild(size); fileItem.appendChild(remove); fileItems.appendChild(fileItem); }); } // Stored files view async function loadStoredFiles() { const user = window.__sb_get_user(); const currentProject = window.__sb_get_current_project && window.__sb_get_current_project(); if (!user || !currentProject) return; try { const res = await fetch(`/files?user_id=${encodeURIComponent(user.user_id)}&project_id=${encodeURIComponent(currentProject.project_id)}`); if (!res.ok) return; const data = await res.json(); const files = data.files || []; renderStoredFiles(files); // Enable Report button when at least one file exists if (reportLink) { // Disable visually by muted color when no files reportLink.style.pointerEvents = (files.length === 0) ? 'none' : 'auto'; reportLink.title = 'Generate report from selected document'; } window.__sb_current_filenames = new Set((data.filenames || []).map(f => (f || '').toLowerCase())); } catch {} } function renderStoredFiles(files) { const container = document.getElementById('stored-file-items'); if (container) { if (!files || files.length === 0) { container.innerHTML = '
No files stored yet.
'; } else { container.innerHTML = files.map(f => `
${f.filename}
`).join(' '); } } // Also render into Files page section const list = document.getElementById('files-list'); if (!list) return; if (!files || files.length === 0) { list.innerHTML = '
No files in this project.
'; return; } list.innerHTML = files.map((f, idx) => `
${f.filename}
${(f.summary || '').replace(/
`).join(''); // bind deletes list.querySelectorAll('.file-del').forEach(btn => { btn.addEventListener('click', async () => { const filename = decodeURIComponent(btn.getAttribute('data-fn')); if (!confirm(`Delete ${filename}? This will remove all related chunks.`)) return; const user = window.__sb_get_user(); const currentProject = window.__sb_get_current_project && window.__sb_get_current_project(); if (!user || !currentProject) return; try { const res = await fetch(`/files?user_id=${encodeURIComponent(user.user_id)}&project_id=${encodeURIComponent(currentProject.project_id)}&filename=${encodeURIComponent(filename)}`, { method: 'DELETE' }); if (res.ok) { await loadStoredFiles(); } else { alert('Failed to delete file'); } } catch {} }); }); // bind see more/less list.querySelectorAll('.see-more-btn').forEach(btn => { btn.addEventListener('click', () => { const idx = btn.getAttribute('data-idx'); const summary = document.getElementById(`file-summary-${idx}`); if (!summary) return; const expanded = summary.classList.toggle('expanded'); btn.textContent = expanded ? 'See less' : '… See more'; }); }); } // Expose show files section window.__sb_show_files_section = async () => { await loadStoredFiles(); }; // Duplicate detection: returns {toUpload, replace, renameMap} async function resolveDuplicates(files) { const existing = window.__sb_current_filenames || new Set(); const toUpload = []; const replace = []; const renameMap = {}; for (const f of files) { const name = f.name; if (existing.has(name.toLowerCase())) { const choice = await promptDuplicateChoice(name); if (choice === 'cancel') { // skip this file } else if (choice === 'replace') { replace.push(name); toUpload.push(f); } else if (choice && choice.startsWith('rename:')) { const newName = choice.slice(7); // create a new File with newName const renamed = new File([f], newName, { type: f.type, lastModified: f.lastModified }); renameMap[name] = newName; toUpload.push(renamed); } } else { toUpload.push(f); } } return { toUpload, replace, renameMap }; } function promptDuplicateChoice(filename) { return new Promise((resolve) => { // Minimal UX: use confirm/prompt; can be replaced with real modal later const msg = `A similar file named ${filename} already exists.\nChoose: [Cancel] to skip, [OK] to choose Replace or Rename.`; if (!confirm(msg)) { resolve('cancel'); return; } const answer = prompt('Type "replace" to overwrite, or enter a new filename to rename:', 'replace'); if (!answer) { resolve('cancel'); return; } if (answer.trim().toLowerCase() === 'replace') { resolve('replace'); return; } resolve('rename:' + answer.trim()); }); } function removeFile(index) { selectedFiles.splice(index, 1); updateFileList(); updateUploadButton(); } function updateUploadButton() { const hasFiles = selectedFiles.length > 0; const hasProject = window.__sb_get_current_project && window.__sb_get_current_project(); uploadBtn.disabled = !hasFiles || !hasProject || isUploading; if (hasFiles && hasProject) { uploadBtn.querySelector('.btn-text').textContent = `Upload ${selectedFiles.length} Document${selectedFiles.length > 1 ? 's' : ''}`; } else if (!hasProject) { uploadBtn.querySelector('.btn-text').textContent = 'Select a Project First'; } else { uploadBtn.querySelector('.btn-text').textContent = 'Upload Documents'; } } function formatFileSize(bytes) { if (bytes === 0) return '0 Bytes'; const k = 1024; const sizes = ['Bytes', 'KB', 'MB', 'GB']; const i = Math.floor(Math.log(bytes) / Math.log(k)); return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]; } async function handleUpload(e) { e.preventDefault(); if (selectedFiles.length === 0) { alert('Please select files to upload'); return; } const user = window.__sb_get_user(); if (!user) { alert('Please sign in to upload files'); window.__sb_show_auth_modal(); return; } const currentProject = window.__sb_get_current_project && window.__sb_get_current_project(); if (!currentProject) { alert('Please select a project first'); return; } isUploading = true; updateUploadButton(); showUploadProgress(); try { // Check duplicates against server list first await loadStoredFiles(); const { toUpload, replace, renameMap } = await resolveDuplicates(selectedFiles); if (toUpload.length === 0) { updateProgressStatus('No files to upload'); setTimeout(() => hideUploadProgress(), 1000); return; } const formData = new FormData(); formData.append('user_id', user.user_id); formData.append('project_id', currentProject.project_id); toUpload.forEach(file => formData.append('files', file)); if (replace.length > 0) { formData.append('replace_filenames', JSON.stringify(replace)); } if (Object.keys(renameMap).length > 0) { formData.append('rename_map', JSON.stringify(renameMap)); } const response = await fetch('/upload', { method: 'POST', body: formData }); const data = await response.json(); if (response.ok) { updateProgressStatus('Upload successful! Processing documents...'); updateProgressFill(0); // Friendly, non-technical messages only logProgress('Files uploaded successfully'); // Poll backend for real progress startUploadStatusPolling(data.job_id, data.total_files || toUpload.length); // Refresh stored files list after a short delay setTimeout(loadStoredFiles, 2000); } else { throw new Error(data.detail || 'Upload failed'); } } catch (error) { logProgress(`Error: ${error.message}`); updateProgressStatus('Upload failed'); setTimeout(() => hideUploadProgress(), 3000); } finally { isUploading = false; updateUploadButton(); } } function showUploadProgress() { uploadProgress.style.display = 'block'; updateProgressStatus('Uploading files... (DO NOT REFRESH)'); updateProgressFill(0); progressLog.innerHTML = ''; } function hideUploadProgress() { uploadProgress.style.display = 'none'; selectedFiles = []; updateFileList(); updateUploadButton(); } function updateProgressStatus(status) { progressStatus.textContent = status; } function updateProgressFill(percentage) { progressFill.style.width = `${percentage}%`; } function logProgress(message) { const timestamp = new Date().toLocaleTimeString(); progressLog.innerHTML += `[${timestamp}] ${message}\n`; progressLog.scrollTop = progressLog.scrollHeight; } function startUploadStatusPolling(jobId, totalFiles) { let stopped = false; let failCount = 0; const maxFailsBeforeSilentStop = 30; // ~36s at 1200ms const interval = setInterval(async () => { if (stopped) return; try { const res = await fetch(`/upload/status?job_id=${encodeURIComponent(jobId)}`); if (!res.ok) { failCount++; return; } const status = await res.json(); const percent = Math.max(0, Math.min(100, parseInt(status.percent || 0, 10))); const completed = status.completed || 0; const total = status.total || totalFiles || 1; updateProgressFill(percent); updateProgressStatus(percent >= 100 ? 'Finalizing...' : `Processing documents (${completed}/${total}) · ${percent}%`); if (status.status === 'completed' || percent >= 100) { clearInterval(interval); stopped = true; updateProgressFill(100); updateProgressStatus('Processing complete!'); logProgress('You can now start chatting with your documents'); setTimeout(() => hideUploadProgress(), 1500); enableChat(); // Final refresh of stored files setTimeout(loadStoredFiles, 1000); } } catch (e) { // Swallow transient errors; update a friendly spinner-like status failCount++; if (failCount >= maxFailsBeforeSilentStop) { clearInterval(interval); stopped = true; updateProgressStatus('Still working...'); } } }, 1200); } function enableChat() { questionInput.disabled = false; sendBtn.disabled = false; chatHint.style.display = 'none'; autoGrowTextarea(); } async function handleAsk() { const question = questionInput.value.trim(); if (!question) return; const user = window.__sb_get_user(); if (!user) { alert('Please sign in to chat'); window.__sb_show_auth_modal(); return; } const currentProject = window.__sb_get_current_project && window.__sb_get_current_project(); if (!currentProject) { alert('Please select a project first'); return; } // Add user message appendMessage('user', question); questionInput.value = ''; autoGrowTextarea(); // Save user message to chat history await saveChatMessage(user.user_id, currentProject.project_id, 'user', question); // Generate session ID for status tracking const sessionId = 'chat_' + Date.now() + '_' + Math.random().toString(36).substr(2, 9); // Add thinking message with dynamic status const thinkingMsg = appendMessage('thinking', 'Receiving request...'); // Disable input during processing questionInput.disabled = true; sendBtn.disabled = true; showButtonLoading(sendBtn, true); // Start status polling const statusInterval = startStatusPolling(sessionId, thinkingMsg); try { // Branch: if report mode is active → call /report with textarea as instructions if (isReportModeActive()) { const filename = pickActiveFilename(); if (!filename) throw new Error('Please select a document to generate a report'); const form = new FormData(); form.append('user_id', user.user_id); form.append('project_id', currentProject.project_id); form.append('filename', filename); form.append('outline_words', '200'); form.append('report_words', '1200'); form.append('instructions', question); form.append('session_id', sessionId); // If Search is toggled on, enable web augmentation for report const useWeb = searchLink && searchLink.classList.contains('active'); if (useWeb) { form.append('use_web', '1'); form.append('max_web', '20'); } const response = await fetch('/report', { method: 'POST', body: form }); const data = await response.json(); if (response.ok) { thinkingMsg.remove(); appendMessage('assistant', data.report_markdown || 'No report', true); // isReport = true if (data.sources && data.sources.length) appendSources(data.sources); // Save assistant report to chat history for persistence try { await saveChatMessage(user.user_id, currentProject.project_id, 'assistant', data.report_markdown || 'No report'); } catch {} } else { throw new Error(data.detail || 'Failed to generate report'); } } else { const formData = new FormData(); formData.append('user_id', user.user_id); formData.append('project_id', currentProject.project_id); formData.append('question', question); formData.append('k', '6'); formData.append('session_id', sessionId); // If Search is toggled on, enable web augmentation const useWeb = searchLink && searchLink.classList.contains('active'); if (useWeb) { formData.append('use_web', '1'); formData.append('max_web', '30'); } const response = await fetch('/chat', { method: 'POST', body: formData }); const data = await response.json(); if (response.ok) { thinkingMsg.remove(); appendMessage('assistant', data.answer || 'No answer received'); if (data.sources && data.sources.length > 0) { appendSources(data.sources); } await saveChatMessage( user.user_id, currentProject.project_id, 'assistant', data.answer || 'No answer received', (data.sources && data.sources.length > 0) ? data.sources : null ); } else { throw new Error(data.detail || 'Failed to get answer'); } } } catch (error) { thinkingMsg.remove(); const errorMsg = `⚠️ Error: ${error.message}`; appendMessage('assistant', errorMsg); await saveChatMessage(user.user_id, currentProject.project_id, 'assistant', errorMsg); } finally { // Stop status polling if (statusInterval) { clearInterval(statusInterval); } // Re-enable input questionInput.disabled = false; sendBtn.disabled = false; showButtonLoading(sendBtn, false); questionInput.focus(); } } function toggleReportMode() { if (!reportLink) return; reportLink.classList.toggle('active'); } function isReportModeActive() { return reportLink && reportLink.classList.contains('active'); } function pickActiveFilename() { const candidates = Array.from(document.querySelectorAll('#stored-file-items .pill')); let active = candidates.find(el => el.classList.contains('active')); if (!active && candidates.length) active = candidates[0]; return active ? active.textContent.trim() : ''; } function autoGrowTextarea() { if (!questionInput) return; // Reset height to measure content size questionInput.style.height = 'auto'; const style = window.getComputedStyle(questionInput); const borderTop = parseInt(style.borderTopWidth) || 0; const borderBottom = parseInt(style.borderBottomWidth) || 0; const paddingTop = parseInt(style.paddingTop) || 0; const paddingBottom = parseInt(style.paddingBottom) || 0; const boxExtras = borderTop + borderBottom + paddingTop + paddingBottom; const contentHeight = questionInput.scrollHeight - boxExtras; const lineHeight = 22; // approx for 16px font const minRows = 2; const maxRows = 7; // Compute rows required based on content height, clamped const neededRows = Math.ceil(contentHeight / lineHeight); const clamped = Math.min(maxRows, Math.max(minRows, neededRows)); questionInput.rows = clamped; // Prevent jumpy growth for long single lines by restricting until wrap actually occurs // If no wrap (scrollWidth <= clientWidth), keep at least minRows const isWrapping = questionInput.scrollWidth > questionInput.clientWidth; if (!isWrapping) questionInput.rows = Math.min(questionInput.rows, minRows); } async function handleGenerateReport() { const user = window.__sb_get_user(); const currentProject = window.__sb_get_current_project && window.__sb_get_current_project(); if (!user || !currentProject) { alert('Please sign in and select a project'); return; } // Determine selected/active file from files section; fallback to first const candidates = Array.from(document.querySelectorAll('#stored-file-items .pill')); let active = candidates.find(el => el.classList.contains('active')); if (!active && candidates.length) active = candidates[0]; if (!active) { alert('Please upload and select a document first'); return; } const filename = active.textContent.trim(); const instructions = (questionInput && questionInput.value || '').trim(); showLoading('Generating report...'); try { const form = new FormData(); form.append('user_id', user.user_id); form.append('project_id', currentProject.project_id); form.append('filename', filename); form.append('outline_words', '200'); form.append('report_words', '1200'); form.append('instructions', instructions); // Respect Search toggle when using quick report button const useWeb = searchLink && searchLink.classList.contains('active'); if (useWeb) { form.append('use_web', '1'); form.append('max_web', '20'); } const res = await fetch('/report', { method: 'POST', body: form }); const data = await res.json(); if (!res.ok) throw new Error(data.detail || 'Report failed'); // Append as assistant message appendMessage('assistant', data.report_markdown || 'No report'); if (data.sources && data.sources.length) { appendSources(data.sources); } } catch (e) { alert(e.message || 'Failed to generate report'); } finally { hideLoading(); } } // Toggle active file pill selection document.addEventListener('click', (e) => { const tgt = e.target; if (tgt && tgt.classList && tgt.classList.contains('pill') && tgt.parentElement && tgt.parentElement.id === 'stored-file-items') { document.querySelectorAll('#stored-file-items .pill').forEach(el => el.classList.remove('active')); tgt.classList.add('active'); // Enable link visually if (reportLink) reportLink.classList.add('active'); } }); async function saveChatMessage(userId, projectId, role, content, sources = null) { try { const formData = new FormData(); formData.append('user_id', userId); formData.append('project_id', projectId); formData.append('role', role); formData.append('content', content); formData.append('timestamp', Date.now() / 1000); if (sources) { try { formData.append('sources', JSON.stringify(sources)); } catch {} } await fetch('/chat/save', { method: 'POST', body: formData }); } catch (error) { console.error('Failed to save chat message:', error); } } function appendMessage(role, text, isReport = false) { const messageDiv = document.createElement('div'); messageDiv.className = `msg ${role}`; // Render Markdown for assistant messages if (role === 'assistant') { try { // Use marked library to convert Markdown to HTML const htmlContent = marked.parse(text); messageDiv.innerHTML = htmlContent; // Add copy buttons to code blocks addCopyButtonsToCodeBlocks(messageDiv); // Add download PDF button for reports if (isReport) { addDownloadPdfButton(messageDiv, text); } } catch (e) { // Fallback to plain text if Markdown parsing fails messageDiv.textContent = text; } } else { messageDiv.textContent = text; } messages.appendChild(messageDiv); // Scroll to bottom requestAnimationFrame(() => { messageDiv.scrollIntoView({ behavior: 'smooth', block: 'end' }); }); return messageDiv; } // Expose markdown-aware appenders for use after refresh (projects.js) window.appendMessage = appendMessage; window.appendSources = appendSources; function addCopyButtonsToCodeBlocks(messageDiv) { const codeBlocks = messageDiv.querySelectorAll('pre code'); codeBlocks.forEach((codeBlock, index) => { const pre = codeBlock.parentElement; const language = codeBlock.className.match(/language-(\w+)/)?.[1] || 'code'; // Create wrapper const wrapper = document.createElement('div'); wrapper.className = 'code-block-wrapper'; // Create header with language and copy button const header = document.createElement('div'); header.className = 'code-block-header'; header.innerHTML = ` ${language} `; // Create content wrapper const content = document.createElement('div'); content.className = 'code-block-content'; content.appendChild(codeBlock.cloneNode(true)); // Assemble wrapper wrapper.appendChild(header); wrapper.appendChild(content); // Replace original pre with wrapper pre.parentNode.replaceChild(wrapper, pre); // Add click handler for copy button const copyBtn = wrapper.querySelector('.copy-code-btn'); copyBtn.addEventListener('click', () => copyCodeToClipboard(codeBlock.textContent, copyBtn)); }); } function copyCodeToClipboard(code, button) { navigator.clipboard.writeText(code).then(() => { const originalText = button.innerHTML; button.innerHTML = ` Copied! `; button.classList.add('copied'); setTimeout(() => { button.innerHTML = originalText; button.classList.remove('copied'); }, 2000); }).catch(err => { console.error('Failed to copy code:', err); // Fallback for older browsers const textArea = document.createElement('textarea'); textArea.value = code; document.body.appendChild(textArea); textArea.select(); try { document.execCommand('copy'); button.innerHTML = ` Copied! `; button.classList.add('copied'); setTimeout(() => { button.innerHTML = ` Copy `; button.classList.remove('copied'); }, 2000); } catch (fallbackErr) { console.error('Fallback copy failed:', fallbackErr); } document.body.removeChild(textArea); }); } function addDownloadPdfButton(messageDiv, reportContent) { const downloadBtn = document.createElement('button'); downloadBtn.className = 'download-pdf-btn'; downloadBtn.innerHTML = ` Download PDF `; downloadBtn.addEventListener('click', () => downloadReportAsPdf(reportContent, downloadBtn)); messageDiv.appendChild(downloadBtn); } async function downloadReportAsPdf(reportContent, button) { const user = window.__sb_get_user(); const currentProject = window.__sb_get_current_project && window.__sb_get_current_project(); if (!user || !currentProject) { alert('Please sign in and select a project'); return; } button.disabled = true; button.innerHTML = ` Generating PDF... `; try { // Find sources from the current message or recent sources const sources = findCurrentSources(); const formData = new FormData(); formData.append('user_id', user.user_id); formData.append('project_id', currentProject.project_id); formData.append('report_content', reportContent); formData.append('sources', JSON.stringify(sources)); const response = await fetch('/report/pdf', { method: 'POST', body: formData }); if (response.ok) { const blob = await response.blob(); const url = window.URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = `report-${new Date().toISOString().split('T')[0]}.pdf`; document.body.appendChild(a); a.click(); window.URL.revokeObjectURL(url); document.body.removeChild(a); button.innerHTML = ` Downloaded! `; setTimeout(() => { button.innerHTML = ` Download PDF `; button.disabled = false; }, 2000); } else { throw new Error('Failed to generate PDF'); } } catch (error) { console.error('PDF generation failed:', error); alert('Failed to generate PDF. Please try again.'); button.innerHTML = ` Download PDF `; button.disabled = false; } } function appendSources(sources) { const sourcesDiv = document.createElement('div'); sourcesDiv.className = 'sources'; const sourcesList = sources.map(source => { const filename = source.filename || 'unknown'; const topic = source.topic_name ? ` • ${source.topic_name}` : ''; const pages = source.page_span ? ` [pp. ${source.page_span.join('-')}]` : ''; const score = source.score ? ` (${source.score.toFixed(2)})` : ''; return `${filename}${topic}${pages}${score}`; }).join(' '); sourcesDiv.innerHTML = `Sources: ${sourcesList}`; messages.appendChild(sourcesDiv); // Store sources for PDF generation window.__sb_current_sources = sources; requestAnimationFrame(() => { sourcesDiv.scrollIntoView({ behavior: 'smooth', block: 'end' }); }); } function findCurrentSources() { // Try to get sources from the current message context if (window.__sb_current_sources) { return window.__sb_current_sources; } // Fallback: look for sources in the last assistant message const lastAssistantMsg = Array.from(messages.children).reverse().find(msg => msg.classList.contains('msg') && msg.classList.contains('assistant') ); if (lastAssistantMsg) { const sourcesDiv = lastAssistantMsg.nextElementSibling; if (sourcesDiv && sourcesDiv.classList.contains('sources')) { // Extract sources from the DOM (this is a fallback) const pills = sourcesDiv.querySelectorAll('.pill'); const sources = Array.from(pills).map(pill => { const text = pill.textContent; const parts = text.split(' • '); return { filename: parts[0] || 'Unknown', topic_name: parts[1] || '', score: 0.0 }; }); return sources; } } return []; } function showButtonLoading(button, isLoading) { const textSpan = button.querySelector('.btn-text'); const loadingSpan = button.querySelector('.btn-loading'); // Handle buttons with only loading state (like send button) if (!textSpan && loadingSpan) { if (isLoading) { loadingSpan.style.display = 'inline-flex'; button.disabled = true; } else { loadingSpan.style.display = 'none'; button.disabled = false; } return; } // Handle buttons with both text and loading states (like upload button) if (textSpan && loadingSpan) { if (isLoading) { textSpan.style.display = 'none'; loadingSpan.style.display = 'inline-flex'; button.disabled = true; } else { textSpan.style.display = 'inline'; loadingSpan.style.display = 'none'; button.disabled = false; } return; } // Fallback for buttons without proper loading structure if (isLoading) { button.disabled = true; } else { button.disabled = false; } } function showLoading(message = 'Processing...') { loadingMessage.textContent = message; loadingOverlay.classList.remove('hidden'); } function hideLoading() { loadingOverlay.classList.add('hidden'); } function checkUserAuth() { const user = window.__sb_get_user(); if (user) { // Check if we have a current project const currentProject = window.__sb_get_current_project && window.__sb_get_current_project(); if (currentProject) { enableChat(); } } // Always update upload button state updateUploadButton(); } // Public API window.__sb_update_upload_button = updateUploadButton; window.__sb_enable_chat = enableChat; // Listen for project changes window.addEventListener('projectChanged', () => { updateUploadButton(); const currentProject = window.__sb_get_current_project && window.__sb_get_current_project(); if (currentProject) { enableChat(); } }); // Load stored files whenever project changes document.addEventListener('projectChanged', () => { loadStoredFiles(); }); // Expose to other scripts window.__sb_load_stored_files = loadStoredFiles; // Also attempt loading stored files after initial auth/project load window.addEventListener('load', () => { setTimeout(loadStoredFiles, 500); }); // Reveal animations const observer = new IntersectionObserver((entries) => { entries.forEach(entry => { if (entry.isIntersecting) { entry.target.classList.add('in'); observer.unobserve(entry.target); } }); }, { threshold: 0.1 }); document.querySelectorAll('.reveal').forEach(el => observer.observe(el)); // Status polling function for real-time updates function startStatusPolling(sessionId, thinkingMsg) { const isReportMode = isReportModeActive(); const statusEndpoint = isReportMode ? `/report/status/${sessionId}` : `/chat/status/${sessionId}`; const interval = setInterval(async () => { try { const response = await fetch(statusEndpoint); if (response.ok) { const status = await response.json(); updateThinkingMessage(thinkingMsg, status.message, status.progress); // Stop polling when complete or error if (status.status === 'complete' || status.status === 'error') { clearInterval(interval); } } } catch (error) { console.warn('Status polling failed:', error); } }, 500); // Poll every 500ms return interval; } function updateThinkingMessage(thinkingMsg, message, progress) { if (thinkingMsg && thinkingMsg.querySelector) { const progressBar = thinkingMsg.querySelector('.progress-bar'); const statusText = thinkingMsg.querySelector('.status-text'); if (statusText) { statusText.textContent = message; } if (progressBar && progress !== undefined) { progressBar.style.width = `${progress}%`; } } } // Enhanced thinking message with progress bar function appendMessage(role, text, isReport = false) { const messageDiv = document.createElement('div'); messageDiv.className = `msg ${role}`; if (role === 'thinking') { messageDiv.innerHTML = `
${text}
`; } else if (role === 'assistant') { // Render Markdown for assistant messages try { // Use marked library to convert Markdown to HTML const htmlContent = marked.parse(text); messageDiv.innerHTML = htmlContent; // Add copy buttons to code blocks addCopyButtonsToCodeBlocks(messageDiv); // Add download PDF button for reports if (isReport) { addDownloadPdfButton(messageDiv, text); } } catch (e) { // Fallback to plain text if Markdown parsing fails messageDiv.textContent = text; } } else { messageDiv.textContent = text; } messages.appendChild(messageDiv); // Scroll to bottom requestAnimationFrame(() => { messageDiv.scrollIntoView({ behavior: 'smooth', block: 'end' }); }); return messageDiv; } })();