Urmila Saini
add DockerFile
b9fa4a6
Raw
History Blame Contribute Delete
34.2 kB
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("&", "&amp;")
.replaceAll("<", "&lt;")
.replaceAll(">", "&gt;")
.replaceAll('"', "&quot;")
.replaceAll("'", "&#039;");
}
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();