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 = `
`;
} 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 = `
Prompt: --
Response: --
`;
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.
';
}
}
});