// Global State let currentTelemetry = null; let apiBaseUrl = window.location.origin; // Dynamically bind to current host (works locally and on HF spaces!) // DOM Elements const docIdInput = document.getElementById("doc-id"); const docTitleInput = document.getElementById("doc-title"); const docTextInput = document.getElementById("doc-text"); const btnIngest = document.getElementById("btn-ingest"); const documentsList = document.getElementById("documents-list"); const ingestForm = document.getElementById("ingest-form"); const searchQueryInput = document.getElementById("search-query"); const btnSearch = document.getElementById("btn-search"); const btnSearchText = document.getElementById("btn-search-text"); const welcomeSection = document.getElementById("welcome-section"); const resultsSection = document.getElementById("results-section"); const answerText = document.getElementById("answer-text"); const answerBadge = document.getElementById("answer-badge"); const totalTimeBadge = document.getElementById("total-time-badge"); const correctionsBadge = document.getElementById("corrections-badge"); const detailsTitle = document.getElementById("details-title"); const detailsSubtitle = document.getElementById("details-subtitle"); const detailsContent = document.getElementById("details-content"); // Stats Indicators const statQueriesVal = document.querySelector("#stat-queries .stat-value"); const statExpansionsVal = document.querySelector("#stat-expansions .stat-value"); const statBlocksVal = document.querySelector("#stat-blocks .stat-value"); // Slider Parameters const paramRelevance = document.getElementById("param-relevance"); const paramNli = document.getElementById("param-nli"); const paramTemp = document.getElementById("param-temp"); const valRelevance = document.getElementById("val-relevance"); const valNli = document.getElementById("val-nli"); const valTemp = document.getElementById("val-temp"); // Global statistics counters let totalQueries = 0; let totalCorrections = 0; let totalBlocks = 0; // Initialize document.addEventListener("DOMContentLoaded", () => { // Sliders event listeners paramRelevance.addEventListener("input", (e) => { valRelevance.textContent = parseFloat(e.target.value).toFixed(2); }); paramNli.addEventListener("input", (e) => { valNli.textContent = parseFloat(e.target.value).toFixed(2); }); paramTemp.addEventListener("input", (e) => { valTemp.textContent = parseFloat(e.target.value).toFixed(2); }); // Ingest submit listener ingestForm.addEventListener("submit", handleIngestion); // Search click & enter key btnSearch.addEventListener("click", executeSearch); searchQueryInput.addEventListener("keydown", (e) => { if (e.key === "Enter") executeSearch(); }); // Setup timeline step click listeners const steps = document.querySelectorAll(".timeline-step"); steps.forEach(step => { step.addEventListener("click", () => { if (!currentTelemetry) return; // Toggle active classes on steps steps.forEach(s => s.classList.remove("active-selected")); step.classList.add("active-selected"); const stepName = step.getAttribute("data-step"); showStepDetails(stepName); }); }); // Load initial documents listDocuments(); }); // Load Ingested Documents List async function listDocuments() { try { const response = await fetch(`${apiBaseUrl}/api/documents`); if (!response.ok) throw new Error("Failed to load documents list."); const docs = await response.json(); if (docs.length === 0) { documentsList.innerHTML = `

No documents indexed yet.

`; return; } documentsList.innerHTML = ""; docs.forEach(doc => { const item = document.createElement("div"); item.className = "doc-inventory-item"; // Round document size for readability const kbSize = (doc.text_length / 1024).toFixed(1); item.innerHTML = `

${doc.title}

ID: ${doc.id} • ${kbSize} KB • ${doc.chunks_count} chunks
`; // Delete button functionality item.querySelector(".btn-delete").addEventListener("click", async (e) => { const docId = e.currentTarget.getAttribute("data-id"); if (confirm(`Are you sure you want to delete document "${docId}"? This will rebuild the index.`)) { await deleteDocument(docId); } }); documentsList.appendChild(item); }); } catch (err) { console.error(err); documentsList.innerHTML = `

Error loading inventory.

`; } } // Ingest a document async function handleIngestion(e) { e.preventDefault(); const docId = docIdInput.value.trim(); const title = docTitleInput.value.trim(); const text = docTextInput.value.trim(); if (!docId || !title || !text) return; btnIngest.disabled = true; btnIngest.innerHTML = ` Processing...`; try { const response = await fetch(`${apiBaseUrl}/api/ingest`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ doc_id: docId, title: title, text: text }) }); if (!response.ok) throw new Error("Error occurred during document ingestion."); const resData = await response.json(); alert(resData.message); // Reset inputs docIdInput.value = ""; docTitleInput.value = ""; docTextInput.value = ""; // Refresh list listDocuments(); } catch (err) { alert(err.message); } finally { btnIngest.disabled = false; btnIngest.innerHTML = ` Process & Index`; } } // Delete a document async function deleteDocument(docId) { try { const response = await fetch(`${apiBaseUrl}/api/documents/${docId}`, { method: "DELETE" }); if (!response.ok) throw new Error("Error deleting document."); // Refresh list listDocuments(); } catch (err) { alert(err.message); } } // Execute Query Search async function executeSearch() { const query = searchQueryInput.value.trim(); if (!query) return; // Reset UI and show loading states btnSearch.disabled = true; btnSearchText.textContent = "Processing..."; btnSearch.querySelector("i").className = "fa-solid fa-circle-notch fa-spin"; welcomeSection.style.display = "none"; resultsSection.style.display = "block"; // Clear answer panel and reset trace nodes answerText.innerHTML = `
Orchestrating self-correcting RAG loop (resolving indices, query synonyms, and NLI verification)...
`; answerBadge.className = "badge badge-warning"; answerBadge.textContent = "Processing"; resetTimeline(); // Get parameter thresholds const relevance_threshold = parseFloat(paramRelevance.value); const nli_threshold = parseFloat(paramNli.value); const temperature = parseFloat(paramTemp.value); try { const response = await fetch(`${apiBaseUrl}/api/query`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ query: query, relevance_threshold: relevance_threshold, nli_threshold: nli_threshold, temperature: temperature }) }); if (!response.ok) throw new Error("RAG execution failed on the server."); const data = await response.json(); currentTelemetry = data.telemetry; // Display results displayAnswer(data.answer, data.telemetry); updatePipelineVisualization(data.telemetry); // Increment statistics totalQueries++; if (data.telemetry.query_expansion_triggered) totalCorrections++; totalBlocks += data.telemetry.hallucination_blocked_count; // Update stats UI statQueriesVal.textContent = totalQueries; statExpansionsVal.textContent = totalCorrections; statBlocksVal.textContent = totalBlocks; } catch (err) { answerText.innerHTML = ` Error: ${err.message}`; answerBadge.className = "badge badge-danger"; answerBadge.textContent = "Error"; } finally { btnSearch.disabled = false; btnSearchText.textContent = "Search"; btnSearch.querySelector("i").className = "fa-solid fa-chevron-right"; } } // Display final answer function displayAnswer(answer, telemetry) { answerText.textContent = answer; // Update badge status based on verification outcome if (telemetry.success) { answerBadge.className = "badge badge-success"; answerBadge.textContent = "Verified Factual"; } else { answerBadge.className = "badge badge-danger"; answerBadge.textContent = "Unverified Fallback"; } totalTimeBadge.innerHTML = ` Response Time: ${telemetry.execution_time_sec.toFixed(2)}s`; const expansionsCount = telemetry.retrieval_attempts.length - 1; correctionsBadge.innerHTML = ` Queries Expanded: ${expansionsCount}`; } // Reset trace map classes function resetTimeline() { const nodes = ["input", "retrieval", "rerank", "expansion", "generation", "guard"]; nodes.forEach(node => { const element = document.getElementById(`step-node-${node}`); element.className = "timeline-step"; }); // Welcome subpanel reset detailsTitle.innerHTML = ` Pipeline Trace Map`; detailsSubtitle.textContent = "Select any step to view deep logs"; detailsContent.innerHTML = `

Select any step in the Pipeline Trace Map above to inspect dense/sparse data hits, Cross-Encoder weights, Query Expansion synonyms, NLI logic, or generation history.

`; } // Update pipeline trace colors based on logs function updatePipelineVisualization(telemetry) { const nodeInput = document.getElementById("step-node-input"); const nodeRetrieval = document.getElementById("step-node-retrieval"); const nodeRerank = document.getElementById("step-node-rerank"); const nodeExpansion = document.getElementById("step-node-expansion"); const nodeGeneration = document.getElementById("step-node-generation"); const nodeGuard = document.getElementById("step-node-guard"); // 1. Input Node: Always successful nodeInput.classList.add("success"); // 2. Retrieval Node: Warning if expansion triggered, success otherwise if (telemetry.query_expansion_triggered) { nodeRetrieval.classList.add("warning"); nodeExpansion.classList.add("warning"); } else { nodeRetrieval.classList.add("success"); nodeExpansion.classList.add("success"); // expansion skipped (green) } // 3. Re-rank Node: Always success if retrieved chunks successfully nodeRerank.classList.add("success"); // 4. Generation & Guard Node const blockedCount = telemetry.hallucination_blocked_count; if (blockedCount > 0) { nodeGeneration.classList.add("warning"); nodeGuard.classList.add("danger"); // flagged hallucination } else { nodeGeneration.classList.add("success"); nodeGuard.classList.add("success"); // entailment verified } // Automatically select the Guard step to show NLI logs first (best practice) nodeGuard.click(); } // Show Step Details inside the details subpanel function showStepDetails(stepName) { if (!currentTelemetry) return; let title = ""; let subtitle = ""; let html = ""; switch(stepName) { case "input": title = "Input Query Validation"; subtitle = "Ingested user prompt character validation"; const wasExpanded = currentTelemetry.query_expansion_triggered; html = `

Original Prompt

"${currentTelemetry.original_query}"

Character Count

Total letters: ${currentTelemetry.original_query.length} chars

Query State

Was expanded: ${wasExpanded ? 'YES' : 'NO'}

${wasExpanded ? `

Final search query: "${currentTelemetry.final_query}"

` : ''}
`; break; case "retrieval": title = "Two-Stage Hybrid Retrieval"; subtitle = "FAISS Dense Search & BM25 Sparse Search outputs"; const finalAttemptIdx = currentTelemetry.retrieval_attempts.length - 1; const finalAttempt = currentTelemetry.retrieval_attempts[finalAttemptIdx]; html = `

Index Hits (Final Search Attempt)

${finalAttempt.dense_hits} Dense hits (FAISS)
${finalAttempt.sparse_hits} Sparse hits (BM25)

Merged Candidates (${finalAttempt.total_candidates} deduplicated chunks)

Both retrieval stages were merged and unique documents kept to form the candidates stack.

`; break; case "rerank": title = "Cross-Encoder Re-ranking"; subtitle = "ms-marco-MiniLM-L-6-v2 dynamic relevance scoring"; const lastRetAttempt = currentTelemetry.retrieval_attempts[currentTelemetry.retrieval_attempts.length - 1]; const top5 = lastRetAttempt.chunks; let tableRows = ""; top5.forEach((item, index) => { const scoreClass = item.cross_score >= parseFloat(paramRelevance.value) ? "score-high" : "score-low"; // Truncate text for table const textTrunc = item.text.length > 90 ? item.text.substring(0, 90) + "..." : item.text; tableRows += ` #${index + 1} ${textTrunc} ${item.cross_score.toFixed(3)} `; }); html = `

Top 5 Re-ranked Context Chunks

Cross-Encoder evaluates deep relationships between query and document text. Relevance threshold is set to ${parseFloat(paramRelevance.value).toFixed(2)}.

${tableRows}
Rank Chunk Text Preview Relevance
`; break; case "expansion": title = "Self-Corrective Loop / Query Expansion"; subtitle = "Thesaurus and generative synonym query expansion"; const expCount = currentTelemetry.retrieval_attempts.length - 1; if (expCount === 0) { html = `

Expansion Skipped

The top candidate's re-ranking score (${currentTelemetry.retrieval_attempts[0].top_score.toFixed(3)}) was above the threshold of ${parseFloat(paramRelevance.value).toFixed(2)}. Retrieval was marked as relevant, skipping query expansion.

`; } else { let attemptsHtml = ""; currentTelemetry.retrieval_attempts.forEach((att, idx) => { attemptsHtml += `
Attempt #${idx}: ${att.status.toUpperCase()}

Query used: "${att.query}"

Top relevance score: ${att.top_score.toFixed(3)}

`; }); html = `

Loop Triggered

Initial query score was below threshold. Query expansion was run to broaden search vocabulary.

${attemptsHtml}
`; } break; case "generation": title = "Local LLM Response Drafts"; subtitle = "Factual answer draft generation attempts"; let genAttemptsHtml = ""; currentTelemetry.generation_attempts.forEach((gen, idx) => { const statusClass = gen.status === "verified" ? "badge-success" : "badge-danger"; genAttemptsHtml += `

Draft #${idx + 1} (Temp: ${gen.temperature})

${gen.status}

${gen.response_draft}

`; }); html = `
${genAttemptsHtml}
`; break; case "guard": title = "NLI Hallucination Guard"; subtitle = "Natural Language Inference premise verification"; const lastGenAttemptIdx = currentTelemetry.generation_attempts.length - 1; const lastGenAttempt = currentTelemetry.generation_attempts[lastGenAttemptIdx]; const nliScores = lastGenAttempt.nli_scores; // Format percentages const entPercent = (nliScores.entailment * 100).toFixed(1); const conPercent = (nliScores.contradiction * 100).toFixed(1); const neuPercent = (nliScores.neutral * 100).toFixed(1); html = `

Entailment Verification Metrics

Verifies if the response statement is strictly supported by the retrieved context. If Entailment score falls below ${parseFloat(paramNli.value).toFixed(2)}, it is flagged as a hallucination.

Entailment (Factual Support) ${entPercent}%
Neutral (External Facts) ${neuPercent}%
Contradiction (Hallucinations) ${conPercent}%

Guard Status Result

Blocked Hallucinations Count: ${currentTelemetry.hallucination_blocked_count}

${currentTelemetry.success ? 'The final output passed verification audits successfully and was released to the user console.' : 'The pipeline hit the retry limit, blocking potential hallucinations and outputting a fallback safety response.' }

`; break; } detailsTitle.innerHTML = ` ${title}`; detailsSubtitle.textContent = subtitle; detailsContent.innerHTML = html; }