const form = document.querySelector("#uploadForm"); const input = document.querySelector("#pdfInput"); const fileName = document.querySelector("#fileName"); const resultText = document.querySelector("#resultText"); const routeValue = document.querySelector("#routeValue"); const pageCount = document.querySelector("#pageCount"); const directText = document.querySelector("#directText"); const pageDetails = document.querySelector("#pageDetails"); const copyButton = document.querySelector("#copyButton"); const submitButton = form.querySelector('button[type="submit"]'); const uploadMessage = document.querySelector("#uploadMessage"); const fileCard = document.querySelector("#fileCard"); const uploadedFileName = document.querySelector("#uploadedFileName"); const uploadedFileMeta = document.querySelector("#uploadedFileMeta"); const pdfPreviewCard = document.querySelector("#pdfPreviewCard"); const pdfPreview = document.querySelector("#pdfPreview"); const docStatus = document.querySelector("#docStatus"); const indexSteps = document.querySelector("#indexSteps"); const activeDocument = document.querySelector("#activeDocument"); const dashboardStatus = document.querySelector("#dashboardStatus"); const chunkComparison = document.querySelector("#chunkComparison"); const bestChunking = document.querySelector("#bestChunking"); const hybridStats = document.querySelector("#hybridStats"); const pipelineViz = document.querySelector("#pipelineViz"); const evaluationCards = document.querySelector("#evaluationCards"); const hallucinationCard = document.querySelector("#hallucinationCard"); const bestRecommendation = document.querySelector("#bestRecommendation"); const cragStatus = document.querySelector("#cragStatus"); const queryForm = document.querySelector("#queryForm"); const queryInput = document.querySelector("#queryInput"); const confidenceBadge = document.querySelector("#confidenceBadge"); const answerBox = document.querySelector("#answerBox"); const citations = document.querySelector("#citations"); const retrievedChunks = document.querySelector("#retrievedChunks"); const queryHistory = document.querySelector("#queryHistory"); const historyCount = document.querySelector("#historyCount"); const feedbackForm = document.querySelector("#feedbackForm"); const feedbackRating = document.querySelector("#feedbackRating"); const feedbackComment = document.querySelector("#feedbackComment"); const unrelatedPdfAnswer = "Sorry, I can't give any answer because your question is not related to the PDF."; const timelineSteps = [ "Text Cleaning", "Metadata Extraction", "Parent Chunks", "Child Chunks", "Embeddings", "Qdrant Index", "BM25 Index", "Ready", ]; const pageTexts = new Map(); const queryHistoryItems = []; let directPages = 0; let ocrPages = 0; let currentDocumentId = ""; let currentFileName = ""; let currentFileSize = 0; let lastQuery = ""; let chunkCounts = {}; let chunkScores = {}; let completedTimeline = new Set(); let currentTimelineStep = ""; let activeRunId = 0; let activeQueryId = 0; let pdfPreviewUrl = ""; let uploadController = null; let indexController = null; let queryController = null; input.addEventListener("change", () => { const file = input.files[0]; fileName.textContent = file?.name || "Choose or drop a PDF"; if (file) { handleNewFileSelected(file); } }); citations.addEventListener("click", (event) => { const button = event.target.closest("[data-page]"); if (!button) { return; } scrollToExtractedPage(button.dataset.page); }); queryHistory.addEventListener("click", (event) => { const button = event.target.closest("button"); if (!button) { return; } queryInput.value = button.textContent.trim(); queryInput.focus(); }); form.addEventListener("dragover", (event) => { event.preventDefault(); form.classList.add("is-dragging"); }); form.addEventListener("dragleave", () => { form.classList.remove("is-dragging"); }); form.addEventListener("drop", (event) => { event.preventDefault(); form.classList.remove("is-dragging"); const file = event.dataTransfer.files[0]; if (file) { input.files = event.dataTransfer.files; fileName.textContent = file.name; handleNewFileSelected(file); } }); form.addEventListener("submit", async (event) => { event.preventDefault(); const file = input.files[0]; if (!file) { setError("Please select a PDF first."); return; } const runId = startUploadRun(file.name); setLoading(); const data = new FormData(); data.append("file", file); try { const response = await fetch("/api/pdf-to-text-stream", { method: "POST", body: data, signal: uploadController.signal, }); if (!isActiveRun(runId)) { return; } if (!response.ok) { throw new Error(await readErrorMessage(response, "PDF processing failed.")); } await readNdjson(response, (event) => handlePdfEvent(event, runId)); } catch (error) { if (error.name !== "AbortError" && isActiveRun(runId)) { setError(error.message); } } finally { if (isActiveRun(runId)) { submitButton.disabled = false; submitButton.textContent = "Upload PDF"; } } }); queryForm.addEventListener("submit", async (event) => { event.preventDefault(); const query = queryInput.value.trim(); const documentId = currentDocumentId; const queryRunId = activeRunId; if (!currentDocumentId) { answerBox.textContent = ""; showUploadMessage("PDF abhi ready nahi hai. Pehle PDF successfully upload/index hone do.", true); return; } if (!query) { answerBox.textContent = "Question likho, phir Ask dabao."; return; } lastQuery = query; answerBox.textContent = "Finding answer from your PDF..."; citations.innerHTML = ""; retrievedChunks.innerHTML = ""; renderConfidenceBadge(); renderHallucinationCard(); activeQueryId += 1; const queryId = activeQueryId; if (queryController) { queryController.abort(); } queryController = new AbortController(); try { const response = await fetch("/api/query", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ document_id: documentId, query }), signal: queryController.signal, }); const payload = await readJsonResponse(response, "Query failed."); if ( !isActiveRun(queryRunId) || queryId !== activeQueryId || documentId !== currentDocumentId || (payload.document_id && payload.document_id !== currentDocumentId) ) { return; } if (!response.ok) { throw new Error(payload.detail || "Query failed."); } renderAnswer(payload); } catch (error) { if (error.name !== "AbortError" && isActiveRun(queryRunId) && queryId === activeQueryId) { answerBox.textContent = error.message; } } }); feedbackForm.addEventListener("submit", async (event) => { event.preventDefault(); if (!currentDocumentId || !lastQuery) { answerBox.textContent = "Feedback ke liye pehle query run karo."; return; } const response = await fetch("/api/feedback", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ document_id: currentDocumentId, query: lastQuery, rating: Number(feedbackRating.value), comment: feedbackComment.value, }), }); const payload = await response.json(); answerBox.textContent += `\n\nFeedback saved: ${payload.feedback_id}`; feedbackComment.value = ""; }); copyButton.addEventListener("click", async () => { const text = resultText.textContent; if (!text || text.startsWith("Upload a PDF")) { return; } await navigator.clipboard.writeText(text); copyButton.textContent = "Copied"; setTimeout(() => { copyButton.textContent = "Copy"; }, 1200); }); function startUploadRun(fileNameValue) { activeRunId += 1; activeQueryId += 1; if (uploadController) { uploadController.abort(); } if (indexController) { indexController.abort(); } if (queryController) { queryController.abort(); } uploadController = new AbortController(); indexController = null; queryController = null; currentFileName = fileNameValue; currentFileSize = input.files[0]?.size || 0; currentDocumentId = ""; return activeRunId; } function isActiveRun(runId) { return runId === activeRunId; } function handleNewFileSelected(file) { activeRunId += 1; activeQueryId += 1; if (uploadController) { uploadController.abort(); } if (indexController) { indexController.abort(); } if (queryController) { queryController.abort(); } uploadController = null; indexController = null; queryController = null; currentDocumentId = ""; currentFileName = file.name; currentFileSize = file.size; lastQuery = ""; queryInput.value = ""; pageTexts.clear(); directPages = 0; ocrPages = 0; pageDetails.innerHTML = ""; resultText.textContent = "Upload the selected PDF to see extracted text here."; routeValue.textContent = "Waiting"; pageCount.textContent = "0"; directText.textContent = "-"; completedTimeline = new Set(); currentTimelineStep = ""; activeDocument.textContent = "New PDF selected. Upload to index."; dashboardStatus.textContent = "Upload required"; docStatus.textContent = "Waiting"; answerBox.textContent = "New PDF selected. Click Upload PDF, then ask questions from this file."; citations.innerHTML = ""; retrievedChunks.innerHTML = ""; chunkComparison.innerHTML = ""; bestChunking.textContent = "Evaluation Pending"; chunkCounts = {}; chunkScores = {}; renderIndexTimeline(); resetHybridStats(); resetEvaluationCards(); renderConfidenceBadge(); resetCragStatus(); renderHallucinationCard(); bestRecommendation.textContent = "Index a PDF to compare chunking strategies."; showUploadMessage("New PDF selected. Upload it to replace the active document."); submitButton.disabled = false; submitButton.textContent = "Upload PDF"; renderSelectedFile(file, "Selected"); } function setLoading() { pageTexts.clear(); directPages = 0; ocrPages = 0; currentDocumentId = ""; lastQuery = ""; queryInput.value = ""; feedbackComment.value = ""; completedTimeline = new Set(); currentTimelineStep = "Text Cleaning"; submitButton.disabled = true; submitButton.textContent = "Uploading"; routeValue.textContent = "Processing"; pageCount.textContent = "-"; directText.textContent = "-"; pageDetails.innerHTML = ""; resultText.textContent = ""; docStatus.textContent = "Waiting"; dashboardStatus.textContent = "Waiting for PDF"; renderIndexTimeline(); activeDocument.textContent = currentFileName ? `Indexing: ${currentFileName}` : "Not ready"; answerBox.textContent = "Upload and index the PDF, then ask your question."; renderConfidenceBadge(); showUploadMessage("Reading PDF. Please wait..."); citations.innerHTML = ""; retrievedChunks.innerHTML = ""; chunkComparison.innerHTML = ""; bestChunking.textContent = "Evaluation Pending"; resetHybridStats(); resetEvaluationCards(); resetCragStatus(); renderHallucinationCard(); bestRecommendation.textContent = "Index a PDF to compare chunking strategies."; chunkCounts = {}; chunkScores = {}; if (input.files[0]) { renderSelectedFile(input.files[0], "Processing Upload"); } } function setError(message) { const cleanMessage = String(message || "PDF processing failed."); routeValue.textContent = "Error"; pageCount.textContent = "0"; directText.textContent = "-"; pageDetails.innerHTML = ""; resultText.textContent = cleanMessage; answerBox.textContent = ""; renderConfidenceBadge(); showUploadMessage(cleanUploadError(cleanMessage), true); dashboardStatus.textContent = "Error"; activeDocument.textContent = "Not ready"; submitButton.disabled = false; submitButton.textContent = "Upload PDF"; } async function readNdjson(response, handler) { const reader = response.body.getReader(); const decoder = new TextDecoder(); let buffer = ""; const dispatchLine = (line) => { const trimmed = line.trim(); if (!trimmed) { return; } let event; try { event = JSON.parse(trimmed); } catch (error) { throw new Error("Processing stream ended unexpectedly. Please upload the PDF again."); } if (event.type === "error") { throw new Error(event.message || "Processing failed."); } handler(event); }; while (true) { const { value, done } = 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) { dispatchLine(line); } } buffer += decoder.decode(); dispatchLine(buffer); } async function readJsonResponse(response, fallbackMessage) { const text = await response.text(); if (!text.trim()) { throw new Error(`${fallbackMessage} Empty response from server.`); } try { return JSON.parse(text); } catch (error) { throw new Error(`${fallbackMessage} Server returned an invalid response.`); } } async function readErrorMessage(response, fallbackMessage) { let text = ""; try { text = await response.text(); } catch (error) { return `${fallbackMessage} HTTP ${response.status}.`; } if (!text.trim()) { return `${fallbackMessage} HTTP ${response.status}: empty response from server.`; } try { const payload = JSON.parse(text); return payload.detail || payload.message || `${fallbackMessage} HTTP ${response.status}.`; } catch (error) { return text.slice(0, 500); } } function handlePdfEvent(event, runId) { if (!isActiveRun(runId)) { return; } if (event.type === "start") { pageCount.textContent = `0 / ${event.page_count}`; updateFileCardMeta(event.page_count, "Processing Upload"); directText.textContent = "Checking"; resultText.textContent = "Processing page 1..."; showUploadMessage(`Reading PDF pages: 0 / ${event.page_count}`); return; } if (event.type === "page") { if (String(event.text || "").trim()) { pageTexts.set(event.page_number, event.text || ""); } routeValue.textContent = event.route; pageCount.textContent = `${Math.max(pageTexts.size, event.page_number)} / ${event.page_count}`; updateFileCardMeta(event.page_count, "Processing Upload"); if (event.direct_text_found) { directPages += 1; } else { ocrPages += 1; } directText.textContent = directStatus(); appendPageChip(event); renderPageText(); showUploadMessage(`Reading PDF pages: ${event.page_number} / ${event.page_count}`); dashboardStatus.textContent = "Extracting PDF text"; return; } if (event.type === "done") { routeValue.textContent = "Completed"; pageCount.textContent = `${event.page_count} / ${event.page_count}`; updateFileCardMeta(event.page_count, "Uploaded Successfully"); if (!resultText.textContent.trim()) { resultText.textContent = "No text was extracted."; return; } indexExtractedText(runId).catch((error) => { if (error.name !== "AbortError" && isActiveRun(runId)) { docStatus.textContent = "error"; appendIndexStep("Indexing failed", error.message); showUploadMessage(cleanUploadError(error.message), true); } }); return; } if (event.type === "error") { setError(event.message || "PDF processing failed."); } } async function indexExtractedText(runId) { if (!isActiveRun(runId)) { return; } docStatus.textContent = "processing"; dashboardStatus.textContent = "Indexing"; currentTimelineStep = "Text Cleaning"; renderIndexTimeline(); showUploadMessage("Preparing PDF for questions..."); const fileNameForRun = currentFileName || "uploaded.pdf"; const textForRun = resultText.textContent; indexController = new AbortController(); const response = await fetch("/api/index-text-stream", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ file_name: fileNameForRun, text: textForRun, }), signal: indexController.signal, }); if (!isActiveRun(runId)) { return; } if (!response.ok) { throw new Error(await readErrorMessage(response, "Indexing failed.")); } await readNdjson(response, (event) => handleIndexEvent(event, runId, fileNameForRun)); } function handleIndexEvent(event, runId, fileNameForRun) { if (!isActiveRun(runId)) { return; } if (event.type === "status") { docStatus.textContent = event.status; appendIndexStep(event.step); return; } if (event.type === "step") { appendIndexStep(event.step); captureIndexDetail(event.step, event.detail); return; } if (event.type === "done") { currentDocumentId = event.document_id || ""; docStatus.textContent = "Indexed"; dashboardStatus.textContent = "Ready"; activeDocument.textContent = fileNameForRun; appendIndexStep(event.step); captureIndexSummary(event.summary); answerBox.textContent = "Ask a question from this PDF."; showUploadMessage("PDF ready. Ask your question.", false, true); queryInput.focus(); return; } if (event.type === "error") { docStatus.textContent = "error"; appendIndexStep("Error", event.message); showUploadMessage(cleanUploadError(event.message), true); } } function appendPageChip(page) { const confidence = page.confidence === null || page.confidence === undefined ? "" : ` - ${(page.confidence * 100).toFixed(1)}%`; const chip = document.createElement("div"); chip.className = "page-chip"; chip.dataset.page = String(page.page_number); chip.textContent = `Page ${page.page_number}: ${page.engine}${confidence}`; pageDetails.appendChild(chip); } function appendIndexStep(step) { const labelValue = timelineLabel(step); completedTimeline.add(timelineStepKey(labelValue)); const index = timelineSteps.findIndex((item) => timelineStepKey(item) === timelineStepKey(labelValue)); if (index >= 0) { const next = timelineSteps[index + 1]; currentTimelineStep = next && !completedTimeline.has(timelineStepKey(next)) ? next : ""; } renderIndexTimeline(); } function renderPageText() { const orderedPages = [...pageTexts.keys()].sort((a, b) => a - b); resultText.textContent = orderedPages .filter((pageNumber) => String(pageTexts.get(pageNumber) || "").trim()) .map((pageNumber) => `--- Page ${pageNumber} ---\n${pageTexts.get(pageNumber)}`) .join("\n\n"); } function renderAnswer(payload) { addQueryHistory(payload.query || lastQuery); answerBox.textContent = payload.answer || unrelatedPdfAnswer; dashboardStatus.textContent = payload.status === "answered" ? "Answered" : "No answer"; renderConfidenceBadge(payload.retrieval_grade || payload.status, payload.evaluation || {}); citations.innerHTML = ""; for (const citation of payload.citations || []) { const card = document.createElement("div"); card.className = "citation-card"; const heading = inferHeading(citation.preview); const pageNumber = findPageForText(citation.preview) || citation.page_number || citation.parent_index + 1; card.innerHTML = `
${escapeHtml(citation.preview)}
`; citations.appendChild(card); } renderHybridStats(payload.retrieval_stats || {}); renderEvaluationCards(payload.evaluation || {}); renderCragStatus(payload.retrieval_grade || payload.status); renderHallucinationCard(payload.evaluation || {}, payload.retrieval_grade || payload.status); renderRetrievedChunks(payload.hits || []); } function directStatus() { if (directPages > 0 && ocrPages > 0) { return "Mixed"; } if (directPages > 0) { return "Yes"; } if (ocrPages > 0) { return "No"; } return "Checking"; } function escapeHtml(value) { return String(value || "") .replaceAll("&", "&") .replaceAll("<", "<") .replaceAll(">", ">") .replaceAll('"', """) .replaceAll("'", "'"); } function captureIndexDetail(step, detail) { if (step.includes("Child Chunks") && detail && typeof detail === "object") { chunkCounts = detail; renderChunkComparison(); } if (step === "Chunk Size Optimization" && detail && typeof detail === "object") { chunkScores = detail.scores || {}; bestChunking.textContent = bestPerformerLabel(detail.best_chunking); bestRecommendation.innerHTML = recommendationHtml(detail.best_chunking, detail.reason); renderChunkComparison(); } } function captureIndexSummary(summary) { if (!summary) { return; } if (summary.chunk_strategy_counts) { chunkCounts = summary.chunk_strategy_counts; } const recommendation = summary.chunking_recommendation || {}; chunkScores = recommendation.scores || chunkScores; bestChunking.textContent = bestPerformerLabel(recommendation.best_chunking); bestRecommendation.innerHTML = recommendationHtml(recommendation.best_chunking, recommendation.reason); renderChunkComparison(); } function renderChunkComparison() { const strategies = ["fixed", "recursive", "semantic"]; chunkComparison.innerHTML = `Chunk Text${escapeHtml(hit.child_text)}
Reason: