Spaces:
Sleeping
Sleeping
| 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 = ` | |
| <div class="citation-title"> | |
| <span class="citation-file-badge">PDF</span> | |
| <strong>${escapeHtml(currentFileName || payload.file_name || "Uploaded PDF")}</strong> | |
| </div> | |
| <div class="citation-meta"> | |
| <button type="button" data-page="${escapeHtml(pageNumber)}">Page ${escapeHtml(pageNumber)}</button> | |
| <span>Heading: ${escapeHtml(heading)}</span> | |
| <span>Parent Chunk: ${citation.parent_index + 1}</span> | |
| </div> | |
| <p>${escapeHtml(citation.preview)}</p> | |
| `; | |
| 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 = ` | |
| <div class="table-row table-head"><span>Chunking</span><span>Chunks</span><span>Score</span></div> | |
| ${strategies | |
| .map((strategy) => { | |
| const winner = bestChunking.textContent.toLowerCase().includes(strategy) ? " winner" : ""; | |
| return `<div class="table-row${winner}"> | |
| <span>${label(strategy)}</span> | |
| <span>${chunkCounts[strategy] || 0}</span> | |
| <span>${chunkScores[strategy] ?? "-"}</span> | |
| </div>`; | |
| }) | |
| .join("")} | |
| `; | |
| } | |
| function renderHybridStats(stats) { | |
| renderPipelineStats(stats); | |
| hybridStats.innerHTML = ` | |
| <div><span>Vector Results</span><strong>${stats.vector_results ?? "-"}</strong></div> | |
| <div><span>BM25 Results</span><strong>${stats.bm25_results ?? "-"}</strong></div> | |
| <div><span>Merged</span><strong>${stats.merged ?? "-"}</strong></div> | |
| <div><span>Final Reranked</span><strong>${stats.final_reranked ?? "-"}</strong></div> | |
| `; | |
| } | |
| function resetHybridStats() { | |
| renderHybridStats({}); | |
| } | |
| function renderEvaluationCards(metrics) { | |
| evaluationCards.innerHTML = ` | |
| <div><span>Context Precision</span><strong>${metricScore(metrics.context_precision)}</strong></div> | |
| <div><span>Context Recall</span><strong>${metricScore(metrics.context_recall)}</strong></div> | |
| <div><span>Faithfulness</span><strong>${metricScore(metrics.faithfulness)}</strong></div> | |
| <div><span>Answer Relevancy</span><strong>${metricScore(metrics.answer_relevancy)}</strong></div> | |
| `; | |
| } | |
| function resetEvaluationCards() { | |
| renderEvaluationCards({}); | |
| } | |
| function renderRetrievedChunks(hits) { | |
| if (!hits.length) { | |
| retrievedChunks.innerHTML = '<div class="empty-state">No retrieved chunks yet.</div>'; | |
| return; | |
| } | |
| retrievedChunks.innerHTML = hits | |
| .map( | |
| (hit, index) => ` | |
| <details class="chunk-card"> | |
| <summary> | |
| <span>#${index + 1}</span> | |
| <strong>${label(hit.strategy)} Chunk</strong> | |
| <div class="chunk-summary-scores"> | |
| <em>Similarity: ${score(hit.vector_score)}</em> | |
| <em>BM25: ${bm25Score(hit.bm25_raw_score, hit.bm25_score)}</em> | |
| <em>Reranker: ${score(hit.rerank_score)}</em> | |
| </div> | |
| </summary> | |
| <p><strong>Chunk Text</strong>${escapeHtml(hit.child_text)}</p> | |
| <div class="chunk-scores"> | |
| <span>Similarity Score: <strong>${score(hit.vector_score)}</strong></span> | |
| <span>BM25 Score: <strong>${bm25Score(hit.bm25_raw_score, hit.bm25_score)}</strong></span> | |
| <span>Reranker Score: <strong>${score(hit.rerank_score)}</strong></span> | |
| </div> | |
| </details> | |
| ` | |
| ) | |
| .join(""); | |
| } | |
| function score(value) { | |
| return value === undefined || value === null ? "-" : Number(value).toFixed(2); | |
| } | |
| function metricScore(value) { | |
| return value === undefined || value === null ? "Not Evaluated Yet" : Number(value).toFixed(2); | |
| } | |
| function bm25Score(raw, normalized) { | |
| if (raw !== undefined && raw !== null) { | |
| return Number(raw).toFixed(2); | |
| } | |
| return score(normalized); | |
| } | |
| function label(value) { | |
| return String(value || "") | |
| .replace(/^./, (char) => char.toUpperCase()); | |
| } | |
| function inferHeading(preview) { | |
| const text = String(preview || "").trim(); | |
| if (!text) { | |
| return "Document context"; | |
| } | |
| const firstLine = text.split(/\n|\. /)[0].trim(); | |
| const words = firstLine.split(/\s+/).slice(0, 5).join(" "); | |
| return words || "Document context"; | |
| } | |
| function timelineLabel(step) { | |
| const labels = { | |
| "Document Status: processing": "Processing Started", | |
| "Text Cleaning": "Text Cleaning", | |
| "Metadata": "Metadata Extraction", | |
| "Parent Chunks": "Parent Chunks", | |
| "Fixed / Recursive / Semantic Child Chunks": "Child Chunks", | |
| "Chunk Size Optimization": "Chunk Size Optimization", | |
| "Embeddings": "Embeddings", | |
| "Embedding Evaluation": "Embedding Evaluation", | |
| "Qdrant Store": "Qdrant Index", | |
| "BM25 Index": "BM25 Index", | |
| "Document Status: indexed": "Ready", | |
| }; | |
| return labels[step] || step; | |
| } | |
| function renderCragStatus(grade) { | |
| const good = grade === "good" || grade === "answered"; | |
| cragStatus.className = `crag-box ${good ? "good" : "bad"}`; | |
| cragStatus.innerHTML = good | |
| ? "<strong>Good</strong><span>Retrieved context passed CRAG grading.</span>" | |
| : "<strong>Low Confidence</strong><span>Query rewrite or no-answer fallback was used.</span>"; | |
| } | |
| function resetCragStatus() { | |
| cragStatus.className = "crag-box neutral"; | |
| cragStatus.innerHTML = "<strong>Waiting</strong><span>Run a query to grade retrieval quality.</span>"; | |
| } | |
| function renderSelectedFile(file, status) { | |
| currentFileName = file.name; | |
| currentFileSize = file.size; | |
| uploadedFileName.textContent = file.name; | |
| uploadedFileMeta.textContent = `0 Pages / ${formatBytes(file.size)}`; | |
| fileCard.querySelector("em").textContent = status; | |
| fileCard.classList.remove("is-hidden"); | |
| if (pdfPreviewUrl) { | |
| URL.revokeObjectURL(pdfPreviewUrl); | |
| } | |
| pdfPreviewUrl = URL.createObjectURL(file); | |
| pdfPreview.src = `${pdfPreviewUrl}#page=1&toolbar=0&navpanes=0&scrollbar=0`; | |
| pdfPreviewCard.classList.remove("is-hidden"); | |
| } | |
| function updateFileCardMeta(totalPages, status) { | |
| if (!currentFileName) { | |
| return; | |
| } | |
| uploadedFileName.textContent = currentFileName; | |
| uploadedFileMeta.textContent = `${totalPages || pageTexts.size || 0} Pages / ${formatBytes(currentFileSize)}`; | |
| fileCard.querySelector("em").textContent = status; | |
| fileCard.classList.remove("is-hidden"); | |
| } | |
| function formatBytes(bytes) { | |
| if (!bytes) { | |
| return "0 MB"; | |
| } | |
| const mb = bytes / (1024 * 1024); | |
| if (mb >= 1) { | |
| return `${mb.toFixed(1)} MB`; | |
| } | |
| return `${Math.max(bytes / 1024, 1).toFixed(0)} KB`; | |
| } | |
| function renderIndexTimeline() { | |
| indexSteps.innerHTML = timelineSteps | |
| .map((step) => { | |
| const key = timelineStepKey(step); | |
| const done = completedTimeline.has(key); | |
| const current = !done && currentTimelineStep && timelineStepKey(currentTimelineStep) === key; | |
| const state = done ? "done" : current ? "current" : "pending"; | |
| const marker = done ? "OK" : current ? "..." : ""; | |
| return `<div class="timeline-item ${state}"> | |
| <span>${marker}</span> | |
| <strong>${escapeHtml(step)}</strong> | |
| </div>`; | |
| }) | |
| .join(""); | |
| } | |
| function timelineStepKey(value) { | |
| return String(value || "").toLowerCase().replace(/[^a-z0-9]+/g, ""); | |
| } | |
| function bestPerformerLabel(strategy) { | |
| return strategy ? `Best Performer: ${label(strategy)} Chunking` : "Evaluation Pending"; | |
| } | |
| function recommendationHtml(strategy, reason) { | |
| const strategyLabel = label(strategy || "semantic"); | |
| return ` | |
| <strong>Recommended Strategy</strong> | |
| <span>${strategyLabel} Chunking</span> | |
| <p>Reason:</p> | |
| <ul> | |
| <li>${escapeHtml(reason || "Highest retrieval quality.")}</li> | |
| <li>Best faithfulness</li> | |
| <li>Best citation accuracy</li> | |
| <li>Lowest hallucination risk</li> | |
| </ul> | |
| `; | |
| } | |
| function renderPipelineStats(stats = {}) { | |
| const hasStats = Object.keys(stats).length > 0; | |
| const vector = hasStats ? stats.vector_results ?? 0 : "Not Run"; | |
| const bm25 = hasStats ? stats.bm25_results ?? 0 : "Not Run"; | |
| const merged = hasStats ? stats.merged ?? 0 : "Not Run"; | |
| const final = hasStats ? stats.final_reranked ?? 0 : "Not Run"; | |
| pipelineViz.innerHTML = ` | |
| <div>Query</div> | |
| <span></span> | |
| <div>Vector Search (${vector})</div> | |
| <span></span> | |
| <div>BM25 (${bm25})</div> | |
| <span></span> | |
| <div>Merged (${merged})</div> | |
| <span></span> | |
| <div>Reranked (${final})</div> | |
| <span></span> | |
| <div>Answer</div> | |
| `; | |
| } | |
| function renderConfidenceBadge(grade, metrics = {}) { | |
| if (!grade) { | |
| confidenceBadge.className = "confidence-badge neutral"; | |
| confidenceBadge.textContent = "Confidence pending"; | |
| return; | |
| } | |
| const faithfulness = Number(metrics.faithfulness || 0); | |
| const gradeGood = grade === "good" || grade === "answered"; | |
| const level = gradeGood && faithfulness >= 0.9 ? "high" : gradeGood && faithfulness >= 0.75 ? "medium" : "low"; | |
| const labelMap = { high: "High Confidence", medium: "Medium Confidence", low: "Low Confidence" }; | |
| confidenceBadge.className = `confidence-badge ${level}`; | |
| confidenceBadge.textContent = labelMap[level]; | |
| } | |
| function renderHallucinationCard(metrics = {}, grade) { | |
| const faithfulness = metrics.faithfulness; | |
| const precision = metrics.context_precision; | |
| const coverage = faithfulness === undefined || precision === undefined ? null : Math.round(((faithfulness + precision) / 2) * 100); | |
| const good = (grade === "good" || grade === "answered") && Number(faithfulness || 0) >= 0.75; | |
| hallucinationCard.className = `hallucination-card ${coverage === null ? "neutral" : good ? "good" : "bad"}`; | |
| hallucinationCard.innerHTML = ` | |
| <span>Answer Source Coverage</span> | |
| <strong>${coverage === null ? "-" : `${coverage}%`}</strong> | |
| <span>Faithfulness</span> | |
| <strong>${score(faithfulness)}</strong> | |
| <em>${coverage === null ? "Waiting for answer" : good ? "No Hallucination Detected" : "Needs review"}</em> | |
| `; | |
| } | |
| function addQueryHistory(query) { | |
| const clean = String(query || "").trim(); | |
| if (!clean) { | |
| return; | |
| } | |
| const existingIndex = queryHistoryItems.findIndex((item) => item.toLowerCase() === clean.toLowerCase()); | |
| if (existingIndex >= 0) { | |
| queryHistoryItems.splice(existingIndex, 1); | |
| } | |
| queryHistoryItems.unshift(clean); | |
| queryHistoryItems.splice(5); | |
| renderQueryHistory(); | |
| } | |
| function renderQueryHistory() { | |
| historyCount.textContent = `${queryHistoryItems.length} saved`; | |
| if (!queryHistoryItems.length) { | |
| queryHistory.innerHTML = "<span>No questions yet.</span>"; | |
| return; | |
| } | |
| queryHistory.innerHTML = queryHistoryItems | |
| .map((query) => `<button type="button">${escapeHtml(query)}</button>`) | |
| .join(""); | |
| } | |
| function findPageForText(preview) { | |
| const needle = String(preview || "").replace(/\s+/g, " ").trim().slice(0, 90); | |
| if (!needle) { | |
| return ""; | |
| } | |
| for (const [page, text] of pageTexts.entries()) { | |
| const haystack = String(text || "").replace(/\s+/g, " "); | |
| if (haystack.includes(needle)) { | |
| return page; | |
| } | |
| } | |
| return ""; | |
| } | |
| function scrollToExtractedPage(pageNumber) { | |
| const details = document.querySelector(".details-panel details"); | |
| details.open = true; | |
| document.querySelector(".details-panel").scrollIntoView({ behavior: "smooth", block: "start" }); | |
| pageDetails.querySelectorAll(".page-chip").forEach((chip) => { | |
| chip.classList.toggle("is-active", chip.dataset.page === String(pageNumber)); | |
| }); | |
| } | |
| function showUploadMessage(message, isError = false, isSuccess = false) { | |
| uploadMessage.textContent = message; | |
| uploadMessage.className = `upload-message${isError ? " error" : ""}${isSuccess ? " success" : ""}`; | |
| } | |
| function cleanUploadError(message) { | |
| const text = String(message || ""); | |
| const keyMatch = text.match(/Tried\s+(\d+)\s+active API key\(s\) out of\s+(\d+)\s+configured key\(s\);\s+(\d+)\s+key\(s\)/i); | |
| if (keyMatch) { | |
| const [, triedKeys, totalKeys, quotaKeys] = keyMatch; | |
| return `Gemini OCR ne ${triedKeys}/${totalKeys} API keys try ki. ${quotaKeys} key(s) quota/rate-limit de rahi hain. Agar yahi message aa raha hai to ya to sab keys exhausted hain, ya server/browser abhi old code use kar raha hai. Server restart aur browser hard refresh ke baad dobara try karo.`; | |
| } | |
| if (text.toLowerCase().includes("api key not valid") || text.toLowerCase().includes("invalid api key")) { | |
| return "Gemini API key valid nahi lag rahi. env file me GEMINI_API_KEY_1 se GEMINI_API_KEY_4 tak valid Google Gemini keys daalo, phir server restart karo."; | |
| } | |
| if (text.includes("RESOURCE_EXHAUSTED") || text.toLowerCase().includes("quota")) { | |
| return "Gemini OCR quota/rate-limit error de raha hai. App next keys try karta hai; agar phir bhi ye aa raha hai to configured keys exhausted/invalid ho sakti hain. Server restart aur hard refresh ke baad dobara try karo."; | |
| } | |
| if (text.includes("temporarily unavailable") || text.includes("503")) { | |
| return "Gemini OCR abhi busy hai. Thodi der baad try karo ya text-based PDF upload karo."; | |
| } | |
| if (text.toLowerCase().includes("password")) { | |
| return "PDF password-protected hai. Unlocked PDF upload karo."; | |
| } | |
| return text; | |
| } | |
| renderIndexTimeline(); | |
| resetHybridStats(); | |
| resetEvaluationCards(); | |
| resetCragStatus(); | |
| renderHallucinationCard(); | |
| renderRetrievedChunks([]); | |
| renderQueryHistory(); | |