document.addEventListener('DOMContentLoaded', () => { const uploadBtn = document.getElementById('upload-btn'); const indexBtn = document.getElementById('index-btn'); const sendBtn = document.getElementById('send-btn'); const promptInput = document.getElementById('prompt-input'); const chatContainer = document.getElementById('chat-container'); const topKInput = document.getElementById('top-k'); const statusMsg = document.getElementById('index-status'); // Auto-resize textarea promptInput.addEventListener('input', function () { this.style.height = 'auto'; this.style.height = (this.scrollHeight) + 'px'; if (this.value.trim().length > 0) { sendBtn.removeAttribute('disabled'); } else { sendBtn.setAttribute('disabled', 'true'); } }); promptInput.addEventListener('keydown', (e) => { if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); handleSend(); } }); sendBtn.addEventListener('click', handleSend); async function fetchDocuments() { const docList = document.getElementById('documents-list'); try { const res = await fetch('/api/documents'); const data = await res.json(); docList.innerHTML = ''; if (data.documents.length === 0) { docList.innerHTML = '
No documents uploaded.
'; return; } data.documents.forEach(doc => { const card = document.createElement('div'); card.className = 'document-card'; const sizeKb = (doc.size / 1024).toFixed(1); let iconHtml = ''; if (doc.thumbnail) { iconHtml = `${doc.name}`; } else { const iconClass = doc.type === 'pdf' ? 'icon-pdf' : doc.type === 'docx' ? 'icon-docx' : doc.type === 'txt' ? 'icon-txt' : 'icon-default'; iconHtml = `
${doc.type.toUpperCase()}
`; } card.innerHTML = ` ${iconHtml}
${doc.name}
${sizeKb} KB
`; // Add click handler to open embeddings viewer card.style.cursor = 'pointer'; card.addEventListener('click', () => openChunksModal(doc.name)); docList.appendChild(card); }); } catch (e) { console.error("Failed to load documents", e); } } // Call once on load fetchDocuments(); fetchStats(); let isIndexing = false; let pollInterval = null; const overlay = document.getElementById('indexing-overlay'); const overlayText = document.getElementById('overlay-text'); async function checkIndexStatus() { try { const res = await fetch('/api/index/status'); const data = await res.json(); if (data.is_indexing) { if (data.progress) { overlayText.textContent = data.progress; } if (!isIndexing) { isIndexing = true; statusMsg.innerHTML = ' Indexing in progress...'; statusMsg.style.color = 'var(--brand-color)'; indexBtn.disabled = true; uploadBtn.disabled = true; overlay.style.display = 'flex'; if (!pollInterval) { pollInterval = setInterval(checkIndexStatus, 2000); } } } else { if (isIndexing) { isIndexing = false; statusMsg.textContent = 'Ready.'; statusMsg.style.color = 'var(--text-secondary)'; indexBtn.disabled = false; uploadBtn.disabled = false; overlay.style.display = 'none'; if (pollInterval) { clearInterval(pollInterval); pollInterval = null; // Wait a tiny bit and refresh so that thumbnails appear setTimeout(fetchDocuments, 1000); } } } } catch (e) { console.error("Failed to check index status", e); } } // Start polling if we are currently indexing checkIndexStatus(); uploadBtn.addEventListener('click', async () => { const fileInput = document.getElementById('file-upload'); if (fileInput.files.length === 0) return alert('Select files first.'); const formData = new FormData(); for (const file of fileInput.files) { formData.append('files', file); } uploadBtn.textContent = 'Uploading...'; uploadBtn.disabled = true; try { const response = await fetch('/api/upload', { method: 'POST', body: formData }); const result = await response.json(); alert(result.message); fileInput.value = ""; // Refresh documents list fetchDocuments(); // Force status check for auto-indexing checkIndexStatus(); } catch (error) { console.error(error); alert('Upload failed.'); } finally { uploadBtn.textContent = 'Upload'; if (!isIndexing) uploadBtn.disabled = false; } }); indexBtn.addEventListener('click', async () => { indexBtn.textContent = 'Starting...'; indexBtn.disabled = true; statusMsg.innerHTML = ' Triggering index...'; try { const response = await fetch('/api/index', { method: 'POST' }); const result = await response.json(); console.log(result.message); checkIndexStatus(); } catch (error) { console.error(error); statusMsg.textContent = "Error triggering indexing."; indexBtn.disabled = false; } finally { indexBtn.textContent = 'Re-index All'; } }); const clearBtn = document.getElementById('clear-btn'); clearBtn.addEventListener('click', async () => { if (!confirm('Are you sure you want to clear ALL indexed data? This action cannot be undone.')) { return; } clearBtn.textContent = 'Clearing...'; clearBtn.disabled = true; try { const response = await fetch('/api/index/clear', { method: 'POST' }); const result = await response.json(); alert(result.message); fetchDocuments(); fetchStats(); } catch (error) { console.error(error); alert('Failed to clear database.'); } finally { clearBtn.textContent = 'Clear All Data'; clearBtn.disabled = false; } }); async function fetchStats() { try { const response = await fetch('/api/stats'); const data = await response.json(); document.getElementById('stat-embed').textContent = data.tokens.embedding_tokens.toLocaleString(); document.getElementById('stat-prompt').textContent = data.tokens.prompt_tokens.toLocaleString(); document.getElementById('stat-response').textContent = data.tokens.completion_tokens.toLocaleString(); document.getElementById('stat-toc').textContent = (data.tokens.toc_analysis_tokens || 0).toLocaleString(); document.getElementById('stat-total').textContent = data.total_tokens.toLocaleString(); } catch (error) { console.error('Error fetching stats:', error); } } function cleanMarkdown(text) { return marked.parse(text); } function createResponseCard(title) { const card = document.createElement('div'); card.className = 'response-card'; card.innerHTML = `
${title}
`; return card; } async function handleSend() { const query = promptInput.value.trim(); if (!query) return; promptInput.value = ''; promptInput.style.height = 'auto'; sendBtn.disabled = true; // Create User Message const userMsg = document.createElement('div'); userMsg.className = 'message user'; userMsg.innerHTML = `
${query}
`; chatContainer.appendChild(userMsg); // Create Container for Responses const sysMsg = document.createElement('div'); sysMsg.className = 'message system'; const responsesGrid = document.createElement('div'); responsesGrid.className = 'system-responses'; // Create Cards const vectorCard = createResponseCard('Vector RAG'); const pageCard = createResponseCard('Page Index RAG'); const tocCard = createResponseCard('TOC Index RAG'); responsesGrid.appendChild(vectorCard); responsesGrid.appendChild(pageCard); responsesGrid.appendChild(tocCard); sysMsg.appendChild(responsesGrid); chatContainer.appendChild(sysMsg); chatContainer.scrollTop = chatContainer.scrollHeight; const topK = parseInt(topKInput.value) || 5; // Trigger all three SSE calls fetchSSE('/api/chat/vector', { query, top_k: topK }, vectorCard); fetchSSE('/api/chat/page', { query, top_k: topK }, pageCard); fetchSSE('/api/chat/toc', { query, top_k: topK }, tocCard); } async function fetchSSE(url, payload, cardElement) { const contentDiv = cardElement.querySelector('.response-content'); const tokenDisplay = cardElement.querySelector('.prompt-tokens'); const sourcesBtn = cardElement.querySelector('.sources-toggle'); const sourcesList = cardElement.querySelector('.sources-list'); sourcesBtn.addEventListener('click', () => { if (sourcesList.style.display === 'none') { sourcesList.style.display = 'block'; sourcesBtn.textContent = 'Hide Sources ▲'; } else { sourcesList.style.display = 'none'; sourcesBtn.textContent = 'View Sources ▼'; } }); try { const response = await fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload) }); const reader = response.body.getReader(); const decoder = new TextDecoder("utf-8"); let fullText = ""; let buffer = ""; while (true) { const { done, value } = await reader.read(); if (done) break; buffer += decoder.decode(value, { stream: true }); const parts = buffer.split('\n\n'); buffer = parts.pop(); for (const part of parts) { if (part.startsWith('data: ')) { try { const dataStr = part.substring(6); if (!dataStr.trim()) continue; const data = JSON.parse(dataStr); if (data.type === 'token') { fullText += data.content; contentDiv.innerHTML = cleanMarkdown(fullText) + ''; } else if (data.type === 'sources') { // Render sources let sourcesHtml = ''; data.sources.forEach((s, idx) => { let label = s.chunk_index !== undefined ? `Chunk #${s.chunk_index}` : s.node_id !== undefined ? `Node ${s.node_id}: ${s.title || ''}` : `Page ${s.page_num}`; sourcesHtml += `
[${idx + 1}] ${s.source} — ${label} (score: ${s.score}) ${s.text.substring(0, 150)}...
`; }); sourcesList.innerHTML = sourcesHtml || '
No sources found.
'; } else if (data.type === 'stats') { const promptBadge = cardElement.querySelector('.prompt-badge'); const completionBadge = cardElement.querySelector('.completion-badge'); if (promptBadge) { promptBadge.textContent = 'Prompt: ' + data.prompt_eval_count; promptBadge.style.display = 'inline-block'; } if (completionBadge) { completionBadge.textContent = 'Response: ' + data.eval_count; completionBadge.style.display = 'inline-block'; } fetchStats(); } else if (data.type === 'error') { fullText += `\n\n**Error:** ${data.content}`; contentDiv.innerHTML = cleanMarkdown(fullText); } } catch (e) { console.error('JSON parse error:', e, part); } } } chatContainer.scrollTop = chatContainer.scrollHeight; } // Remove typing indicator at the end contentDiv.innerHTML = cleanMarkdown(fullText); } catch (error) { contentDiv.innerHTML = "Connection error parsing stream."; console.error(error); } } // Modal Logic const chunksModal = document.getElementById('chunks-modal'); const closeModalBtn = document.getElementById('close-modal-btn'); const modalDocTitle = document.getElementById('modal-doc-title'); const pageChunksList = document.getElementById('page-chunks-list'); const vectorChunksList = document.getElementById('vector-chunks-list'); const tocChunksList = document.getElementById('toc-chunks-list'); closeModalBtn.addEventListener('click', () => { chunksModal.style.display = 'none'; }); // Close if clicking outside window.addEventListener('click', (e) => { if (e.target === chunksModal) { chunksModal.style.display = 'none'; } }); async function openChunksModal(filename) { modalDocTitle.textContent = filename; pageChunksList.innerHTML = '
'; vectorChunksList.innerHTML = '
'; tocChunksList.innerHTML = '
'; chunksModal.style.display = 'flex'; try { const res = await fetch(`/api/documents/${encodeURIComponent(filename)}/chunks`); const data = await res.json(); // Render Page Chunks pageChunksList.innerHTML = ''; let totalPageTokens = 0; if (data.page_chunks && data.page_chunks.length > 0) { data.page_chunks.forEach(chunk => { const el = document.createElement('div'); el.className = 'chunk-item'; el.innerHTML = ` Page ${chunk.page_num} Tokens: ${chunk.tokens || 0}
${chunk.text}
`; pageChunksList.appendChild(el); totalPageTokens += (chunk.tokens || 0); }); const totalEl = document.createElement('div'); totalEl.style.marginTop = '16px'; totalEl.style.padding = '12px'; totalEl.style.backgroundColor = 'var(--bg-secondary)'; totalEl.style.borderRadius = '6px'; totalEl.style.fontWeight = 'bold'; totalEl.style.display = 'flex'; totalEl.style.justifyContent = 'space-between'; totalEl.style.border = '1px solid var(--border-color)'; totalEl.innerHTML = `Total Page RAG Tokens: ${totalPageTokens.toLocaleString()}`; pageChunksList.appendChild(totalEl); } else { pageChunksList.innerHTML = '
No page chunks found.
'; } // Render Vector Chunks vectorChunksList.innerHTML = ''; let totalVectorTokens = 0; if (data.vector_chunks && data.vector_chunks.length > 0) { data.vector_chunks.forEach(chunk => { const el = document.createElement('div'); el.className = 'chunk-item'; el.innerHTML = ` Chunk #${chunk.chunk_index} Tokens: ${chunk.tokens || 0}
${chunk.text}
`; vectorChunksList.appendChild(el); totalVectorTokens += (chunk.tokens || 0); }); const totalEl = document.createElement('div'); totalEl.style.marginTop = '16px'; totalEl.style.padding = '12px'; totalEl.style.backgroundColor = 'var(--bg-secondary)'; totalEl.style.borderRadius = '6px'; totalEl.style.fontWeight = 'bold'; totalEl.style.display = 'flex'; totalEl.style.justifyContent = 'space-between'; totalEl.style.border = '1px solid var(--border-color)'; totalEl.innerHTML = `Total Vector RAG Tokens: ${totalVectorTokens.toLocaleString()}`; vectorChunksList.appendChild(totalEl); } else { vectorChunksList.innerHTML = '
No vector chunks found.
'; } // Render TOC Chunks tocChunksList.innerHTML = ''; let totalTocTokens = 0; if (data.toc_chunks && data.toc_chunks.length > 0) { data.toc_chunks.forEach(chunk => { const el = document.createElement('div'); el.className = 'chunk-item'; el.innerHTML = ` Node ${chunk.node_id}: ${chunk.title || ''} Tokens: ${chunk.tokens || 0}
${chunk.text}
`; tocChunksList.appendChild(el); totalTocTokens += (chunk.tokens || 0); }); const totalEl = document.createElement('div'); totalEl.style.marginTop = '16px'; totalEl.style.padding = '12px'; totalEl.style.backgroundColor = 'var(--bg-secondary)'; totalEl.style.borderRadius = '6px'; totalEl.style.fontWeight = 'bold'; totalEl.style.display = 'flex'; totalEl.style.justifyContent = 'space-between'; totalEl.style.border = '1px solid var(--border-color)'; totalEl.innerHTML = `Total TOC RAG Tokens: ${totalTocTokens.toLocaleString()}`; tocChunksList.appendChild(totalEl); } else { tocChunksList.innerHTML = '
No TOC nodes found.
'; } } catch (error) { console.error(error); pageChunksList.innerHTML = '
Error loading data.
'; vectorChunksList.innerHTML = '
Error loading data.
'; tocChunksList.innerHTML = '
Error loading data.
'; } } });