// 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)} .
Rank
Chunk Text Preview
Relevance
${tableRows}
`;
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;
}