function idfColor(n) {
const stops = [
{ t: 0.0, r: 148, g: 163, b: 184 }, { t: 0.3, r: 99, g: 102, b: 241 },
{ t: 0.7, r: 139, g: 92, b: 246 }, { t: 1.0, r: 236, g: 72, b: 153 }
];
const v = Math.max(0, Math.min(1, n));
let lo = stops[0], hi = stops[stops.length - 1];
for (let i = 0; i < stops.length - 1; i++) {
if (v >= stops[i].t && v <= stops[i + 1].t) { lo = stops[i]; hi = stops[i + 1]; break; }
}
const t2 = lo.t === hi.t ? 0 : (v - lo.t) / (hi.t - lo.t);
return `rgb(${Math.round(lo.r + (hi.r - lo.r) * t2)},${Math.round(lo.g + (hi.g - lo.g) * t2)},${Math.round(lo.b + (hi.b - lo.b) * t2)})`;
}
function heatmapColor(v) {
if (v >= 0) { const i = Math.round(v * 255); return `rgb(255,${255 - i},${255 - i})`; }
const i = Math.round(-v * 255); return `rgb(${255 - i},${255 - i},255)`;
}
function escHtml(s) {
return String(s).replace(/&/g, "&").replace(//g, ">");
}
function renderQueryPanel(d) {
const container = document.getElementById("query-panel");
if (!d) { container.innerHTML = ""; return; }
const maxIdfTok = d.tokens.reduce((best, t) => t.idf > best.idf ? t : best, d.tokens[0]);
const pills = d.tokens.map(tok => {
const bg = idfColor(tok.idf_normalized);
const alpha = (0.15 + tok.idf_normalized * 0.85).toFixed(2);
return `
${escHtml(tok.token.replace('##', 'ยท'))}
#${tok.id}
idf ${tok.idf.toFixed(2)}
`;
}).join("");
const cells = d.embedding.map((v, i) =>
``
).join("");
const cpx = d.stats.complexity, cpxPct = (cpx * 100).toFixed(0);
const cpxLabel = cpx < 0.33 ? "Simple" : cpx < 0.66 ? "Moderate" : "Complex";
const embNote = maxIdfTok
? ` "${escHtml(maxIdfTok.token)}" has the highest IDF (${maxIdfTok.idf.toFixed(2)}) โ it pulls the embedding vector the most and takes maximum retrieval weight.
`
: "";
container.innerHTML = `
๐ Query Analysis
${pills}
Query Embedding โ ${d.embedding.length}-dim PCA projection
โ positive โ negative
${cells}
${embNote}
Tokens
${d.stats.token_count}
Unique
${d.stats.unique_tokens}
Avg IDF
${d.stats.avg_idf}
Complexity โ ${cpxLabel} (${cpxPct}%)
`;
}
const STAGES = [
{ key: "embed", label: "Embed", cls: "seg-embed" },
{ key: "vector", label: "Vector", cls: "seg-vector" },
{ key: "bm25", label: "BM25", cls: "seg-bm25" },
{ key: "hybrid", label: "Hybrid", cls: "seg-hybrid" },
{ key: "mmr", label: "MMR", cls: "seg-mmr" },
{ key: "rerank", label: "Rerank", cls: "seg-rerank" },
{ key: "llm", label: "LLM", cls: "seg-llm" },
];
const SEG_COLORS = {
"seg-embed": "#6366f1", "seg-vector": "#0ea5e9", "seg-bm25": "#10b981",
"seg-hybrid": "#f59e0b", "seg-mmr": "#8b5cf6", "seg-rerank": "#ef4444", "seg-llm": "#64748b"
};
function renderLatencyPanel(timings) {
const container = document.getElementById("latency-panel");
if (!timings) { container.innerHTML = ""; return; }
const total = timings.total || STAGES.reduce((s, st) => s + (timings[st.key] || 0), 0);
const segments = STAGES.map((s, idx) => {
const ms = timings[s.key] || 0, pct = total > 0 ? (ms / total) * 100 : 0;
const isLast = idx === STAGES.length - 1;
const inner = pct > 5 ? ms + "ms" : "";
return `${inner}
`;
}).join("");
const smallLabels = STAGES.map(s => {
const ms = timings[s.key] || 0, pct = total > 0 ? (ms / total) * 100 : 0;
if (ms === 0 || pct > 5) return "";
return `${s.label} ${ms}ms`;
}).filter(Boolean).join("");
const smallRow = smallLabels ? `${smallLabels}
` : "";
const chips = STAGES.map(s => {
const ms = timings[s.key] || 0, pct = total > 0 ? ((ms / total) * 100).toFixed(1) : "0.0";
return `
${s.label}
${ms}ms
${pct}% `;
}).join("");
const slowest = STAGES.reduce((b, s) => (timings[s.key] || 0) > (timings[b.key] || 0) ? s : b, STAGES[0]);
const slowestMs = timings[slowest.key] || 0;
const slowestPct = total > 0 ? Math.round((slowestMs / total) * 100) : 0;
const slowestCallout = slowestMs > 0
? ` ${slowest.label} is taking the most time โ ${slowestMs}ms (${slowestPct}% of total)
`
: "";
const retrieval = STAGES.filter(s => s.key !== "llm");
const bottleneck = retrieval.reduce((b, s) => (timings[s.key] || 0) > (timings[b.key] || 0) ? s : b, retrieval[0]);
const bPct = total > 0 ? Math.round((timings[bottleneck.key] / total) * 100) : 0;
const retrievalCallout = bPct > 15 && slowest.key !== "llm"
? `โ Retrieval bottleneck: ${bottleneck.label} taking ${bPct}% of total (${timings[bottleneck.key]}ms)
`
: "";
const llmPct = total > 0 ? Math.round(((timings.llm || 0) / total) * 100) : 0;
const llmCallout = llmPct > 50
? `LLM generation is ${llmPct}% of total time (${timings.llm}ms) โ retrieval is fast
`
: "";
container.innerHTML = `
โฑ Latency Timeline
${segments}
${smallRow}
${slowestCallout}${retrievalCallout}${llmCallout}
`;
}
function scoreColor(s) {
if (s >= 0.7) return { bg: "#dcfce7", color: "#166534" };
if (s >= 0.4) return { bg: "#fef9c3", color: "#854d0e" };
return { bg: "#fee2e2", color: "#991b1b" };
}
function renderChunksPanel(chunks, scores, raw_scores, sources) {
const container = document.getElementById("chunks-panel");
let html = `๐ Context Chunks
`;
const order = scores
.map((s, i) => ({ i, s }))
.sort((a, b) => b.s - a.s)
.map(x => x.i);
order.forEach(i => {
const score = scores[i];
const sc = scoreColor(score);
const src = sources[i] || {};
const url = src.url || "";
const title = src.title || url;
let displayUrl = url;
try {
const u = new URL(url);
displayUrl = u.hostname + u.pathname.replace(/\/$/, "");
} catch (_) { }
const sourceTag = url
? `
๐${escHtml(displayUrl)}
`
: "";
html += `
${marked.parse(chunks[i])}
`;
});
container.innerHTML = html;
hljs.highlightAll();
}
async function ask() {
const query = document.getElementById("query").value.trim();
if (!query) return;
document.getElementById("answer").innerHTML = "Thinking...
";
document.getElementById("latency-panel").innerHTML = "";
document.getElementById("query-panel").innerHTML = "Analyzing query...
";
document.getElementById("retrieval-panel").innerHTML = "";
document.getElementById("mmr-panel").innerHTML = "";
document.getElementById("chunks-panel").innerHTML = "";
const [askRes, analyzeRes] = await Promise.all([
fetch("/ask", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ query }) }),
fetch("/analyze_query", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ query }) })
]);
const [data, analysis] = await Promise.all([askRes.json(), analyzeRes.json()]);
// 1. Answer
document.getElementById("answer").innerHTML =
`Answer:
${marked.parse(data.answer)}
`;
hljs.highlightAll();
// 2. Latency
renderLatencyPanel(data.timings);
// 3. Query Analysis
renderQueryPanel(analysis);
// 4. Retrieval Comparison
renderRetrievalTable(data.comparison_rows);
// 5. MMR
renderMMRPanel(data.mmr_data);
// 6. Chunks
renderChunksPanel(data.chunks, data.scores, data.raw_scores, data.sources);
}
document.addEventListener("DOMContentLoaded", () => {
document.getElementById("query").addEventListener("keydown", e => {
if (e.key === "Enter") ask();
});
});