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}
${chips}
Total: ${total}ms
${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 += `
Chunk ${i + 1} score ${score.toFixed(3)} ${sourceTag}
${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(); }); });