const API_BASE = ''; const SESSION_KEY = 'studyson:session_id'; function getSessionId() { let id = localStorage.getItem(SESSION_KEY); if (!id) { id = (crypto.randomUUID && crypto.randomUUID()) || `s_${Date.now()}_${Math.random().toString(36).slice(2)}`; localStorage.setItem(SESSION_KEY, id); } return id; } function renderMarkdown(text) { if (window.marked && window.DOMPurify) { const html = window.marked.parse(text, { breaks: true, gfm: true }); return window.DOMPurify.sanitize(html); } const div = document.createElement('div'); div.textContent = text; return div.innerHTML; } function escapeHtml(s) { const div = document.createElement('div'); div.textContent = s; return div.innerHTML; } const toastContainer = document.getElementById('toast-container'); function toast(message, type = 'info', duration = 4000) { if (!toastContainer) return; const el = document.createElement('div'); el.className = `toast toast-${type}`; el.textContent = message; toastContainer.appendChild(el); requestAnimationFrame(() => el.classList.add('show')); setTimeout(() => { el.classList.remove('show'); setTimeout(() => el.remove(), 300); }, duration); } const navItems = document.querySelectorAll('.nav-item'); const viewContainers = document.querySelectorAll('.view-container'); const currentViewName = document.getElementById('current-view-name'); const viewNames = { upload: 'Upload Files', web: 'Web Import', chat: 'Q&A Chat', summary: 'Summarize', }; navItems.forEach(item => { item.addEventListener('click', () => switchView(item.dataset.view)); }); function switchView(viewId) { navItems.forEach(n => n.classList.remove('active')); viewContainers.forEach(v => v.classList.remove('active')); document.querySelector(`[data-view="${viewId}"]`)?.classList.add('active'); document.getElementById(`view-${viewId}`)?.classList.add('active'); if (currentViewName) currentViewName.textContent = viewNames[viewId] || viewId; } const fileInput = document.getElementById('file-input'); const dropZone = document.querySelector('.drop-zone'); const dropZoneTitle = document.getElementById('drop-zone-title'); const fileInfo = document.getElementById('file-info'); const fileNameDisplay = document.getElementById('file-name-display'); const fileSizeDisplay = document.getElementById('file-size-display'); const uploadForm = document.getElementById('upload-form'); const uploadResult = document.getElementById('upload-result'); fileInput.addEventListener('change', e => handleFile(e.target.files[0])); ['dragover', 'dragenter'].forEach(ev => dropZone.addEventListener(ev, e => { e.preventDefault(); dropZone.classList.add('dragging'); }) ); ['dragleave', 'drop'].forEach(ev => dropZone.addEventListener(ev, e => { e.preventDefault(); dropZone.classList.remove('dragging'); }) ); dropZone.addEventListener('drop', e => { const file = e.dataTransfer?.files?.[0]; if (file) { const dt = new DataTransfer(); dt.items.add(file); fileInput.files = dt.files; handleFile(file); } }); function handleFile(file) { if (!file) return; fileNameDisplay.textContent = file.name; fileSizeDisplay.textContent = formatFileSize(file.size); dropZoneTitle.textContent = 'File selected'; fileInfo.style.display = 'flex'; } function formatFileSize(bytes) { if (!bytes) return '0 B'; const k = 1024; const sizes = ['B', 'KB', 'MB', 'GB']; const i = Math.floor(Math.log(bytes) / Math.log(k)); return Math.round((bytes / Math.pow(k, i)) * 100) / 100 + ' ' + sizes[i]; } uploadForm.addEventListener('submit', async e => { e.preventDefault(); const file = fileInput.files[0]; if (!file) { toast('Please select a file', 'error'); return; } const formData = new FormData(); formData.append('file', file); const submitBtn = uploadForm.querySelector('button[type="submit"]'); const originalText = submitBtn.innerHTML; submitBtn.disabled = true; submitBtn.innerHTML = 'Uploading...'; try { const response = await fetch(`${API_BASE}/upload`, { method: 'POST', body: formData }); const data = await response.json(); if (response.ok) { showResult(uploadResult, data.message, 'success'); toast(data.message, 'success'); fileInput.value = ''; fileInfo.style.display = 'none'; dropZoneTitle.textContent = 'Drop your file here'; await updateStatus(); } else { showResult(uploadResult, data.detail || 'Upload failed', 'error'); toast(data.detail || 'Upload failed', 'error'); } } catch (error) { showResult(uploadResult, `Error: ${error.message}`, 'error'); toast(error.message, 'error'); } finally { submitBtn.disabled = false; submitBtn.innerHTML = originalText; } }); const scrapeForm = document.getElementById('scrape-form'); const scrapeResult = document.getElementById('scrape-result'); scrapeForm.addEventListener('submit', async e => { e.preventDefault(); const url = document.getElementById('url-input').value; const submitBtn = scrapeForm.querySelector('button[type="submit"]'); const originalText = submitBtn.innerHTML; submitBtn.disabled = true; submitBtn.innerHTML = 'Fetching...'; try { const response = await fetch(`${API_BASE}/scrape_and_index`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ url, session_id: getSessionId() }), }); const data = await response.json(); if (response.ok) { showResult(scrapeResult, data.message, 'success'); toast(data.message, 'success'); document.getElementById('url-input').value = ''; await updateStatus(); } else { showResult(scrapeResult, data.detail || 'Scraping failed', 'error'); toast(data.detail || 'Scraping failed', 'error'); } } catch (error) { showResult(scrapeResult, `Error: ${error.message}`, 'error'); toast(error.message, 'error'); } finally { submitBtn.disabled = false; submitBtn.innerHTML = originalText; } }); const chatForm = document.getElementById('chat-form'); const chatMessages = document.getElementById('chat-messages'); const questionInput = document.getElementById('question-input'); chatForm.addEventListener('submit', async e => { e.preventDefault(); const question = questionInput.value.trim(); if (!question) return; chatMessages.querySelector('.empty-state')?.remove(); addMessage(question, 'user'); questionInput.value = ''; const assistantMessage = addMessage('', 'assistant'); const messageContent = assistantMessage.querySelector('.message-content'); messageContent.classList.add('markdown-body'); const submitBtn = chatForm.querySelector('button[type="submit"]'); submitBtn.disabled = true; try { const response = await fetch(`${API_BASE}/stream_query`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ question, session_id: getSessionId() }), }); if (!response.ok) { const error = await response.json(); messageContent.textContent = `Error: ${error.detail}`; return; } const reader = response.body.getReader(); const decoder = new TextDecoder(); let buffer = ''; let fullAnswer = ''; while (true) { const { done, value } = await reader.read(); if (done) break; buffer += decoder.decode(value, { stream: true }); const lines = buffer.split('\n'); buffer = lines.pop(); for (const line of lines) { if (!line.startsWith('data: ')) continue; const data = line.slice(6); if (data === '[DONE]') continue; try { const parsed = JSON.parse(data); if (parsed.token) { fullAnswer += parsed.token; messageContent.innerHTML = renderMarkdown(fullAnswer); chatMessages.scrollTop = chatMessages.scrollHeight; } else if (parsed.final_answer) { messageContent.innerHTML = renderMarkdown(parsed.final_answer); if (parsed.sources?.length) { const sourcesDiv = document.createElement('div'); sourcesDiv.className = 'message-sources'; sourcesDiv.innerHTML = 'Sources'; parsed.sources.forEach((source, idx) => { const item = document.createElement('div'); item.className = 'source-item'; const score = source.score != null ? ` (${source.score.toFixed(3)})` : ''; item.innerHTML = ` ${idx + 1}. ${escapeHtml(source.file_name)}${escapeHtml(score)}

${escapeHtml(source.text)}…

`; sourcesDiv.appendChild(item); }); assistantMessage.querySelector('.message-bubble').appendChild(sourcesDiv); } } else if (parsed.error) { messageContent.textContent = `Error: ${parsed.error}`; } } catch (err) { console.error('Parse error:', err); } } } } catch (error) { messageContent.textContent = `Error: ${error.message}`; } finally { submitBtn.disabled = false; } }); function addMessage(text, sender) { const messageDiv = document.createElement('div'); messageDiv.className = `message ${sender}`; const bubbleDiv = document.createElement('div'); bubbleDiv.className = 'message-bubble'; const contentDiv = document.createElement('div'); contentDiv.className = 'message-content'; contentDiv.textContent = text; bubbleDiv.appendChild(contentDiv); messageDiv.appendChild(bubbleDiv); chatMessages.appendChild(messageDiv); chatMessages.scrollTop = chatMessages.scrollHeight; return messageDiv; } const summarizeForm = document.getElementById('summarize-form'); const summaryResult = document.getElementById('summary-result'); const lengthSlider = document.getElementById('max-length'); const lengthDisplay = document.getElementById('length-display'); lengthSlider.addEventListener('input', e => { lengthDisplay.textContent = e.target.value; }); summarizeForm.addEventListener('submit', async e => { e.preventDefault(); const maxLength = parseInt(lengthSlider.value, 10); const submitBtn = summarizeForm.querySelector('button[type="submit"]'); const originalText = submitBtn.innerHTML; submitBtn.disabled = true; submitBtn.innerHTML = 'Generating...'; try { const response = await fetch(`${API_BASE}/summarize`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ max_length: maxLength }), }); const data = await response.json(); if (response.ok) { const sources = data.source_documents.map(escapeHtml).join(', '); summaryResult.innerHTML = `

Summary (${data.word_count} words)

${renderMarkdown(data.summary)}

Sources: ${sources}

`; summaryResult.classList.add('show', 'success'); } else { showResult(summaryResult, data.detail || 'Summarization failed', 'error'); toast(data.detail || 'Summarization failed', 'error'); } } catch (error) { showResult(summaryResult, `Error: ${error.message}`, 'error'); toast(error.message, 'error'); } finally { submitBtn.disabled = false; submitBtn.innerHTML = originalText; } }); const resetBtn = document.getElementById('reset-btn-sidebar'); resetBtn.addEventListener('click', async () => { if (!confirm('Reset all documents? This cannot be undone.')) return; try { const response = await fetch(`${API_BASE}/reset`, { method: 'POST' }); const data = await response.json(); if (response.ok) { localStorage.removeItem(SESSION_KEY); toast(data.message, 'success'); chatMessages.innerHTML = '
💬

Start a Conversation

Ask me anything about your indexed documents

'; await updateStatus(); } else { toast(data.detail || 'Reset failed', 'error'); } } catch (error) { toast(error.message, 'error'); } }); function showResult(element, message, type) { element.innerHTML = escapeHtml(message); element.className = `result-message show ${type}`; setTimeout(() => element.classList.remove('show'), 5000); } async function updateStatus() { try { const response = await fetch(`${API_BASE}/status`); const data = await response.json(); if (!data.details) return; const count = data.details.document_count || 0; document.getElementById('doc-count-sidebar')?.replaceChildren(document.createTextNode(count)); const statusPulse = document.getElementById('status-pulse'); const statusTextSidebar = document.getElementById('status-text-sidebar'); const statusTextTop = document.getElementById('status-text-top'); if (data.details.has_documents) { if (statusPulse) statusPulse.style.background = 'var(--success)'; if (statusTextSidebar) statusTextSidebar.textContent = 'Ready'; if (statusTextTop) statusTextTop.textContent = `Ready · ${data.details.model || ''}`; } else { if (statusPulse) statusPulse.style.background = 'var(--text-light)'; if (statusTextSidebar) statusTextSidebar.textContent = 'No Docs'; if (statusTextTop) statusTextTop.textContent = 'No Documents'; } } catch (error) { console.error('Status update failed:', error); const statusPulse = document.getElementById('status-pulse'); const statusTextTop = document.getElementById('status-text-top'); if (statusPulse) statusPulse.style.background = 'var(--danger)'; if (statusTextTop) statusTextTop.textContent = 'Connection Error'; } } getSessionId(); updateStatus(); setInterval(updateStatus, 30000);