Health-Insurance-Copilot / frontend /dev_console.html
Nagendravarma
Optimize comparison search: bypass redundant KG search, avoid thread-safety ChromaDB init lock error, and prevent false positive tier lighting up in dev console
9b91537
Raw
History Blame Contribute Delete
114 kB
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>🛠️ HealthGuard RAG — Developer Console v2</title>
<meta name="description" content="Live developer console for the Health Insurance RAG pipeline — real-time orchestration tracing, latency profiling, and memory inspection.">
<script src="https://d3js.org/d3.v7.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
<style>
@import url('https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@300;400;500;600;700;800&family=JetBrains+Mono:wght@400;500;700&display=swap');
* { box-sizing: border-box; margin: 0; padding: 0; }
:root {
--glow-primary: rgba(99,102,241,0.4);
--glow-success: rgba(16,185,129,0.4);
--border-glow: rgba(255,255,255,0.08);
--sidebar-bg: rgba(10,17,39,0.97);
--main-bg: radial-gradient(ellipse at 30% 20%, #0f1a3a 0%, #060a16 60%, #03050f 100%);
--glass: rgba(255,255,255,0.04);
--text-primary: #f1f5f9;
--text-muted: #64748b;
--accent: #6366f1;
--success: #10b981;
--warn: #f59e0b;
--danger: #ef4444;
}
body {
background: #060a16;
background-image: var(--main-bg);
color: var(--text-primary);
font-family: 'Plus Jakarta Sans', sans-serif;
height: 100vh;
display: flex;
flex-direction: column;
overflow: hidden;
}
/* ── HEADER ── */
.header {
display: flex;
align-items: center;
gap: 16px;
padding: 14px 28px;
background: rgba(8,14,32,0.9);
backdrop-filter: blur(20px);
border-bottom: 1px solid var(--border-glow);
flex-shrink: 0;
z-index: 10;
}
.header-logo {
font-size: 1.3rem;
background: linear-gradient(135deg, #818cf8, #c084fc);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
font-weight: 800;
white-space: nowrap;
}
.header-badge {
font-size: 0.6rem; font-weight: 800; padding: 3px 9px;
border-radius: 20px;
background: linear-gradient(90deg, #4f46e5, #7c3aed);
color: #fff; letter-spacing: 0.08em; text-transform: uppercase;
box-shadow: 0 0 12px rgba(99,102,241,0.4);
}
.header-badge.v2 {
background: linear-gradient(90deg, #059669, #0891b2);
box-shadow: 0 0 12px rgba(6,182,212,0.4);
}
/* ── Health chips ── */
.health-strip {
display: flex; gap: 8px; margin-left: 4px;
}
.hchip {
display: flex; align-items: center; gap: 5px;
padding: 4px 10px; border-radius: 20px;
font-size: 0.65rem; font-weight: 700;
background: rgba(255,255,255,0.05);
border: 1px solid rgba(255,255,255,0.1);
color: #94a3b8;
transition: all 0.3s;
cursor: default;
}
.hchip .dot { width: 6px; height: 6px; border-radius: 50%; background: #475569; flex-shrink: 0; }
.hchip.ok { border-color: rgba(16,185,129,0.3); color: #6ee7b7; }
.hchip.ok .dot { background: #10b981; box-shadow: 0 0 6px #10b981; }
.hchip.err { border-color: rgba(239,68,68,0.3); color: #fca5a5; }
.hchip.err .dot { background: #ef4444; }
.session-id {
margin-left: auto; font-family: 'JetBrains Mono', monospace;
font-size: 0.68rem; color: #94a3b8;
background: rgba(255,255,255,0.04);
padding: 4px 10px; border-radius: 8px; border: 1px solid var(--border-glow);
white-space: nowrap;
}
.header-actions { display: flex; gap: 8px; align-items: center; }
.hbtn {
background: rgba(255,255,255,0.05); border: 1px solid rgba(255,255,255,0.12);
color: #cbd5e1; padding: 5px 11px; border-radius: 7px; cursor: pointer;
font-size: 0.68rem; font-weight: 600; transition: all 0.2s;
font-family: inherit;
}
.hbtn:hover { background: rgba(255,255,255,0.1); color: #fff; }
.hbtn.kbd { font-family: 'JetBrains Mono', monospace; font-size: 0.62rem; }
/* ── BODY ── */
.body { display: flex; flex: 1; overflow: hidden; }
/* ── LEFT PANEL ── */
.left {
width: 460px; min-width: 440px;
border-right: 1px solid var(--border-glow);
display: flex; flex-direction: column;
background: var(--sidebar-bg);
backdrop-filter: blur(20px);
z-index: 5; overflow-y: auto;
}
/* ── Query area ── */
.query-area { padding: 24px 28px; border-bottom: 1px solid var(--border-glow); }
.query-area label {
display: block; font-size: 0.67rem; font-weight: 700; color: var(--text-muted);
text-transform: uppercase; letter-spacing: 0.08em; margin-bottom: 8px;
}
.query-area textarea {
width: 100%; background: rgba(255,255,255,0.03);
border: 1px solid rgba(255,255,255,0.08); border-radius: 10px;
padding: 12px; color: #fff; font-family: inherit; font-size: 0.85rem;
line-height: 1.5; resize: none; outline: none; transition: all 0.3s;
}
.query-area textarea:focus {
border-color: var(--accent);
box-shadow: 0 0 14px rgba(99,102,241,0.15);
}
/* Preset queries */
.presets { margin-top: 8px; }
.presets-toggle {
width: 100%; background: none; border: 1px dashed rgba(255,255,255,0.1);
border-radius: 8px; color: #64748b; font-size: 0.72rem; font-family: inherit;
padding: 6px; cursor: pointer; display: flex; align-items: center;
justify-content: center; gap: 6px; transition: all 0.2s;
}
.presets-toggle:hover { border-color: rgba(99,102,241,0.4); color: #818cf8; }
.preset-list {
display: none; flex-direction: column; gap: 4px; margin-top: 8px;
}
.preset-list.open { display: flex; }
.preset-item {
padding: 8px 12px; border-radius: 8px; background: rgba(255,255,255,0.03);
border: 1px solid rgba(255,255,255,0.07); cursor: pointer; font-size: 0.78rem;
color: #cbd5e1; transition: all 0.2s; display: flex; gap: 8px; align-items: flex-start;
}
.preset-item:hover { background: rgba(99,102,241,0.1); border-color: rgba(99,102,241,0.3); color: #a5b4fc; }
.preset-icon { flex-shrink: 0; margin-top: 1px; }
.preset-header {
font-size: 0.68rem; font-weight: 700; text-transform: uppercase; color: #64748b;
margin-top: 10px; margin-bottom: 2px; padding-left: 4px; letter-spacing: 0.05em;
}
.preset-header:first-child { margin-top: 2px; }
.run-btn {
width: 100%; margin-top: 10px; padding: 11px; border: none; border-radius: 9px;
background: linear-gradient(135deg, #4f46e5, #7c3aed); color: #fff;
font-size: 0.88rem; font-weight: 700; cursor: pointer; transition: all 0.25s;
display: flex; align-items: center; justify-content: center; gap: 8px;
box-shadow: 0 4px 18px rgba(99,102,241,0.35); font-family: inherit;
}
.run-btn:hover { transform: translateY(-1px); box-shadow: 0 6px 24px rgba(99,102,241,0.55); }
.run-btn:active { transform: translateY(1px); }
.run-btn:disabled { opacity: 0.5; cursor: not-allowed; transform: none; box-shadow: none; }
/* ── Intent + Confidence ── */
.meta-row {
padding: 16px 28px; border-bottom: 1px solid var(--border-glow);
display: none; gap: 12px; align-items: center; flex-wrap: wrap;
}
.meta-row.visible { display: flex; }
.intent-label, .conf-label {
font-size: 0.66rem; font-weight: 700; color: var(--text-muted);
text-transform: uppercase; letter-spacing: 0.08em; margin-right: 4px;
}
.intent-pill {
display: inline-flex; align-items: center; gap: 5px;
padding: 4px 12px; border-radius: 20px; font-size: 0.74rem; font-weight: 700;
background: rgba(99,102,241,0.15); color: #a5b4fc;
border: 1px solid rgba(99,102,241,0.3); text-transform: uppercase; letter-spacing: 0.05em;
}
.conf-badge {
display: inline-flex; align-items: center; gap: 5px;
padding: 4px 12px; border-radius: 20px; font-size: 0.74rem; font-weight: 700;
border: 1px solid; text-transform: uppercase; letter-spacing: 0.04em;
}
.conf-badge.HIGH { background: rgba(16,185,129,0.15); color:#34d399; border-color:rgba(16,185,129,0.3); }
.conf-badge.MEDIUM{ background: rgba(245,158,11,0.15); color:#fbbf24; border-color:rgba(245,158,11,0.3); }
.conf-badge.LOW { background: rgba(239,68,68,0.15); color:#f87171; border-color:rgba(239,68,68,0.3); }
.conf-badge.BLOCKED{background: rgba(100,116,139,0.15);color:#94a3b8; border-color:rgba(100,116,139,0.3);}
/* ── Log area ── */
.log-area {
flex: 1; display: flex; flex-direction: column;
padding: 20px 28px; min-height: 260px; flex-shrink: 0;
}
.log-header {
display: flex; align-items: center; justify-content: space-between;
margin-bottom: 10px; flex-shrink: 0;
}
.log-header label {
font-size: 0.67rem; font-weight: 700; color: var(--text-muted);
text-transform: uppercase; letter-spacing: 0.08em;
display: flex; align-items: center; gap: 6px;
}
.log-controls { display: flex; gap: 6px; align-items: center; }
.log-search {
background: rgba(255,255,255,0.04); border: 1px solid rgba(255,255,255,0.08);
border-radius: 6px; padding: 4px 8px; color: #cbd5e1; font-size: 0.7rem;
outline: none; font-family: 'JetBrains Mono', monospace; width: 120px;
transition: all 0.2s;
}
.log-search:focus { border-color: var(--accent); width: 150px; }
.filter-btn {
padding: 3px 8px; border-radius: 5px; font-size: 0.62rem; font-weight: 700;
cursor: pointer; border: 1px solid; background: transparent;
transition: all 0.15s; font-family: inherit; text-transform: uppercase;
letter-spacing: 0.04em;
}
.filter-btn.ALL { color:#94a3b8; border-color:rgba(148,163,184,0.2); }
.filter-btn.ALL.active { background: rgba(148,163,184,0.15); color:#e2e8f0; }
.filter-btn.CLASSIFY { color:#c084fc; border-color:rgba(139,92,246,0.2); }
.filter-btn.CLASSIFY.active { background:rgba(139,92,246,0.15); }
.filter-btn.RETRIEVE { color:#fbbf24; border-color:rgba(245,158,11,0.2); }
.filter-btn.RETRIEVE.active { background:rgba(245,158,11,0.15); }
.filter-btn.SYNTH { color:#34d399; border-color:rgba(16,185,129,0.2); }
.filter-btn.SYNTH.active { background:rgba(16,185,129,0.15); }
.filter-btn.ERROR { color:#f87171; border-color:rgba(239,68,68,0.2); }
.filter-btn.ERROR.active { background:rgba(239,68,68,0.15); }
.log-list-wrapper {
flex: 1; overflow-y: auto;
border: 1px solid rgba(255,255,255,0.06); border-radius: 12px;
background: rgba(2,4,10,0.7); padding: 16px;
font-family: 'JetBrains Mono', monospace;
box-shadow: inset 0 4px 15px rgba(0,0,0,0.4);
}
.log-entry {
display: flex; gap: 8px; margin-bottom: 8px; font-size: 0.72rem; line-height: 1.5;
border-bottom: 1px solid rgba(255,255,255,0.02); padding-bottom: 6px;
animation: slideIn 0.2s ease-out forwards;
transition: opacity 0.2s;
}
.log-entry.hidden { display: none; }
@keyframes slideIn { from { opacity:0; transform:translateY(6px); } to { opacity:1; transform:translateY(0); } }
.log-icon { flex-shrink: 0; font-size: 0.82rem; }
.log-text { color: #e2e8f0; flex: 1; word-break: break-word; }
.log-tag {
font-size: 0.6rem; font-weight: 700; padding: 1px 5px; border-radius: 3px;
margin-right: 6px; text-transform: uppercase; display: inline-block; letter-spacing: 0.04em;
}
.tag-guard { background: rgba(100,116,139,0.2); color:#94a3b8; border:1px solid rgba(100,116,139,0.3); }
.tag-classify { background: rgba(139,92,246,0.2); color:#c084fc; border:1px solid rgba(139,92,246,0.3); }
.tag-retrieve { background: rgba(245,158,11,0.15); color:#fbbf24; border:1px solid rgba(245,158,11,0.25); }
.tag-synthesize{background: rgba(16,185,129,0.15); color:#34d399; border:1px solid rgba(16,185,129,0.25); }
.tag-critique { background: rgba(59,130,246,0.15); color:#93c5fd; border:1px solid rgba(59,130,246,0.25); }
.tag-system { background: rgba(99,102,241,0.15); color:#818cf8; border:1px solid rgba(99,102,241,0.25); }
.tag-error { background: rgba(239,68,68,0.15); color:#f87171; border:1px solid rgba(239,68,68,0.25); }
/* ── Answer card ── */
.answer-card {
margin: 0 28px 24px; padding: 20px;
background: linear-gradient(135deg, rgba(16,185,129,0.08) 0%, rgba(16,185,129,0.02) 100%);
border: 1px solid rgba(16,185,129,0.3); border-radius: 14px;
display: none; animation: fadeUp 0.3s ease-out;
box-shadow: 0 4px 18px rgba(16,185,129,0.08); flex-shrink: 0;
}
@keyframes fadeUp { from { opacity:0; transform:translateY(8px); } to { opacity:1; transform:translateY(0); } }
.answer-header { display:flex; align-items:center; justify-content:space-between; margin-bottom:10px; }
.ans-label { font-size:0.67rem; font-weight:800; color:#34d399; text-transform:uppercase; letter-spacing:0.08em; display:flex; align-items:center; gap:5px; }
.copy-btn {
background: rgba(52,211,153,0.1); border: 1px solid rgba(52,211,153,0.25);
color: #34d399; padding: 4px 10px; border-radius: 6px; font-size: 0.65rem;
font-weight: 700; cursor: pointer; font-family: inherit; transition: all 0.2s;
}
.copy-btn:hover { background: rgba(52,211,153,0.2); }
.ans-text {
font-size: 0.82rem; line-height: 1.7; color: #e2e8f0;
}
/* Markdown rendering inside answer */
.ans-text h1,.ans-text h2,.ans-text h3 { color:#a5b4fc; margin:10px 0 5px; font-size:0.9rem; }
.ans-text p { margin-bottom:8px; }
.ans-text ul,.ans-text ol { padding-left:16px; margin-bottom:8px; }
.ans-text li { margin-bottom:4px; }
.ans-text strong { color:#e2e8f0; }
.ans-text code { background:rgba(255,255,255,0.06); padding:1px 5px; border-radius:3px; font-family:'JetBrains Mono',monospace; font-size:0.78rem; }
.ans-text blockquote { border-left:2px solid #6366f1; padding-left:10px; color:#94a3b8; margin:8px 0; }
/* ── Memory inspector ── */
.memory-panel {
margin: 0 28px 16px; padding: 16px;
background: rgba(6,182,212,0.05); border: 1px solid rgba(6,182,212,0.2);
border-radius: 12px; display: none; flex-shrink: 0;
}
.memory-panel.open { display: block; }
.mem-header { display:flex; align-items:center; justify-content:space-between; margin-bottom:10px; }
.mem-label { font-size:0.67rem; font-weight:800; color:#22d3ee; text-transform:uppercase; letter-spacing:0.08em; }
.mem-actions { display:flex; gap:6px; }
.mem-btn {
padding: 3px 9px; border-radius: 6px; font-size: 0.65rem; font-weight:700;
cursor:pointer; font-family:inherit; border: 1px solid; transition: all 0.2s;
}
.mem-refresh { background:rgba(6,182,212,0.1); border-color:rgba(6,182,212,0.3); color:#22d3ee; }
.mem-refresh:hover { background:rgba(6,182,212,0.2); }
.mem-wipe { background:rgba(239,68,68,0.1); border-color:rgba(239,68,68,0.3); color:#f87171; }
.mem-wipe:hover { background:rgba(239,68,68,0.2); }
.mem-facts { display:flex; flex-direction:column; gap:5px; max-height:120px; overflow-y:auto; }
.mem-fact {
padding: 6px 10px; border-radius: 7px; background: rgba(6,182,212,0.07);
border: 1px solid rgba(6,182,212,0.15); font-size:0.72rem; color:#a5f3fc;
font-family:'JetBrains Mono',monospace; line-height:1.4;
}
.mem-empty { font-size:0.72rem; color:#475569; font-style:italic; }
/* ── RIGHT PANEL ── */
.right {
flex: 1; overflow: hidden; padding: 24px 32px;
display: flex; flex-direction: column;
}
.diagram-label {
font-size: 0.68rem; font-weight: 700; color: var(--text-muted);
text-transform: uppercase; letter-spacing: 0.08em; margin-bottom: 12px;
display: flex; align-items: center; gap: 8px;
}
.diagram-actions { display:flex; gap:8px; margin-left:auto; }
.diag-btn {
background:rgba(255,255,255,0.04); border:1px solid rgba(255,255,255,0.1);
color:#94a3b8; padding:4px 10px; border-radius:6px; font-size:0.65rem;
font-weight:600; cursor:pointer; font-family:inherit; transition:all 0.2s;
}
.diag-btn:hover { background:rgba(255,255,255,0.09); color:#fff; }
#svg-wrap {
flex: 1; width: 100%;
background: rgba(8,14,32,0.5); border: 1px solid rgba(255,255,255,0.06);
border-radius: 18px; padding: 20px;
display: flex; align-items: center; justify-content: center;
box-shadow: inset 0 4px 30px rgba(0,0,0,0.3);
overflow: hidden;
}
svg { width:100%; height:100%; overflow:visible; }
/* ── Node Graph styling ── */
.n rect { stroke-width:1.5; transition:all 0.35s cubic-bezier(0.16,1,0.3,1); rx:14; ry:14; }
.n.idle rect { fill:#080e22; stroke:rgba(255,255,255,0.1); }
.n.active rect { fill:rgba(124,58,237,0.25); stroke:#a78bfa; stroke-width:2; filter:drop-shadow(0 0 10px rgba(139,92,246,0.5)); }
.n.done rect { fill:rgba(16,185,129,0.13); stroke:#10b981; stroke-width:1.8; }
.n.skip rect { fill:#050811; stroke:rgba(255,255,255,0.03); opacity:0.3; }
.n.hit rect { fill:rgba(16,185,129,0.22) !important; stroke:#10b981 !important; stroke-width:2.5px !important; filter:drop-shadow(0 0 10px rgba(16,185,129,0.6)) !important; }
.n.miss rect { fill:rgba(249,115,22,0.18) !important; stroke:#f97316 !important; stroke-width:2.2px !important; filter:drop-shadow(0 0 8px rgba(249,115,22,0.4)) !important; }
.n .icon { font-size:14px; }
.n .title { font-family:'Plus Jakarta Sans',sans-serif; font-size:10px; font-weight:700; fill:#e2e8f0; }
.n .sub { font-family:'Plus Jakarta Sans',sans-serif; font-size:8px; fill:#64748b; font-weight:500; }
/* Latency badge on nodes */
.n .lat-badge { font-family:'JetBrains Mono',monospace; font-size:7.5px; font-weight:700; }
/* Special node colors */
#nd-langsmith.idle rect { fill:rgba(6,182,212,0.06); stroke:rgba(6,182,212,0.2); }
#nd-langsmith.active rect{ fill:rgba(6,182,212,0.22); stroke:#22d3ee; stroke-width:2; filter:drop-shadow(0 0 10px rgba(6,182,212,0.5)); }
#nd-langsmith.done rect { fill:rgba(6,182,212,0.13); stroke:#06b6d4; stroke-width:1.8; }
#nd-query_guard.active rect { fill:rgba(239,68,68,0.18); stroke:#f87171; stroke-width:2; filter:drop-shadow(0 0 10px rgba(239,68,68,0.4)); }
#nd-query_guard.done rect { fill:rgba(239,68,68,0.08); stroke:rgba(239,68,68,0.5); }
#nd-context_check.active rect{ fill:rgba(59,130,246,0.2); stroke:#93c5fd; stroke-width:2; filter:drop-shadow(0 0 10px rgba(59,130,246,0.4)); }
#nd-context_check.done rect { fill:rgba(59,130,246,0.1); stroke:rgba(59,130,246,0.5); }
#nd-self_critique.active rect{ fill:rgba(245,158,11,0.18); stroke:#fbbf24; stroke-width:2; filter:drop-shadow(0 0 10px rgba(245,158,11,0.4)); }
#nd-self_critique.done rect { fill:rgba(245,158,11,0.08); stroke:rgba(245,158,11,0.5); }
#nd-confidence.active rect { fill:rgba(16,185,129,0.2); stroke:#34d399; stroke-width:2; filter:drop-shadow(0 0 10px rgba(16,185,129,0.4)); }
#nd-confidence.done rect { fill:rgba(16,185,129,0.1); stroke:rgba(16,185,129,0.5); }
.pulse { fill:none; stroke:#a78bfa; stroke-width:1.5; opacity:0; pointer-events:none; }
.n.active .pulse { animation: pulseNode 1.8s ease-out infinite; }
@keyframes pulseNode { 0%{r:30px;opacity:0.7;} 100%{r:65px;opacity:0;} }
.edge { fill:none; stroke:rgba(148,163,184,0.15); stroke-width:1.8; stroke-dasharray:5 4; transition:all 0.3s; }
.edge.active-edge { stroke:#818cf8; stroke-width:2.5; stroke-dasharray:8 3; animation:dash 0.9s linear infinite; filter:drop-shadow(0 0 4px rgba(129,140,248,0.6)); }
.edge.done-edge { stroke:#10b981; stroke-width:1.8; stroke-dasharray:none; }
.edge.retry-edge { stroke:#f59e0b; stroke-dasharray:6 4; }
.edge.retry-edge.active-edge { stroke:#fbbf24; }
@keyframes dash { to { stroke-dashoffset:-20; } }
.intent-tag { rx:6; ry:6; transition:all 0.3s; }
.intent-tag.active-intent { fill:rgba(124,58,237,0.25); stroke:#a78bfa; stroke-width:1.5; }
.intent-tag.inactive-intent { fill:rgba(255,255,255,0.02); stroke:rgba(255,255,255,0.06); }
/* ── STATUS BAR ── */
.status {
padding: 12px 28px; border-top: 1px solid var(--border-glow);
background: rgba(8,14,32,0.9); display: flex; align-items: center;
gap: 10px; font-size: 0.75rem; color: #64748b; flex-shrink: 0; font-weight: 500;
}
.dot { width:7px; height:7px; border-radius:50%; background:#475569; flex-shrink:0; transition:all 0.3s; }
.dot.running { background:#6366f1; box-shadow:0 0 8px #6366f1; animation:blink 1.2s infinite; }
.dot.done { background:#10b981; box-shadow:0 0 8px #10b981; }
.dot.error { background:#ef4444; box-shadow:0 0 8px #ef4444; }
@keyframes blink { 50%{ opacity:0.4; } }
.elapsed { margin-left:auto; font-family:'JetBrains Mono',monospace; font-size:0.68rem; color:#475569; }
/* ── TOOLTIP ── */
#tooltip {
position:absolute; background:rgba(8,14,32,0.97);
border:1px solid rgba(99,102,241,0.4); border-radius:10px; padding:14px;
color:#e2e8f0; font-size:0.78rem; pointer-events:none; display:none;
z-index:2000; box-shadow:0 10px 30px rgba(0,0,0,0.6); backdrop-filter:blur(12px);
max-width:300px; line-height:1.6;
}
#tooltip b { color:#a5b4fc; display:block; margin-bottom:6px; font-size:0.68rem; text-transform:uppercase; letter-spacing:0.06em; }
#tooltip .lat-info { font-family:'JetBrains Mono',monospace; font-size:0.72rem; color:#34d399; margin-bottom:4px; }
#tooltip div { margin-bottom:4px; border-left:2px solid #6366f1; padding-left:8px; }
/* ── LOGIN OVERLAY ── */
#login-overlay {
position:fixed; inset:0;
background:rgba(4,7,16,0.97); backdrop-filter:blur(30px);
z-index:9999; display:flex; align-items:center; justify-content:center;
flex-direction:column; transition:opacity 0.3s;
}
.login-box {
background:rgba(15,23,42,0.7); border:1px solid rgba(99,102,241,0.2);
padding:52px 44px; border-radius:28px; text-align:center;
box-shadow:0 20px 60px rgba(0,0,0,0.7); width:380px;
}
.login-logo { font-size:2.5rem; margin-bottom:12px; }
.login-box h2 { color:#fff; font-size:1.35rem; font-weight:800; margin-bottom:6px; }
.login-box p { color:#64748b; font-size:0.8rem; margin-bottom:24px; }
.login-box input {
width:100%; padding:14px; border-radius:10px;
background:rgba(0,0,0,0.4); border:1px solid rgba(255,255,255,0.1);
color:#fff; font-size:1.4rem; text-align:center; margin-bottom:18px;
outline:none; font-family:'JetBrains Mono',monospace; letter-spacing:0.2em; transition:all 0.3s;
}
.login-box input:focus { border-color:#6366f1; box-shadow:0 0 14px rgba(99,102,241,0.25); }
.login-box button {
width:100%; padding:13px; border-radius:10px;
background:linear-gradient(135deg,#4f46e5,#7c3aed); color:#fff;
border:none; font-weight:700; font-size:0.9rem; cursor:pointer;
transition:all 0.25s; box-shadow:0 4px 14px rgba(99,102,241,0.4);
font-family:inherit;
}
.login-box button:hover { box-shadow:0 6px 20px rgba(99,102,241,0.6); }
.login-error { color:#ef4444; font-size:0.76rem; margin-top:12px; display:none; font-weight:600; }
/* ── CMD-K PALETTE ── */
#palette-overlay {
position:fixed; inset:0; background:rgba(0,0,0,0.7); backdrop-filter:blur(6px);
z-index:8888; display:none; align-items:flex-start; justify-content:center; padding-top:18vh;
}
#palette-overlay.open { display:flex; }
.palette-box {
background:rgba(12,20,42,0.98); border:1px solid rgba(99,102,241,0.3);
border-radius:16px; width:520px; overflow:hidden;
box-shadow:0 24px 60px rgba(0,0,0,0.8);
}
.palette-title {
padding:18px 20px 10px; font-size:0.68rem; font-weight:700; color:#64748b;
text-transform:uppercase; letter-spacing:0.08em; border-bottom:1px solid rgba(255,255,255,0.06);
}
.palette-item {
display:flex; align-items:center; gap:14px; padding:13px 20px;
cursor:pointer; transition:background 0.15s; font-size:0.83rem;
}
.palette-item:hover { background:rgba(99,102,241,0.1); }
.palette-key {
font-family:'JetBrains Mono',monospace; font-size:0.68rem; font-weight:700;
padding:2px 8px; border-radius:5px; background:rgba(255,255,255,0.07);
border:1px solid rgba(255,255,255,0.12); color:#94a3b8; min-width:36px; text-align:center;
}
.palette-desc { color:#cbd5e1; }
.palette-close { padding:10px 20px; text-align:right; border-top:1px solid rgba(255,255,255,0.06); }
.palette-close span { font-size:0.7rem; color:#475569; cursor:pointer; }
.palette-close span:hover { color:#94a3b8; }
/* Scrollbars */
::-webkit-scrollbar { width:4px; background:transparent; }
::-webkit-scrollbar-thumb { background:rgba(255,255,255,0.07); border-radius:10px; }
/* ── NODE DETAIL MODAL ── */
#node-modal-backdrop {
position:fixed; inset:0;
background:rgba(0,0,0,0.55); backdrop-filter:blur(8px);
z-index:5000; display:none; align-items:center; justify-content:center;
}
#node-modal-backdrop.open { display:flex; }
.node-modal {
background:rgba(10,18,40,0.97);
border:1px solid rgba(99,102,241,0.35);
border-radius:20px; padding:28px 30px;
width:480px; max-height:75vh; overflow-y:auto;
box-shadow:0 24px 70px rgba(0,0,0,0.75);
animation: modalIn 0.22s cubic-bezier(0.16,1,0.3,1);
}
@keyframes modalIn { from{opacity:0;transform:scale(0.93) translateY(12px);} to{opacity:1;transform:scale(1) translateY(0);} }
.nm-header {
display:flex; align-items:center; justify-content:space-between; margin-bottom:18px;
}
.nm-title { font-size:1rem; font-weight:800; color:#e2e8f0; display:flex; align-items:center; gap:8px; }
.nm-badge {
font-size:0.6rem; font-weight:800; padding:2px 8px; border-radius:10px;
background:rgba(99,102,241,0.2); color:#818cf8;
border:1px solid rgba(99,102,241,0.3); text-transform:uppercase; letter-spacing:0.06em;
}
.nm-badge.new { background:rgba(16,185,129,0.15); color:#34d399; border-color:rgba(16,185,129,0.3); }
.nm-close {
width:28px; height:28px; border-radius:8px; border:1px solid rgba(255,255,255,0.12);
background:rgba(255,255,255,0.05); color:#94a3b8; cursor:pointer;
display:flex; align-items:center; justify-content:center; font-size:0.9rem;
transition:all 0.2s;
}
.nm-close:hover { background:rgba(239,68,68,0.15); color:#f87171; border-color:rgba(239,68,68,0.3); }
.nm-desc { font-size:0.78rem; color:#64748b; margin-bottom:18px; line-height:1.6; }
.nm-section { margin-bottom:16px; }
.nm-section-title {
font-size:0.65rem; font-weight:800; color:#475569;
text-transform:uppercase; letter-spacing:0.09em; margin-bottom:8px;
display:flex; align-items:center; gap:5px;
}
.nm-stat-row {
display:flex; gap:8px; flex-wrap:wrap; margin-bottom:10px;
}
.nm-stat {
padding:6px 12px; border-radius:8px;
background:rgba(255,255,255,0.04); border:1px solid rgba(255,255,255,0.08);
font-size:0.73rem; color:#cbd5e1;
}
.nm-stat span { font-family:'JetBrains Mono',monospace; color:#a5b4fc; font-weight:700; }
.nm-card {
padding:10px 14px; border-radius:10px;
background:rgba(255,255,255,0.03); border:1px solid rgba(255,255,255,0.07);
font-size:0.75rem; color:#e2e8f0; line-height:1.5;
margin-bottom:6px; font-family:'JetBrains Mono',monospace;
border-left:3px solid;
}
.nm-card.variant { border-left-color:#6366f1; }
.nm-card.subq { border-left-color:#a78bfa; }
.nm-card.check { border-left-color:#10b981; }
.nm-card.warning { border-left-color:#f59e0b; }
.nm-card.good { border-left-color:#10b981; }
.nm-card.bad { border-left-color:#ef4444; }
.nm-card.memory { border-left-color:#22d3ee; }
.nm-card.info { border-left-color:#818cf8; }
.nm-empty { font-size:0.75rem; color:#475569; font-style:italic; }
.nm-lat {
display:inline-flex; align-items:center; gap:5px;
padding:5px 12px; border-radius:8px;
background:rgba(16,185,129,0.1); border:1px solid rgba(16,185,129,0.25);
font-family:'JetBrains Mono',monospace; font-size:0.78rem; color:#34d399; font-weight:700;
margin-bottom:14px;
}
</style>
</head>
<body>
<!-- ── LOGIN OVERLAY ── -->
<div id="login-overlay">
<div class="login-box">
<div class="login-logo">🛡️</div>
<h2>HealthGuard Console</h2>
<p>Enter your developer passkey to proceed</p>
<input type="password" id="pass-input" placeholder="••••" maxlength="4" autocomplete="off">
<button onclick="checkPass()">Unlock Console ⚡</button>
<div id="login-err" class="login-error">🔒 Invalid authorization code.</div>
</div>
</div>
<!-- ── CMD-K PALETTE ── -->
<div id="palette-overlay">
<div class="palette-box">
<div class="palette-title">⌨️ Command Palette</div>
<div class="palette-item" onclick="closePalette();run()"><span class="palette-key">R</span><span class="palette-desc">Run current query</span></div>
<div class="palette-item" onclick="closePalette();clearAll()"><span class="palette-key">C</span><span class="palette-desc">Clear logs & reset diagram</span></div>
<div class="palette-item" onclick="closePalette();toggleMemory()"><span class="palette-key">M</span><span class="palette-desc">Toggle memory inspector</span></div>
<div class="palette-item" onclick="closePalette();downloadTrace()"><span class="palette-key">E</span><span class="palette-desc">Export trace as JSON</span></div>
<div class="palette-item" onclick="closePalette();togglePresets()"><span class="palette-key">P</span><span class="palette-desc">Show preset queries</span></div>
<div class="palette-item" onclick="closePalette()"><span class="palette-key">Esc</span><span class="palette-desc">Close palette</span></div>
<div class="palette-close"><span onclick="closePalette()">Close ✕</span></div>
</div>
</div>
<!-- ── HEADER ── -->
<div class="header">
<div class="header-logo">🛡️ HealthGuard</div>
<span class="header-badge">RAG DEV CONSOLE</span>
<span class="header-badge v2">v2.0</span>
<div class="health-strip">
<div class="hchip" id="hc-openai"><span class="dot"></span>OPENAI</div>
<div class="hchip" id="hc-vector"><span class="dot"></span>VECTOR DB</div>
<div class="hchip" id="hc-graph"><span class="dot"></span>GRAPH</div>
<div class="hchip" id="hc-sessions"><span class="dot"></span>SESSIONS: —</div>
</div>
<span class="session-id" id="sid"></span>
<div class="header-actions">
<button class="hbtn kbd" onclick="openPalette()">⌘K</button>
<button class="hbtn" onclick="downloadTrace()">⬇ Trace</button>
<button class="hbtn" onclick="toggleMemory()">🧠 Memory</button>
<button class="hbtn" onclick="clearAll()">↺ Reset</button>
</div>
</div>
<!-- ── BODY ── -->
<div class="body">
<div class="left">
<!-- Query area -->
<div class="query-area">
<label>Run Pipeline Experiment</label>
<textarea id="qinput" rows="3" placeholder="e.g. Compare Bronze and Gold plan copays for Metformin"></textarea>
<!-- Preset queries -->
<div class="presets">
<button class="presets-toggle" id="presets-toggle" onclick="togglePresets()">
📌 Load demo query <span id="preset-arrow"></span>
</button>
<div class="preset-list" id="preset-list">
<div class="preset-header">🔍 RAG Benchmark Queries</div>
<div class="preset-item" onclick="loadPreset(0)"><span class="preset-icon">🟢</span>Easy: What is a deductible?</div>
<div class="preset-item" onclick="loadPreset(1)"><span class="preset-icon">🟡</span>Medium: What is the copay for Metformin on the Silver plan?</div>
<div class="preset-item" onclick="loadPreset(2)"><span class="preset-icon">🟠</span>Hard: Compare the specialist copays and out-of-pocket maximums between the Silver and Gold plans.</div>
<div class="preset-item" onclick="loadPreset(3)"><span class="preset-icon">🔴</span>Very Hard: Compare the overall deductibles, specialist copays, and drug formulary tier copays between the Bronze, Silver, and Gold plans.</div>
<div class="preset-header">⚡ Redis Semantic Cache Testing (Run Base, then Variation)</div>
<div class="preset-item" onclick="loadPreset(4)"><span class="preset-icon">💊</span>Simple Base: Is Metformin covered on the Bronze plan?</div>
<div class="preset-item" onclick="loadPreset(5)"><span class="preset-icon">🌀</span>Simple Variation: Does the Bronze plan cover Metformin?</div>
<div class="preset-item" onclick="loadPreset(6)"><span class="preset-icon">📋</span>Complex Base: What is the policy regarding waiting periods for chronic, pre-existing conditions before the plan starts paying for specialist visits and maintenance medications?</div>
<div class="preset-item" onclick="loadPreset(7)"><span class="preset-icon">🌀</span>Complex Variation: I was diagnosed with asthma last year and just signed up for this insurance. Do I have to wait a certain number of months before you guys will cover my inhalers and pulmonologist appointments, or am I good to go right now?</div>
</div>
</div>
<button class="run-btn" id="runbtn" onclick="run()">🚀 Execute Live Trace</button>
</div>
<!-- Intent row -->
<div class="meta-row" id="meta-row">
<span class="intent-label">Intent</span>
<div class="intent-pill" id="intent-pill"></div>
<span class="intent-label" style="margin-left: 20px;">Confidence</span>
<div class="conf-badge" id="conf-badge"></div>
</div>
<!-- Memory inspector -->
<div class="memory-panel" id="memory-panel">
<div class="mem-header">
<span class="mem-label">🧠 Session Memory (Mem0)</span>
<div class="mem-actions">
<button class="mem-btn mem-refresh" onclick="loadMemory()">↻ Refresh</button>
<button class="mem-btn mem-wipe" onclick="wipeMemory()">🗑 Wipe</button>
</div>
</div>
<div class="mem-facts" id="mem-facts"><span class="mem-empty">No facts stored yet.</span></div>
</div>
<!-- Log area -->
<div class="log-area">
<div class="log-header">
<label>⚡ System Event Stream</label>
<div class="log-controls">
<input class="log-search" id="log-search" placeholder="filter…" oninput="filterLogs()">
<button class="filter-btn RETRIEVE" id="fb-RETRIEVE" onclick="setFilter('RETRIEVE')">RET</button>
<button class="filter-btn SYNTH" id="fb-SYNTH" onclick="setFilter('SYNTH')">SYN</button>
<button class="filter-btn ERROR" id="fb-ERROR" onclick="setFilter('ERROR')">ERR</button>
</div>
</div>
<div class="log-list-wrapper">
<div id="loglist"></div>
</div>
</div>
<!-- Answer card -->
<div class="answer-card" id="answer-card">
<div class="answer-header">
<div class="ans-label">✨ Pipeline Response</div>
<button class="copy-btn" onclick="copyAnswer()">📋 Copy</button>
</div>
<div class="ans-text" id="answer-text"></div>
</div>
</div><!-- /left -->
<div class="right">
<div class="diagram-label">
📊 Interactive Orchestrator Workflow
<div class="diagram-actions">
<button class="diag-btn" onclick="resetZoom()">⊞ Fit</button>
<button class="diag-btn" id="toggle-new" onclick="toggleNewNodes()">✦ New Nodes</button>
</div>
</div>
<div id="svg-wrap"><svg id="diagram"></svg></div>
</div>
</div><!-- /body -->
<div id="tooltip"></div>
<!-- ── NODE DETAIL MODAL ── -->
<div id="node-modal-backdrop" onclick="closeNodeModal(event)">
<div class="node-modal" id="node-modal">
<div class="nm-header">
<div class="nm-title" id="nm-title">🧠 Node Name</div>
<button class="nm-close" onclick="closeNodeModal()">&#x2715;</button>
</div>
<div id="nm-lat"></div>
<div class="nm-desc" id="nm-desc"></div>
<div id="nm-body"></div>
</div>
</div>
<div class="status">
<div class="dot" id="sdot"></div>
<span id="stxt">Ready — Enter parameters to begin orchestrating trace events.</span>
<span class="elapsed" id="elapsed"></span>
</div>
<script>
// ── CONFIG ──────────────────────────────────────────────────────────────────
const API = '';
let SID = localStorage.getItem('rag_sid') || crypto.randomUUID();
localStorage.setItem('rag_sid', SID);
document.getElementById('sid').textContent = 'SID: ' + SID.slice(0,8) + '…';
// ── AUTH ─────────────────────────────────────────────────────────────────────
if (sessionStorage.getItem('dev_auth') === 'true') {
document.getElementById('login-overlay').style.display = 'none';
}
function checkPass() {
const p = document.getElementById('pass-input').value;
if (p === '2002') {
sessionStorage.setItem('dev_auth', 'true');
const ov = document.getElementById('login-overlay');
ov.style.opacity = '0';
setTimeout(() => ov.style.display = 'none', 300);
startHealthPolling();
} else {
document.getElementById('login-err').style.display = 'block';
document.getElementById('pass-input').value = '';
}
}
document.getElementById('pass-input').addEventListener('keydown', e => { if (e.key === 'Enter') checkPass(); });
// ── PRESET QUERIES ───────────────────────────────────────────────────────────
const PRESETS = [
'What is a deductible?',
'What is the copay for Metformin on the Silver plan?',
'Compare the specialist copays and out-of-pocket maximums between the Silver and Gold plans.',
'Compare the overall deductibles, specialist copays, and drug formulary tier copays between the Bronze, Silver, and Gold plans.',
'Is Metformin covered on the Bronze plan?',
'Does the Bronze plan cover Metformin?',
'What is the policy regarding waiting periods for chronic, pre-existing conditions before the plan starts paying for specialist visits and maintenance medications?',
'I was diagnosed with asthma last year and just signed up for this insurance. Do I have to wait a certain number of months before you guys will cover my inhalers and pulmonologist appointments, or am I good to go right now?',
];
function loadPreset(i) {
document.getElementById('qinput').value = PRESETS[i];
document.getElementById('preset-list').classList.remove('open');
document.getElementById('preset-arrow').textContent = '▾';
document.getElementById('qinput').focus();
}
function togglePresets() {
const list = document.getElementById('preset-list');
const arrow = document.getElementById('preset-arrow');
const open = list.classList.toggle('open');
arrow.textContent = open ? '▴' : '▾';
}
// ── CMD-K PALETTE ────────────────────────────────────────────────────────────
function openPalette() { document.getElementById('palette-overlay').classList.add('open'); }
function closePalette() { document.getElementById('palette-overlay').classList.remove('open'); }
// ── HEALTH POLLING ────────────────────────────────────────────────────────────
function startHealthPolling() {
pollHealth();
setInterval(pollHealth, 12000);
}
async function pollHealth() {
try {
const res = await fetch(`${API}/health`);
const data = await res.json();
const comp = data.components || {};
setChip('hc-openai', comp.openai_api === 'ok');
setChip('hc-vector', comp.vector_store === 'ok');
setChip('hc-graph', comp.knowledge_graph=== 'ok');
const sc = document.getElementById('hc-sessions');
sc.querySelector('.dot').className = 'dot';
sc.className = 'hchip';
sc.innerHTML = `<span class="dot"></span>SESSIONS: ${data.active_sessions || 0}`;
} catch(_) { /* offline */ }
}
function setChip(id, ok) {
const el = document.getElementById(id);
el.className = 'hchip ' + (ok ? 'ok' : 'err');
}
if (sessionStorage.getItem('dev_auth') === 'true') startHealthPolling();
// ── MEMORY INSPECTOR ──────────────────────────────────────────────────────────
function toggleMemory() {
const panel = document.getElementById('memory-panel');
const isOpen = panel.classList.toggle('open');
if (isOpen) loadMemory();
}
async function loadMemory() {
const factsEl = document.getElementById('mem-facts');
factsEl.innerHTML = '<span class="mem-empty">Loading…</span>';
try {
const res = await fetch(`${API}/memory/${SID}`);
const data = await res.json();
if (!data.facts || data.facts.length === 0) {
factsEl.innerHTML = '<span class="mem-empty">No facts stored yet for this session.</span>';
return;
}
factsEl.innerHTML = data.facts.map(f =>
`<div class="mem-fact">${escHtml(f.memory)}</div>`
).join('');
} catch(e) {
factsEl.innerHTML = '<span class="mem-empty">Could not load memory (backend offline?).</span>';
}
}
async function wipeMemory() {
if (!confirm('Wipe all Mem0 facts for this session?')) return;
await fetch(`${API}/memory/${SID}`, { method: 'DELETE' });
loadMemory();
addLog('🗑️', 'SYSTEM', 'tag-system', 'Session memory wiped');
}
// ── LOG FILTER ────────────────────────────────────────────────────────────────
let activeFilter = 'ALL';
function setFilter(f) {
activeFilter = f;
document.querySelectorAll('.filter-btn').forEach(b => b.classList.remove('active'));
const btn = document.getElementById('fb-' + (f === 'SYNTH' ? 'SYNTH' : f));
if (btn) btn.classList.add('active');
filterLogs();
}
function filterLogs() {
const search = document.getElementById('log-search').value.toLowerCase();
document.querySelectorAll('.log-entry').forEach(el => {
const text = el.textContent.toLowerCase();
const tag = el.dataset.tag || 'SYSTEM';
const matchFilter = (activeFilter === 'ALL') ||
(activeFilter === 'CLASSIFY' && (tag.includes('CLASSIFY') || tag.includes('GUARD') || tag.includes('DECOMP'))) ||
(activeFilter === 'RETRIEVE' && tag.includes('RETRIEVE')) ||
(activeFilter === 'SYNTH' && (tag.includes('SYNTH') || tag.includes('CRITIQUE') || tag.includes('CONFIDENCE'))) ||
(activeFilter === 'ERROR' && tag.includes('ERROR'));
const matchSearch = !search || text.includes(search);
el.classList.toggle('hidden', !(matchFilter && matchSearch));
});
}
// ── COPY ANSWER ───────────────────────────────────────────────────────────────
function copyAnswer() {
const text = document.getElementById('answer-text').innerText;
navigator.clipboard.writeText(text).then(() => {
const btn = document.querySelector('.copy-btn');
btn.textContent = '✅ Copied!';
setTimeout(() => btn.textContent = '📋 Copy', 1800);
});
}
// ── DOWNLOAD TRACE ────────────────────────────────────────────────────────────
let traceStore = [];
let currentIntent = '';
let currentConfidence = '';
let finalResponseText = '';
function downloadTrace() {
if (!traceStore.length) { alert('Run a query first to generate a trace.'); return; }
const blob = new Blob([JSON.stringify(traceStore, null, 2)], { type: 'application/json' });
const a = document.createElement('a');
a.href = URL.createObjectURL(blob);
a.download = `rag_trace_${Date.now()}.json`;
a.click();
}
// ── HELPERS ───────────────────────────────────────────────────────────────────
function escHtml(s) { return s.replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;'); }
// ── D3 DIAGRAM ────────────────────────────────────────────────────────────────
const NW = 172, NH = 60;
const VW = 1500, VH = 760;
const NODES = [
// ── Main pipeline top row ──
{id:'user', icon:'👤', title:'User Query', sub:'pipeline entry', x:20, y:30},
{id:'fastapi', icon:'⚡', title:'FastAPI Host', sub:'/chat/stream', x:200, y:30},
{id:'orchestrator', icon:'🧠', title:'Orchestrator', sub:'LangGraph core', x:380, y:30},
{id:'query_guard', icon:'🛡️', title:'Query Guard', sub:'PII & safety check', x:560, y:30}, // NEW
{id:'mem_search', icon:'💭', title:'Memory Search', sub:'Mem0 retrieval', x:740, y:30},
{id:'intent_node', icon:'🔍', title:'Intent Classifier', sub:'GPT-4o-mini classify', x:920, y:30},
// ── Sub-pipeline ──
{id:'langsmith', icon:'📡', title:'LangSmith Tracing', sub:'Observability & traces', x:200, y:670},
{id:'query_decomposer',icon:'✂️', title:'Query Decomposer', sub:'MULTI_HOP splitter', x:920, y:130}, // NEW
{id:'retrieve_agent', icon:'📂', title:'Retrieval Agent', sub:'Task distributor', x:1100, y:225},
// ── Retrieval internals ──
{id:'multiquery', icon:'🔮', title:'Multi-Query', sub:'Query variation model', x:200, y:225},
{id:'graphdb', icon:'🕸️', title:'Knowledge Graph', sub:'NetworkX entity lookup', x:20, y:390},
{id:'vectordb', icon:'🗃️', title:'Vector Store', sub:'ChromaDB cosine sim', x:200, y:390},
{id:'bm25', icon:'🔤', title:'BM25 Search', sub:'Sparse lex search', x:380, y:390},
{id:'priorauth', icon:'📋', title:'Prior Auth Rules', sub:'Policy lookups', x:560, y:225},
{id:'bronze', icon:'🥉', title:'Bronze Tier', sub:'Benefit parameters', x:1100, y:315},
{id:'silver', icon:'🥈', title:'Silver Tier', sub:'Benefit parameters', x:1100, y:405},
{id:'gold', icon:'🥇', title:'Gold Tier', sub:'Benefit parameters', x:1100, y:495},
{id:'ensemble', icon:'🤝', title:'Ensemble Fusion', sub:'Rank weight fusion', x:380, y:475},
{id:'reranker', icon:'⚖️', title:'BGE Reranker', sub:'Cross-Encoder model', x:200, y:558},
{id:'context_check', icon:'🔬', title:'Context Quality', sub:'Relevance validator', x:380, y:558}, // NEW
{id:'merger', icon:'🔀', title:'Context Merger', sub:'Clean & deduplicate', x:560, y:558},
// ── Output section ──
{id:'synthesis', icon:'✍️', title:'Synthesis Agent', sub:'GPT-4o generator', x:560, y:670},
{id:'self_critique', icon:'🔄', title:'Self-Critique', sub:'Answer reflection', x:740, y:670}, // NEW
{id:'confidence', icon:'📊', title:'Confidence Scorer', sub:'HIGH/MED/LOW rating', x:920, y:670}, // NEW
{id:'mem_add', icon:'💾', title:'Memory Commit', sub:'Mem0 fact extractor', x:1100, y:670},
{id:'answer', icon:'💬', title:'Response Output', sub:'Client socket stream', x:1300, y:670},
{id:'query_analyzer', icon:'🔍', title:'Query Analyzer', sub:'phrasing normalizer', x:200, y:150},
{id:'redis', icon:'🔴', title:'Redis Cache', sub:'Semantic Vector Cache', x:380, y:150},
];
const INTENT_PILLS = [
{id:'i_simple', label:'SIMPLE_LOOKUP', x:1120, y:30, w:148, h:22},
{id:'i_policy', label:'POLICY_QUESTION', x:1120, y:58, w:148, h:22},
{id:'i_multihop', label:'MULTI_HOP', x:1120, y:86, w:148, h:22},
{id:'i_comparison', label:'COMPARISON', x:1120, y:114, w:148, h:22},
];
const EDGES = [
['user','fastapi'],['redis','orchestrator'],['orchestrator','query_guard'],
['query_guard','mem_search'],['mem_search','intent_node'],
['orchestrator','langsmith'],
['intent_node','query_decomposer'],['query_decomposer','retrieve_agent'],
['retrieve_agent','multiquery'],
['multiquery','graphdb'],['multiquery','vectordb'],['multiquery','bm25'],
['retrieve_agent','priorauth'],
['retrieve_agent','bronze'],['retrieve_agent','silver'],['retrieve_agent','gold'],
['vectordb','ensemble'],['bm25','ensemble'],
['graphdb','reranker'],['ensemble','reranker'],
['priorauth','merger'],['bronze','merger'],['silver','merger'],['gold','merger'],
['reranker','context_check'],['context_check','merger'],
['merger','synthesis'],['synthesis','langsmith'],
['synthesis','self_critique'],
['self_critique','confidence'],['confidence','mem_add'],['mem_add','answer'],
['fastapi','query_analyzer'],['query_analyzer','redis'],['redis','answer'],
];
// Self-critique retry edge (back to retrieve — special styling)
const RETRY_EDGES = [['self_critique','retrieve_agent']];
const STEP_MAP = [
// ── Cache nodes ──
{pat:/Semantic Cache/i, nodes:['redis']},
{pat:/Cache HIT/i, nodes:['redis']},
// ── New nodes ──
{pat:/Guard.*validated|Guard.*passed|Guard.*checks/i, nodes:['query_guard']},
{pat:/Guard BLOCKED.*PII|PII detected/i, nodes:['query_guard']},
{pat:/Guard BLOCKED/i, nodes:['query_guard']},
{pat:/Decomposer.*split|sub-question/i, nodes:['query_decomposer']},
{pat:/Decomposer.*skipped/i, nodes:['query_decomposer']},
{pat:/Context Quality.*section|Context Quality.*sufficient/i, nodes:['context_check','merger']},
{pat:/Context Quality.*insufficient/i, nodes:['context_check']},
{pat:/Self-Critique.*GOOD|Self-Critique.*verified/i, nodes:['self_critique']},
{pat:/Self-Critique.*retry|Retry.*retrieval/i, nodes:['self_critique','retrieve_agent']},
{pat:/Confidence: HIGH/i, nodes:['confidence']},
{pat:/Confidence: MEDIUM/i, nodes:['confidence']},
{pat:/Confidence: LOW/i, nodes:['confidence']},
// ── Existing patterns ──
{pat:/SIMPLE_LOOKUP/i, nodes:['graphdb','vectordb','bm25','ensemble','multiquery']},
{pat:/POLICY_QUESTION/i, nodes:['vectordb','bm25','ensemble','multiquery']},
{pat:/MULTI_HOP.*Graph.*Policy/i, nodes:['graphdb','vectordb','bm25','ensemble','multiquery','priorauth']},
{pat:/MULTI_HOP/i, nodes:['graphdb','vectordb','bm25','ensemble','multiquery','priorauth']},
{pat:/COMPARISON/i, nodes:['bronze','silver','gold','vectordb','bm25','ensemble','multiquery']},
{pat:/Bronze Tier/i, nodes:['bronze']},
{pat:/Silver Tier/i, nodes:['silver']},
{pat:/Gold Tier/i, nodes:['gold']},
{pat:/Graph entity lookup/i, nodes:['graphdb','vectordb','bm25','ensemble']},
{pat:/Hybrid document retrieval/i, nodes:['vectordb','bm25','ensemble']},
{pat:/Hybrid policy retrieval/i, nodes:['vectordb','bm25','ensemble']},
{pat:/Multi-Query Variant/i, nodes:['multiquery','vectordb','bm25','ensemble','graphdb']},
{pat:/Multi-Query/i, nodes:['multiquery']},
{pat:/Prior.?auth/i, nodes:['priorauth']},
{pat:/SBC_BRONZE|Bronze SBC|Bronze Tier benefits|Bronze deductible|Bronze copay/i, nodes:['bronze']},
{pat:/SBC_SILVER|Silver SBC|Silver Tier benefits|Silver deductible|Silver copay/i, nodes:['silver']},
{pat:/SBC_GOLD|Gold SBC|Gold Tier benefits|Gold deductible|Gold copay/i, nodes:['gold']},
{pat:/Intent classified/i, nodes:['intent_node']},
{pat:/Retrieved \d+/i, nodes:['reranker','merger']},
{pat:/Answer synthesized/i, nodes:['synthesis','langsmith']},
{pat:/LangSmith|langsmith|trace/i, nodes:['langsmith']},
{pat:/Mem0 stored|Mem0: no new/i, nodes:['mem_add']},
{pat:/Searching Mem0|Mem0 recalled|Mem0: no relevant/i, nodes:['mem_search']},
];
const LG_START = {
query_analyzer: ['query_analyzer'],
semantic_cache: ['redis'],
query_guard: ['query_guard'],
memory_search: ['mem_search'],
classify_intent: ['intent_node'],
query_decomposer: ['query_decomposer'],
retrieve: ['retrieve_agent'],
context_quality_check:['context_check'],
synthesize: ['synthesis'],
self_critique: ['self_critique'],
confidence_scorer: ['confidence'],
memory_add: ['mem_add'],
};
// ── Build SVG ─────────────────────────────────────────────────
const svg = d3.select('#diagram');
svg.attr('viewBox', `0 0 ${VW} ${VH}`);
const zoomG = svg.append('g');
// Zoom behavior
const zoomBehavior = d3.zoom().scaleExtent([0.3, 2.5]).on('zoom', e => {
zoomG.attr('transform', e.transform);
});
svg.call(zoomBehavior);
function resetZoom() {
svg.transition().duration(500).call(zoomBehavior.transform, d3.zoomIdentity.scale(0.72).translate(10, 10));
}
setTimeout(resetZoom, 100);
// Arrow marker
svg.append('defs').append('marker')
.attr('id','arr').attr('viewBox','0 -6 12 12')
.attr('refX',12).attr('refY',0).attr('markerWidth',7).attr('markerHeight',7)
.attr('orient','auto')
.append('path').attr('d','M0,-5L10,0L0,5').attr('fill','rgba(148,163,184,0.25)');
const cx = n => n.x + NW/2;
const cy = n => n.y + NH/2;
const nm = {}; NODES.forEach(n => nm[n.id] = n);
// Edges
const edgeG = zoomG.append('g');
const edgeSel = {};
EDGES.forEach(([f,t], i) => {
const fn=nm[f], tn=nm[t]; if(!fn||!tn) return;
const x1=cx(fn), y1=cy(fn), x2=cx(tn), y2=cy(tn);
const mx=(x1+x2)/2;
const p = edgeG.append('path')
.attr('id',`e${i}`).attr('class','edge')
.attr('d',`M${x1},${y1} C${mx},${y1} ${mx},${y2} ${x2},${y2}`)
.attr('marker-end','url(#arr)');
edgeSel[`${f}${t}`] = p;
});
// Retry edges (dashed amber)
RETRY_EDGES.forEach(([f,t]) => {
const fn=nm[f], tn=nm[t]; if(!fn||!tn) return;
const x1=cx(fn), y1=cy(fn), x2=cx(tn), y2=cy(tn);
const p = edgeG.append('path')
.attr('class','edge retry-edge')
.attr('d',`M${x1},${y1} C${x1},${y1+80} ${x2},${y2-80} ${x2},${y2}`)
.attr('marker-end','url(#arr)');
edgeSel[`${f}${t}_retry`] = p;
});
// Intent pills
const pillG = zoomG.append('g');
const pillSel = {};
INTENT_PILLS.forEach(p => {
const g = pillG.append('g').attr('id',`pill-${p.id}`);
g.append('rect').attr('x',p.x).attr('y',p.y).attr('width',p.w).attr('height',p.h)
.attr('class','intent-tag inactive-intent');
g.append('text').attr('x',p.x+p.w/2).attr('y',p.y+p.h/2+4)
.attr('text-anchor','middle').attr('fill','#475569')
.attr('font-family',"'Plus Jakarta Sans',sans-serif").attr('font-size',9).attr('font-weight',700)
.text(p.label);
pillSel[p.id] = g;
});
// Nodes
const nodeG = zoomG.append('g');
const nodeSel = {};
const nodeTimers = {}; // track start times per node
const nodeLatencies = {}; // completed latencies
const tt = document.getElementById('tooltip');
// Node descriptions for tooltip
const NODE_DESC = {
query_guard: 'NEW — Validates incoming queries. Detects PII (SSN, DOB), off-topic requests, and sanitizes input before the pipeline starts.',
mem_search: 'Searches Mem0 in-memory vector store for facts from previous turns in this session.',
intent_node: 'Uses GPT-4o-mini to classify the query into: SIMPLE_LOOKUP, POLICY_QUESTION, MULTI_HOP, or COMPARISON.',
query_decomposer:'NEW — For MULTI_HOP queries, decomposes the question into atomic sub-questions for targeted per-hop retrieval.',
retrieve_agent: 'Routes retrieval tasks based on intent. Dispatches to Graph, Hybrid, Prior-Auth, or Tier-specific tools in parallel.',
multiquery: 'Generates 3 query rephrases via GPT-4o-mini to improve document recall before vector search.',
graphdb: 'Queries the NetworkX knowledge graph for structured entity facts (providers, drugs, specialties).',
vectordb: 'ChromaDB cosine similarity search across all ingested policy documents using text-embedding-3-small.',
bm25: 'BM25 sparse lexical retrieval — excellent for exact term matches and plan-specific terminology.',
priorauth: 'Specialized search targeting prior authorization documents using PA keyword filtering.',
ensemble: 'Weighted fusion of BM25 (40%) and Vector (60%) results using Reciprocal Rank Fusion.',
reranker: 'BGE cross-encoder reranker scores (query, chunk) pairs and filters to top-N by relevance.',
context_check: 'NEW — Validates retrieved context before synthesis. Skips GPT-4o if context is empty to prevent hallucination.',
merger: 'Deduplicates and formats all retrieved context sections into a structured prompt payload.',
synthesis: 'GPT-4o generates a cited, safety-compliant answer from the merged context.',
self_critique: 'NEW — Self-RAG reflection: GPT-4o-mini evaluates answer quality. Triggers a single retry if answer is vague.',
confidence: 'NEW — Heuristic confidence scorer: counts citations, checks context richness, penalizes hedging language.',
langsmith: 'LangSmith observability: records full traces with latency, token usage, and intermediate states.',
mem_add: 'Mem0 fact extractor: persists key Q&A facts to session memory for future context personalization.',
answer: 'Final answer streamed back to the client via Server-Sent Events (SSE).',
query_analyzer: 'Translates conversational user queries into standardized third-person insurance policy search terms using GPT-4o-mini.',
redis: 'Cognitive Semantic Vector Cache: checks incoming queries against previously answered queries using GPT-4o-mini normalization and vector embeddings to bypass the orchestrator and LLM on cache hits.',
};
NODES.forEach(n => {
const g = nodeG.append('g').attr('id',`nd-${n.id}`).attr('class','n idle');
g.append('rect').attr('x',n.x).attr('y',n.y).attr('width',NW).attr('height',NH);
g.append('circle').attr('cx',n.x+NW/2).attr('cy',n.y+NH/2).attr('class','pulse');
g.append('text').attr('class','icon').attr('x',n.x+10).attr('y',n.y+NH/2+5).text(n.icon);
g.append('text').attr('class','title').attr('x',n.x+30).attr('y',n.y+NH/2-5).text(n.title);
g.append('text').attr('class','sub').attr('x',n.x+30).attr('y',n.y+NH/2+9).text(n.sub);
// Latency badge placeholder (will be filled on node_done)
g.append('text').attr('class','lat-badge').attr('id',`lat-${n.id}`)
.attr('x',n.x+NW-5).attr('y',n.y+12).attr('text-anchor','end').attr('fill','#10b981').text('');
nodeSel[n.id] = g;
// Quick tooltip on hover
g.style('cursor','pointer');
g.on('mouseover', (e) => {
const lat = nodeLatencies[n.id] ? `${nodeLatencies[n.id]}ms` : null;
tt.style.display = 'block';
tt.innerHTML = `<b>${n.icon} ${n.title}</b>
${lat ? `<div class="lat-info">⏱ Latency: ${lat}</div>` : ''}
<div>${NODE_DESC[n.id] || n.sub}</div>
<div style="font-size:0.63rem;color:#475569;margin-top:6px">🖱 Click for runtime details</div>`;
tt.style.left = (e.pageX + 14) + 'px';
tt.style.top = (e.pageY + 14) + 'px';
})
.on('mousemove', (e) => {
tt.style.left = (e.pageX + 14) + 'px';
tt.style.top = (e.pageY + 14) + 'px';
})
.on('mouseout', () => tt.style.display = 'none')
.on('click', () => { tt.style.display = 'none'; openNodeModal(n.id, n.icon, n.title); });
});
// ── STATE HELPERS ──────────────────────────────────────────────
let multiQueryVariants = [];
let nodeData = {}; // runtime data collected per-node during streaming
let newNodesVisible = true;
const NEW_NODE_IDS = ['query_guard','query_decomposer','context_check','self_critique','confidence'];
function toggleNewNodes() {
newNodesVisible = !newNodesVisible;
NEW_NODE_IDS.forEach(id => {
nodeSel[id]?.style('opacity', newNodesVisible ? 1 : 0.2);
});
document.getElementById('toggle-new').textContent = newNodesVisible ? '✦ New Nodes' : '◇ New Nodes';
}
function setNode(id, state) {
const g = nodeSel[id]; if(!g) return;
g.attr('class', `n ${state}`);
}
function activateEdge(f, t) {
const k = `${f}${t}`;
if (edgeSel[k]) edgeSel[k].attr('class','edge active-edge');
}
function doneEdge(f, t) {
const k = `${f}${t}`;
if (edgeSel[k]) edgeSel[k].attr('class','edge done-edge');
}
function setIntent(intentStr) {
const map = {SIMPLE_LOOKUP:'i_simple',POLICY_QUESTION:'i_policy',MULTI_HOP:'i_multihop',COMPARISON:'i_comparison'};
INTENT_PILLS.forEach(p => {
const g = pillSel[p.id]; if(!g) return;
const active = map[intentStr] === p.id;
g.select('rect').attr('class',`intent-tag ${active?'active-intent':'inactive-intent'}`);
g.select('text').attr('fill', active?'#a5b4fc':'#475569');
});
const colors = {SIMPLE_LOOKUP:'#818cf8',POLICY_QUESTION:'#10b981',MULTI_HOP:'#a78bfa',COMPARISON:'#f59e0b'};
const pill = document.getElementById('intent-pill');
pill.textContent = intentStr.replace(/_/g,' ');
pill.style.background = `${colors[intentStr]||'#6366f1'}20`;
pill.style.color = colors[intentStr] || '#a5b4fc';
pill.style.borderColor = `${colors[intentStr]||'#6366f1'}40`;
}
function setConfidence(conf, reason) {
const badge = document.getElementById('conf-badge');
if (!conf) return;
const confClean = conf.trim().toUpperCase();
badge.textContent = confClean;
badge.className = `conf-badge ${confClean}`;
badge.title = reason || '';
document.getElementById('meta-row').classList.add('visible');
}
function clearAll() {
NODES.forEach(n => { setNode(n.id,'idle'); document.getElementById(`lat-${n.id}`)?.setAttribute('text-content',''); d3.select(`#lat-${n.id}`).text(''); });
EDGES.forEach(([f,t]) => { const k=`${f}${t}`; if(edgeSel[k]) edgeSel[k].attr('class','edge'); });
RETRY_EDGES.forEach(([f,t]) => { const k=`${f}${t}_retry`; if(edgeSel[k]) edgeSel[k].attr('class','edge retry-edge'); });
INTENT_PILLS.forEach(p => {
pillSel[p.id].select('rect').attr('class','intent-tag inactive-intent');
pillSel[p.id].select('text').attr('fill','#475569');
});
document.getElementById('loglist').innerHTML = '';
document.getElementById('answer-card').style.display = 'none';
document.getElementById('answer-text').innerHTML = '';
document.getElementById('meta-row').classList.remove('visible');
const confBadge = document.getElementById('conf-badge');
if (confBadge) {
confBadge.className = 'conf-badge';
confBadge.textContent = '—';
}
const intentPill = document.getElementById('intent-pill');
if (intentPill) {
intentPill.textContent = '—';
}
Object.keys(nodeTimers).forEach(k => delete nodeTimers[k]);
Object.keys(nodeLatencies).forEach(k => delete nodeLatencies[k]);
multiQueryVariants = [];
nodeData = {};
traceStore = [];
setFilter('ALL');
}
function addLog(icon, tag, tagClass, text) {
const d = document.createElement('div');
d.className = 'log-entry';
d.dataset.tag = tag;
d.innerHTML = `<span class="log-icon">${icon}</span><div class="log-text"><span class="log-tag ${tagClass}">${tag}</span>${escHtml(String(text))}</div>`;
const ll = document.getElementById('loglist');
ll.appendChild(d);
ll.scrollTop = ll.scrollHeight;
filterLogs(); // apply current filter
}
function setStatus(txt, state='') {
document.getElementById('stxt').textContent = txt;
document.getElementById('sdot').className = `dot ${state}`;
}
function updateElapsed(ms) {
document.getElementById('elapsed').textContent = ms ? `${(ms/1000).toFixed(1)}s elapsed` : '';
}
function activateFromStep(step, lgNode) {
const earlyNodes = ['user', 'fastapi', 'query_analyzer', 'query_guard', 'semantic_cache', 'memory_search'];
for (const m of STEP_MAP) {
if (m.pat.test(step)) {
m.nodes.forEach(id => {
// Prevent early false matches from query strings during initial parsing/guard rails/cache checks
if (earlyNodes.includes(lgNode) && ['bronze','silver','gold','vectordb','bm25','ensemble','graphdb','multiquery','priorauth','reranker','merger','synthesis','self_critique','confidence','mem_add'].includes(id)) {
return;
}
if (id === 'graphdb' && currentIntent === 'POLICY_QUESTION') return;
if (['bronze', 'silver', 'gold'].includes(id) && currentIntent !== 'COMPARISON') return;
setNode(id, 'active');
const fromNode = lgNode === 'retrieve' ? 'retrieve_agent' : lgNode;
activateEdge(fromNode, id);
if(id==='ensemble') { activateEdge('vectordb','ensemble'); activateEdge('bm25','ensemble'); }
if(id==='multiquery') { activateEdge('retrieve_agent','multiquery'); }
if(id==='graphdb') { activateEdge('multiquery','graphdb'); }
if(id==='vectordb') { activateEdge('multiquery','vectordb'); }
if(id==='bm25') { activateEdge('multiquery','bm25'); }
if(id==='bronze') { activateEdge('retrieve_agent','bronze'); activateEdge('bronze','merger'); }
if(id==='silver') { activateEdge('retrieve_agent','silver'); activateEdge('silver','merger'); }
if(id==='gold') { activateEdge('retrieve_agent','gold'); activateEdge('gold','merger'); }
if(id==='priorauth') { activateEdge('retrieve_agent','priorauth'); activateEdge('priorauth','merger'); }
if(id==='reranker') { activateEdge('ensemble','reranker'); activateEdge('graphdb','reranker'); }
if(id==='context_check'){ activateEdge('reranker','context_check'); }
if(id==='merger') { activateEdge('context_check','merger'); activateEdge('reranker','context_check'); }
if(id==='confidence') { activateEdge('self_critique','confidence'); }
});
break;
}
}
}
// ── TYPEWRITER EFFECT ─────────────────────────────────────────
function typewriterMarkdown(rawText, el, onDone) {
el.innerHTML = '';
// Render markdown first, then animate
const rendered = marked.parse(rawText || '');
const temp = document.createElement('div');
temp.innerHTML = rendered;
// We'll fade-in paragraphs one by one
const children = Array.from(temp.childNodes);
let i = 0;
function next() {
if (i >= children.length) { onDone && onDone(); return; }
const node = children[i++];
el.appendChild(node.cloneNode(true));
setTimeout(next, 80);
}
next();
}
// ── MAIN RUN ─────────────────────────────────────────────────
async function run() {
const q = document.getElementById('qinput').value.trim();
if (!q) { alert('Enter a query first!'); return; }
clearAll();
traceStore = [];
const btn = document.getElementById('runbtn');
btn.disabled = true;
setStatus('Connecting to pipeline…', 'running');
const startMs = performance.now();
let elapsedTimer = setInterval(() => updateElapsed(performance.now()-startMs), 500);
let activeNodeIds = [];
currentIntent = '';
currentConfidence = '';
finalResponseText = '';
try {
const url = `${API}/chat/stream?session_id=${encodeURIComponent(SID)}&query=${encodeURIComponent(q)}`;
const resp = await fetch(url);
const reader = resp.body.getReader();
const dec = new TextDecoder();
let buf = '';
while (true) {
const { value, done } = await reader.read();
if (done) break;
buf += dec.decode(value, { stream: true });
const lines = buf.split('\n');
buf = lines.pop();
for (const line of lines) {
if (!line.startsWith('data: ')) continue;
const raw = line.slice(6).trim();
if (raw === '[DONE]') {
activeNodeIds.forEach(id => setNode(id, 'done'));
setNode('answer', 'done');
doneEdge('mem_add', 'answer');
clearInterval(elapsedTimer);
const totalMs = Math.round(performance.now() - startMs);
updateElapsed(totalMs);
setStatus(`✅ Pipeline completed in ${(totalMs/1000).toFixed(2)}s`, 'done');
const ansEl = document.getElementById('answer-text');
if (finalResponseText && (!ansEl.innerHTML || ansEl.innerHTML.trim() === '')) {
document.getElementById('answer-card').style.display = 'block';
typewriterMarkdown(finalResponseText, ansEl);
}
break;
}
try {
const ev = JSON.parse(raw);
traceStore.push(ev);
handleEvent(ev);
if (ev.intent) currentIntent = ev.intent;
if (ev.confidence) currentConfidence = ev.confidence;
} catch(_) {}
}
}
} catch(err) {
clearInterval(elapsedTimer);
setStatus('❌ Connection error: ' + err.message, 'error');
addLog('❌', 'ERROR', 'tag-error', err.message);
} finally {
btn.disabled = false;
}
function handleEvent(ev) {
const { type, node, intent, step, msg, answer, confidence, confidence_reason, blocked, sub_questions } = ev;
if (answer) {
finalResponseText = answer;
}
if (intent && intent !== currentIntent) { currentIntent = intent; setIntent(intent); document.getElementById('meta-row').classList.add('visible'); }
if (confidence) setConfidence(confidence, confidence_reason);
if (type === 'node_start') {
// Record latency start
nodeTimers[node] = performance.now();
const diagramNodes = LG_START[node] || [];
activeNodeIds.forEach(id => setNode(id, 'done'));
if (node === 'user') { setNode('user','active'); activeNodeIds=['user']; activateEdge('user','fastapi'); }
else if (node === 'fastapi') { setNode('fastapi','active'); activeNodeIds=['fastapi']; activateEdge('fastapi','query_analyzer'); }
else if (node === 'query_analyzer'){ setNode('query_analyzer','active'); activeNodeIds=['query_analyzer']; activateEdge('fastapi','query_analyzer'); }
else if (node === 'semantic_cache') {
setNode('redis','active');
activeNodeIds=['redis'];
activateEdge('query_analyzer','redis');
}
else if (node === 'orchestrator') { setNode('orchestrator','active'); activeNodeIds=['orchestrator']; activateEdge('redis','orchestrator'); }
else {
diagramNodes.forEach(id => { setNode(id,'active'); });
activeNodeIds = diagramNodes;
}
const tagMap = {
query_guard:'tag-guard', classify_intent:'tag-classify',
query_decomposer:'tag-classify', retrieve:'tag-retrieve',
context_quality_check:'tag-critique', synthesize:'tag-synthesize',
self_critique:'tag-critique', confidence_scorer:'tag-critique',
memory_add:'tag-system', memory_search:'tag-system',
};
const tagClass = tagMap[node] || 'tag-system';
addLog('▶', 'START', tagClass, node + (msg ? ' — ' + msg : ''));
setStatus(`Executing node: [${node}]…`, 'running');
}
if (type === 'substep') {
activateFromStep(step||'', node||'');
const isGraphEdge = step && step.startsWith('[GraphDB-Edge]');
const tagMap = {
retrieve:'tag-retrieve', synthesize:'tag-synthesize',
query_guard:'tag-guard', query_decomposer:'tag-classify',
context_quality_check:'tag-critique', self_critique:'tag-critique',
confidence_scorer:'tag-critique',
};
const tagClass = tagMap[node] || 'tag-classify';
if (!isGraphEdge) {
addLog('→', 'STEP', tagClass, step||'');
}
if (step && step.includes('🧠 Multi-Query Variant')) {
const variantText = step.split(':').slice(1).join(':').trim();
multiQueryVariants.push(variantText);
nodeData.multiquery = nodeData.multiquery || { variants: [] };
if (!nodeData.multiquery.variants.includes(variantText)) nodeData.multiquery.variants.push(variantText);
}
// Collect per-node runtime data from log messages
collectNodeData(step || '', node || '', ev);
// Show sub-questions in log
if (sub_questions && sub_questions.length) {
sub_questions.forEach((sq, i) => addLog('✂️','DECOMP','tag-classify',`Sub-Q ${i+1}: ${sq}`));
nodeData.query_decomposer = nodeData.query_decomposer || { sub_questions: [] };
nodeData.query_decomposer.sub_questions = sub_questions;
}
}
if (type === 'node_done') {
// Record latency
if (nodeTimers[node]) {
const ms = Math.round(performance.now() - nodeTimers[node]);
nodeLatencies[node] = ms;
// Draw latency badge on the corresponding D3 node(s)
const diagramNodes = LG_START[node] || [];
diagramNodes.forEach(id => {
d3.select(`#lat-${id}`).text(`${ms}ms`);
});
}
if (node === 'query_guard' && blocked) {
// Blocked — mark guard as error-like
setNode('query_guard', 'done');
d3.select('#nd-query_guard rect').style('fill','rgba(239,68,68,0.15)').style('stroke','#ef4444');
}
if (node === 'retrieve') {
const allRetrieval = ['graphdb','vectordb','bm25','ensemble','multiquery','priorauth','bronze','silver','gold'];
allRetrieval.forEach(id => {
const el = document.getElementById(`nd-${id}`);
if (el && el.getAttribute('class') === 'n idle') setNode(id, 'skip');
else if (el) setNode(id, 'done');
});
activateEdge('ensemble','reranker'); activateEdge('graphdb','reranker');
setNode('reranker','active');
}
if (node === 'context_quality_check') {
setNode('context_check','done');
setNode('reranker','done');
doneEdge('reranker','context_check');
}
if (node === 'synthesize') {
setNode('reranker','done'); setNode('merger','done');
doneEdge('merger','synthesis');
setNode('langsmith','active'); activateEdge('synthesis','langsmith');
setTimeout(() => setNode('langsmith','done'), 600);
}
if (node === 'self_critique') {
setNode('self_critique', 'done');
// If retry, show the retry edge as active
if (ev.needs_retry) {
const rk = `self_critique→retrieve_agent_retry`;
if (edgeSel[rk]) edgeSel[rk].attr('class','edge retry-edge active-edge');
setTimeout(() => { if (edgeSel[rk]) edgeSel[rk].attr('class','edge retry-edge'); }, 3000);
}
}
if (node === 'confidence_scorer') {
setNode('confidence','done');
if (confidence) setConfidence(confidence, confidence_reason);
activateEdge('self_critique','confidence');
doneEdge('self_critique','confidence');
}
if (node === 'query_analyzer') {
doneEdge('fastapi', 'query_analyzer');
setNode('query_analyzer', 'done');
nodeData.query_analyzer = nodeData.query_analyzer || {};
nodeData.query_analyzer.normalized_query = ev.normalized_query || '';
}
if (node === 'semantic_cache') {
doneEdge('query_analyzer', 'redis');
if (answer) {
setNode('redis', 'hit');
doneEdge('redis', 'answer');
setNode('answer', 'active');
const ansEl = document.getElementById('answer-text');
document.getElementById('answer-card').style.display = 'block';
typewriterMarkdown(answer, ansEl);
} else {
setNode('redis', 'miss');
doneEdge('redis', 'orchestrator');
}
}
if (node === 'memory_add') {
doneEdge('synthesis','self_critique'); doneEdge('self_critique','confidence');
doneEdge('confidence','mem_add'); doneEdge('mem_add','answer');
setNode('mem_add','done'); setNode('answer','active');
if (answer) {
const ansEl = document.getElementById('answer-text');
document.getElementById('answer-card').style.display = 'block';
typewriterMarkdown(answer, ansEl);
}
}
const tagMap = { synthesize:'tag-synthesize', retrieve:'tag-retrieve', self_critique:'tag-critique', confidence_scorer:'tag-critique' };
const tagClass = tagMap[node] || 'tag-classify';
addLog('✅', 'DONE', tagClass, `${node} execution complete`);
}
if (type === 'error') {
addLog('❌', 'ERROR', 'tag-error', msg);
setStatus('❌ Node error: ' + msg, 'error');
}
}
}
// ── COLLECT NODE DATA (from substep log messages) ─────────────────────────
function collectNodeData(step, node, ev) {
// Guard checks
if (/Guard.*validated|Guard.*passed/i.test(step)) {
nodeData.query_guard = nodeData.query_guard || { checks: [] };
nodeData.query_guard.checks.push({ type: 'ok', msg: step.replace(/^[\S]+\s/, '') });
}
if (/Guard BLOCKED/i.test(step)) {
nodeData.query_guard = nodeData.query_guard || { checks: [] };
nodeData.query_guard.checks.push({ type: 'blocked', msg: step });
nodeData.query_guard.blocked = true;
}
if (/Guard.*PII detected/i.test(step)) {
nodeData.query_guard = nodeData.query_guard || { checks: [] };
nodeData.query_guard.checks.push({ type: 'warn', msg: step });
nodeData.query_guard.pii = true;
}
// Intent
const intentMatch = step.match(/Intent classified → (\w+)/);
if (intentMatch) {
nodeData.intent_node = { intent: intentMatch[1] };
}
// Memory search
if (/Mem0 recalled/i.test(step)) {
const m = step.match(/(\d+) fact/);
nodeData.mem_search = { message: step, count: m ? parseInt(m[1]) : 0 };
} else if (/Mem0: no relevant/i.test(step)) {
nodeData.mem_search = { message: step, count: 0 };
}
// Context quality
if (/Context Quality/i.test(step)) {
const secM = step.match(/(\d+) section/);
const charM = step.match(/([\d,]+) chars/);
const skipped = /insufficient/i.test(step);
nodeData.context_check = { message: step, sections: secM ? parseInt(secM[1]) : 0,
chars: charM ? charM[1] : '0', skipped };
}
// Retrieval summary
if (/Retrieved (\d+) context section/i.test(step)) {
const m = step.match(/Retrieved (\d+)/);
nodeData.retrieve_agent = nodeData.retrieve_agent || {};
nodeData.retrieve_agent.sections = m ? parseInt(m[1]) : 0;
}
// Self-critique
if (/Self-Critique/i.test(step)) {
nodeData.self_critique = nodeData.self_critique || { verdicts: [] };
nodeData.self_critique.verdicts.push(step);
if (/GOOD|verified/i.test(step)) nodeData.self_critique.final = 'GOOD';
if (/retry|INSUFFICIENT/i.test(step)) nodeData.self_critique.final = 'RETRY';
}
// Confidence
const confMatch = step.match(/Confidence: (\w+) \(score=(\d+)\s*[\u2013\u2014-]\s*(.+)\)/);
if (confMatch) {
nodeData.confidence = { level: confMatch[1], score: confMatch[2], reasons: confMatch[3].split(' | ') };
}
// Memory add
if (/Mem0 stored (\d+)/i.test(step)) {
const m = step.match(/stored (\d+)/);
nodeData.mem_add = { count: m ? parseInt(m[1]) : 0, message: step };
} else if (/Mem0: no new/i.test(step)) {
nodeData.mem_add = { count: 0, message: step };
}
// LangSmith run ID from event
if (ev && ev.run_id && !nodeData.langsmith) {
nodeData.langsmith = { run_id: ev.run_id };
}
// Retrieved chunks for Vector, BM25, Ensemble, and Reranker
const chunkMatch = step.match(/\[(VectorDB|BM25|Ensemble|Reranker)\] Retrieved: (.+?)(?:\|([-\.\d]+)\|(.+))?$/i);
if (chunkMatch) {
const nodeKey = chunkMatch[1].toLowerCase();
nodeData[nodeKey] = nodeData[nodeKey] || { retrieved_chunks: [] };
if (chunkMatch[3] !== undefined && chunkMatch[4] !== undefined) {
nodeData[nodeKey].retrieved_chunks.push({
source: chunkMatch[2].trim(),
score: parseFloat(chunkMatch[3]),
content: chunkMatch[4].trim()
});
} else {
nodeData[nodeKey].retrieved_chunks.push({
source: 'Unknown',
score: 0,
content: chunkMatch[2].trim()
});
}
}
// GraphDB retrieved entities
const graphMatch = step.match(/\[GraphDB\] Retrieved entities: (.+)/i);
if (graphMatch) {
nodeData['graphdb'] = nodeData['graphdb'] || { entities: [] };
nodeData['graphdb'].entities = graphMatch[1].split(',').map(s => s.trim());
}
if (/\[GraphDB\] No entities matched./i.test(step)) {
nodeData['graphdb'] = nodeData['graphdb'] || { entities: [] };
}
const graphEdgeMatch = step.match(/\[GraphDB-Edge\] (.+)\|\|\|(.+)\|\|\|(.+)/i);
if (graphEdgeMatch) {
nodeData['graphdb'] = nodeData['graphdb'] || { entities: [], edges: [] };
nodeData['graphdb'].edges = nodeData['graphdb'].edges || [];
nodeData['graphdb'].edges.push({
source: graphEdgeMatch[1].trim(),
relation: graphEdgeMatch[2].trim(),
target: graphEdgeMatch[3].trim()
});
}
// Redis Cache match info
if (/Matched cached query: '(.+)'/i.test(step) || /Matched query: '(.+)'/i.test(step)) {
const m = step.match(/Matched (?:cached )?query: '(.+?)'/i);
nodeData.redis = nodeData.redis || {};
nodeData.redis.matched_query = m ? m[1] : '';
nodeData.redis.hit = true;
}
if (/Similarity Score: ([\d\.]+)%/i.test(step)) {
const m = step.match(/Similarity Score: ([\d\.]+)%/i);
nodeData.redis = nodeData.redis || {};
nodeData.redis.similarity = m ? parseFloat(m[1]) : 0;
nodeData.redis.hit = true;
}
if (/Cache MISS/i.test(step)) {
nodeData.redis = nodeData.redis || {};
nodeData.redis.hit = false;
}
}
// ── NODE DETAIL MODAL ──────────────────────────────────────────────
const NEW_NODES_SET = new Set(['query_guard','query_decomposer','context_check','self_critique','confidence']);
function openNodeModal(nodeId, icon, title) {
const data = nodeData[nodeId] || {};
const lat = nodeLatencies[nodeId];
const desc = NODE_DESC[nodeId] || '';
const isNew = NEW_NODES_SET.has(nodeId);
document.getElementById('nm-title').innerHTML =
`${icon} ${title} ${isNew ? '<span class="nm-badge new">NEW</span>' : '<span class="nm-badge">NODE</span>'}`;
document.getElementById('nm-desc').textContent = desc;
document.getElementById('nm-lat').innerHTML = lat
? `<div class="nm-lat">⏱ Latency: ${lat}ms</div>`
: '';
let body = '';
const noData = '<p class="nm-empty">No runtime data yet — run a query first.</p>';
if (nodeId === 'multiquery') {
const variants = data.variants || [];
body += `<div class="nm-section"><div class="nm-section-title">🔄 Generated Query Variants</div>`;
body += variants.length
? variants.map((v, i) => `<div class="nm-card variant"><strong>Variant ${i+1}:</strong> ${escHtml(v)}</div>`).join('')
: noData;
body += '</div>';
}
else if (nodeId === 'query_decomposer') {
const sqs = data.sub_questions || [];
const orig = data.original_query || document.getElementById('qinput').value || '';
const currentIntent = document.getElementById('intent-pill').textContent.trim();
body += `<div class="nm-section"><div class="nm-section-title">✂️ Decomposing Complex Intent</div>`;
if (currentIntent === 'MULTI_HOP' && sqs.length) {
body += `
<div class="nm-card info" style="margin-bottom:12px; border-left-color:#a78bfa">
<strong>Decomposer Logic:</strong> Since the query was classified as <strong>${currentIntent}</strong>, it is split into independent sub-queries to retrieve targeted facts from different documents (e.g. drug formularies, provider databases) which are later synthesized.
</div>
<div class="nm-card subq" style="background:rgba(255,255,255,0.03); border-left-color:rgba(167,139,250,0.5); margin-bottom:16px;">
<strong>Original Input Query:</strong> <em>"${escHtml(orig)}"</em>
</div>
<div class="nm-section-title" style="font-size:0.85rem; margin-top:8px;">Generated Atomic Sub-Questions:</div>
`;
body += sqs.map((q, i) => `<div class="nm-card subq" style="border-left-color:#a78bfa; margin-bottom:6px;"><strong>Sub-Q ${i+1}:</strong> ${escHtml(q)}</div>`).join('');
body += `<div class="nm-stat-row" style="margin-top:12px;"><div class="nm-stat">Sub-questions generated: <span>${sqs.length}</span></div></div>`;
} else {
body += `
<div class="nm-card warning" style="border-left-color:#fbbf24">
<strong>Decomposer Skipped:</strong> The query intent was classified as <strong>${currentIntent || 'non-MULTI_HOP'}</strong>. Only queries with <strong>MULTI_HOP</strong> intent are decomposed. Other intents retrieve results in a single hop.
</div>
`;
}
body += '</div>';
}
else if (nodeId === 'query_guard') {
const checks = data.checks || [];
body += `<div class="nm-section"><div class="nm-section-title">🛡️ Security Check Results</div>`;
if (checks.length) {
body += checks.map(c => {
const cls = c.type === 'ok' ? 'check' : c.type === 'warn' ? 'warning' : 'bad';
const icon = c.type === 'ok' ? '✅' : c.type === 'warn' ? '⚠️' : '🚫';
return `<div class="nm-card ${cls}">${icon} ${escHtml(c.msg)}</div>`;
}).join('');
} else { body += noData; }
body += '</div>';
if (data.pii) body += `<div class="nm-stat-row"><div class="nm-stat" style="border-color:rgba(245,158,11,0.3);color:#fbbf24">⚠️ PII detected and redacted from query</div></div>`;
if (data.blocked) body += `<div class="nm-stat-row"><div class="nm-stat" style="border-color:rgba(239,68,68,0.3);color:#f87171">🚫 Query was blocked — pipeline short-circuited</div></div>`;
}
else if (nodeId === 'intent_node') {
const INTENT_COLORS = {SIMPLE_LOOKUP:'#818cf8',POLICY_QUESTION:'#10b981',MULTI_HOP:'#a78bfa',COMPARISON:'#f59e0b'};
const intent = data.intent || '';
body += `<div class="nm-section"><div class="nm-section-title">🔍 Classification Result</div>`;
if (intent) {
const c = INTENT_COLORS[intent] || '#818cf8';
body += `<div class="nm-card info" style="border-left-color:${c};font-size:0.85rem;font-weight:700;color:${c}">${intent}</div>`;
const INTENT_DESCRIPTIONS = {
SIMPLE_LOOKUP: 'Quick single-fact lookup (drug copay, provider lookup, coverage check)',
POLICY_QUESTION: 'General policy or procedure question (claims, referrals, preventive care)',
MULTI_HOP: 'Multi-step question requiring several document lookups. Query was decomposed into sub-questions.',
COMPARISON: 'Plan comparison query — retrieves Bronze, Silver, and Gold contexts in parallel.',
};
body += `<div class="nm-card info" style="margin-top:6px;font-family:inherit;font-size:0.73rem">${INTENT_DESCRIPTIONS[intent]||''}</div>`;
} else { body += noData; }
body += '</div>';
}
else if (nodeId === 'mem_search') {
body += `<div class="nm-section"><div class="nm-section-title">🧠 Mem0 Recall Result</div>`;
if (data.message) {
const cls = data.count > 0 ? 'memory' : 'info';
body += `<div class="nm-card ${cls}">${escHtml(data.message)}</div>`;
body += `<div class="nm-stat-row"><div class="nm-stat">Facts recalled: <span>${data.count}</span></div></div>`;
} else { body += noData; }
body += '</div>';
}
else if (nodeId === 'context_check') {
body += `<div class="nm-section"><div class="nm-section-title">🔬 Context Validation Result</div>`;
if (data.message) {
const cls = data.skipped ? 'bad' : 'good';
body += `<div class="nm-card ${cls}">${escHtml(data.message)}</div>`;
if (!data.skipped) {
body += `<div class="nm-stat-row">
<div class="nm-stat">Sections: <span>${data.sections}</span></div>
<div class="nm-stat">Total chars: <span>${data.chars}</span></div>
<div class="nm-stat">Synthesis: <span style="color:#34d399">✓ Proceeding</span></div>
</div>`;
} else {
body += `<div class="nm-stat-row"><div class="nm-stat" style="color:#f87171">Synthesis was skipped — fallback answer returned</div></div>`;
}
} else { body += noData; }
body += '</div>';
}
else if (nodeId === 'retrieve_agent') {
body += `<div class="nm-section"><div class="nm-section-title">📂 Retrieval Summary</div>`;
if (data.sections !== undefined) {
body += `<div class="nm-stat-row"><div class="nm-stat">Context sections returned: <span>${data.sections}</span></div></div>`;
} else { body += noData; }
body += '</div>';
body += `<div class="nm-section"><div class="nm-section-title">🔧 Tools Invoked (by intent)</div>
<div class="nm-card info" style="font-family:inherit">
<strong>SIMPLE_LOOKUP</strong> → Knowledge Graph + Hybrid Search<br>
<strong>POLICY_QUESTION</strong> → Full Hybrid Pipeline<br>
<strong>MULTI_HOP</strong> → Graph + Hybrid + Prior Auth + Sub-question retrieval<br>
<strong>COMPARISON</strong> → Bronze + Silver + Gold tier-scoped searches
</div></div>`;
}
else if (nodeId === 'self_critique') {
body += `<div class="nm-section"><div class="nm-section-title">🔄 Self-RAG Reflection</div>`;
const verdicts = data.verdicts || [];
if (verdicts.length) {
body += verdicts.map(v => {
const cls = /GOOD|verified/i.test(v) ? 'good' : 'bad';
const ico = /GOOD|verified/i.test(v) ? '✅' : '🔄';
return `<div class="nm-card ${cls}">${ico} ${escHtml(v)}</div>`;
}).join('');
} else { body += noData; }
if (data.final) {
const cls = data.final === 'GOOD' ? 'good' : 'bad';
body += `<div class="nm-stat-row"><div class="nm-stat nm-card ${cls}" style="padding:5px 12px">Final verdict: <span style="color:inherit">${data.final}</span></div></div>`;
}
body += '</div>';
}
else if (nodeId === 'confidence') {
body += `<div class="nm-section"><div class="nm-section-title">📊 Confidence Score Breakdown</div>`;
if (data.level) {
const colors = { HIGH:'#34d399', MEDIUM:'#fbbf24', LOW:'#f87171', BLOCKED:'#94a3b8' };
const c = colors[data.level] || '#818cf8';
body += `<div class="nm-stat-row">
<div class="nm-stat" style="border-color:${c}40;color:${c};font-size:0.9rem;font-weight:800">
${data.level} confidence
</div>
<div class="nm-stat">Score: <span>${data.score}</span></div>
</div>`;
if (data.reasons && data.reasons.length) {
body += `<div class="nm-section-title" style="margin-top:10px">📌 Scoring Signals</div>`;
body += data.reasons.map(r =>
`<div class="nm-card info">${escHtml(r)}</div>`
).join('');
}
} else { body += noData; }
body += '</div>';
}
else if (nodeId === 'mem_add') {
body += `<div class="nm-section"><div class="nm-section-title">💾 Mem0 Storage Result</div>`;
if (data.message) {
body += `<div class="nm-card memory">${escHtml(data.message)}</div>`;
body += `<div class="nm-stat-row"><div class="nm-stat">Facts stored this turn: <span>${data.count}</span></div></div>`;
} else { body += noData; }
body += '</div>';
}
else if (nodeId === 'langsmith') {
body += `<div class="nm-section"><div class="nm-section-title">📡 LangSmith Trace</div>`;
if (data.run_id) {
body += `<div class="nm-card info">Run ID: <code>${escHtml(data.run_id)}</code></div>`;
body += `<div class="nm-stat-row"><a href="https://smith.langchain.com" target="_blank" style="color:#22d3ee;font-size:0.74rem">🔗 Open LangSmith Dashboard ↗</a></div>`;
} else {
body += `<div class="nm-card info">Tracing enabled — all nodes are being recorded with latency and token usage.</div>`;
}
body += '</div>';
}
else if (nodeId === 'redis') {
body += `<div class="nm-section"><div class="nm-section-title">🔴 Redis Semantic Cache Details</div>`;
if (data.hit) {
body += `<div class="nm-card good">⚡ Semantic Cache HIT!</div>`;
body += `<div class="nm-card info"><strong>Matched Query in Cache:</strong><br>${escHtml(data.matched_query)}</div>`;
body += `<div class="nm-stat-row">
<div class="nm-stat">Similarity Score: <span style="color:#10b981">${data.similarity}%</span></div>
<div class="nm-stat">Bypass Status: <span style="color:#10b981">Active (LLM Bypassed)</span></div>
</div>`;
} else {
body += `<div class="nm-card info">The semantic cache was checked but resulted in a cache MISS. The query was forwarded to the orchestrator pipeline for standard processing.</div>`;
}
body += `</div>`;
}
else if (nodeId === 'bm25') {
body += `<div class="nm-section"><div class="nm-section-title">🔤 BM25 Scoring Formula</div>`;
body += `<div class="nm-card info" style="font-family:monospace; font-size: 0.8rem; overflow-x: auto; white-space: nowrap;">
score(D,Q) = ∑ IDF(q_i) × [ f(q_i,D) × (k_1 + 1) ] / [ f(q_i,D) + k_1 × (1 - b + b × |D|/avgdl) ]
</div>`;
if (data.retrieved_chunks && data.retrieved_chunks.length) {
body += `<div class="nm-section-title" style="margin-top:10px; display:flex; justify-content:space-between; align-items:center;">
<span>📊 BM25 Scores</span>
<button onclick="toggleScoreFullscreen()" style="background:none;border:none;color:#38bdf8;cursor:pointer;font-size:0.8rem;padding:0;">⛶ Fullscreen</button>
</div>`;
body += `<svg id="nm-score-viz" width="100%" height="250" style="background:#1e293b; border-radius:8px; margin-top:5px; border:1px solid #334155; transition: height 0.3s ease;"></svg>`;
body += `<div class="nm-section-title" style="margin-top:10px">📄 Top Retrieved Chunks</div>`;
body += data.retrieved_chunks.map(c => {
const text = typeof c === 'string' ? c : `[Score: ${c.score.toFixed(4)}] ${c.content}`;
return `<div class="nm-card info" style="font-size: 0.8rem;">${escHtml(text)}</div>`;
}).join('');
} else {
body += noData;
}
body += `</div>`;
}
else if (nodeId === 'vectordb') {
body += `<div class="nm-section"><div class="nm-section-title">🗃️ Vector Store Retrieval</div>`;
if (data.retrieved_chunks && data.retrieved_chunks.length) {
body += `<div class="nm-section-title" style="margin-top:10px; display:flex; justify-content:space-between; align-items:center;">
<span>📊 Cosine Similarity Scores</span>
<button onclick="toggleScoreFullscreen()" style="background:none;border:none;color:#38bdf8;cursor:pointer;font-size:0.8rem;padding:0;">⛶ Fullscreen</button>
</div>`;
body += `<svg id="nm-score-viz" width="100%" height="250" style="background:#1e293b; border-radius:8px; margin-top:5px; border:1px solid #334155; transition: height 0.3s ease;"></svg>`;
body += `<div class="nm-section-title" style="margin-top:10px">📄 Top Retrieved Chunks</div>`;
body += data.retrieved_chunks.map(c => {
const text = typeof c === 'string' ? c : `[Score: ${c.score.toFixed(4)}] ${c.content}`;
return `<div class="nm-card info" style="font-size: 0.8rem;">${escHtml(text)}</div>`;
}).join('');
} else {
body += noData;
}
body += `</div>`;
}
else if (nodeId === 'ensemble') {
body += `<div class="nm-section"><div class="nm-section-title">⚖️ Reciprocal Rank Fusion (RRF) Formula</div>`;
body += `<div class="nm-card info" style="font-family:monospace; font-size: 0.8rem; overflow-x: auto; white-space: nowrap;">
RRF_Score(d ∈ D) = ∑_{m ∈ M} w_m / (r_m(d) + k)
</div>`;
body += `<div class="nm-card info" style="font-size:0.75rem; line-height:1.4;">
Melds Vector search similarity results with BM25 keyword rankings. w_m represents retriever weights, and k is the constant rank smoothing parameter (default c=60).
</div>`;
if (data.retrieved_chunks && data.retrieved_chunks.length) {
body += `<div class="nm-section-title" style="margin-top:10px; display:flex; justify-content:space-between; align-items:center;">
<span>📊 RRF Merged Scores</span>
<button onclick="toggleScoreFullscreen()" style="background:none;border:none;color:#38bdf8;cursor:pointer;font-size:0.8rem;padding:0;">⛶ Fullscreen</button>
</div>`;
body += `<svg id="nm-score-viz" width="100%" height="250" style="background:#1e293b; border-radius:8px; margin-top:5px; border:1px solid #334155; transition: height 0.3s ease;"></svg>`;
body += `<div class="nm-section-title" style="margin-top:10px">📄 Merged Documents (RRF Order)</div>`;
body += data.retrieved_chunks.map(c => {
const text = typeof c === 'string' ? c : `[Score/RRF: ${c.score.toFixed(4)}] ${c.content}`;
return `<div class="nm-card info" style="font-size: 0.8rem;">${escHtml(text)}</div>`;
}).join('');
} else {
body += noData;
}
body += `</div>`;
}
else if (nodeId === 'reranker') {
body += `<div class="nm-section"><div class="nm-section-title">⚖️ Cross-Encoder Relevance Scoring</div>`;
body += `<div class="nm-card info" style="font-size:0.75rem; line-height:1.4;">
Uses the BGE Cross-Encoder model to score the raw relevance of each retrieved chunk against the query. Chunks scoring below the minimum relevance threshold are filtered out.
</div>`;
if (data.retrieved_chunks && data.retrieved_chunks.length) {
body += `<div class="nm-section-title" style="margin-top:10px; display:flex; justify-content:space-between; align-items:center;">
<span>📊 Reranker Relevance Scores</span>
<button onclick="toggleScoreFullscreen()" style="background:none;border:none;color:#38bdf8;cursor:pointer;font-size:0.8rem;padding:0;">⛶ Fullscreen</button>
</div>`;
body += `<svg id="nm-score-viz" width="100%" height="250" style="background:#1e293b; border-radius:8px; margin-top:5px; border:1px solid #334155; transition: height 0.3s ease;"></svg>`;
body += `<div class="nm-section-title" style="margin-top:10px">📄 Top Reranked Chunks</div>`;
body += data.retrieved_chunks.map(c => {
const text = typeof c === 'string' ? c : `[Score: ${c.score.toFixed(4)}] ${c.content}`;
return `<div class="nm-card info" style="font-size: 0.8rem;">${escHtml(text)}</div>`;
}).join('');
} else {
body += noData;
}
body += `</div>`;
}
else if (nodeId === 'query_analyzer') {
body += `<div class="nm-section"><div class="nm-section-title">🔍 Query Analyzer & Normalizer</div>`;
body += `<div class="nm-card info" style="font-size:0.75rem; line-height:1.4;">
Translates conversational, user-specific phrasing (first-person details, questions) into standard, formal third-person search terminology using GPT-4o-mini. This bridges the vocabulary gap and enables accurate semantic cache matching.
</div>`;
if (data.normalized_query) {
body += `<div class="nm-section-title" style="margin-top:10px;">📌 Normalization Output</div>`;
body += `<div class="nm-card good" style="font-size:0.85rem; font-weight:700;">
"${escHtml(data.normalized_query)}"
</div>`;
} else {
body += noData;
}
body += `</div>`;
}
else if (nodeId === 'redis') {
body += `<div class="nm-section"><div class="nm-section-title">🔴 Redis Semantic Cache Details</div>`;
body += `<div class="nm-card info" style="font-size:0.75rem; line-height:1.4;">
Checks the Redis vector cache (or fallback local cache) for semantically equivalent queries. Bypasses the LangGraph orchestrator and LLM generation completely on cache hits.
</div>`;
if (data.matched_query) {
body += `<div class="nm-card good" style="font-size: 0.8rem; border-left: 3px solid #10b981;">
<strong>⚡ Cache HIT!</strong><br>
<strong>Matched Cached Query:</strong> "${escHtml(data.matched_query)}"<br>
<strong>Similarity Score:</strong> ${data.similarity}%
</div>`;
} else if (data.hit === false) {
body += `<div class="nm-card bad" style="font-size: 0.8rem; border-left: 3px solid #ef4444;">
<strong>❌ Cache MISS</strong><br>
No semantically matching query was found in the cache. Proceeding to standard RAG pipeline.
</div>`;
} else {
body += noData;
}
body += `</div>`;
}
else if (nodeId === 'graphdb') {
// Edge Filtering Logic for Neater Visualisation
let filteredEdges = [];
let filteredEntities = data.entities || [];
if (data.edges && data.edges.length > 0) {
const HUBS = new Set(['bronze', 'silver', 'gold', 'all']);
const queryTokens = (document.getElementById('qinput').value || '').toLowerCase();
const directlyRelevant = new Set();
data.edges.forEach(e => {
[e.source, e.target].forEach(nodeName => {
const nLower = nodeName.toLowerCase();
if (queryTokens.includes(nLower) && nLower.length > 2) {
directlyRelevant.add(nodeName);
}
if (nLower === 'cardiology' && (queryTokens.includes('cardiologist') || queryTokens.includes('cardio'))) {
directlyRelevant.add(nodeName);
}
if (nLower === 'dermatology' && (queryTokens.includes('dermatologist') || queryTokens.includes('derm'))) {
directlyRelevant.add(nodeName);
}
if (nLower === 'pediatrics' && (queryTokens.includes('pediatrician') || queryTokens.includes('ped'))) {
directlyRelevant.add(nodeName);
}
if (nLower === 'chicago, il' && queryTokens.includes('chicago')) {
directlyRelevant.add(nodeName);
}
});
});
const secondaryRelevant = new Set();
data.edges.forEach(e => {
const sHub = HUBS.has(e.source.toLowerCase());
const tHub = HUBS.has(e.target.toLowerCase());
if (directlyRelevant.has(e.source) && !sHub) secondaryRelevant.add(e.target);
if (directlyRelevant.has(e.target) && !tHub) secondaryRelevant.add(e.source);
});
filteredEdges = data.edges.filter(e => {
const sLower = e.source.toLowerCase();
const tLower = e.target.toLowerCase();
const sDirect = directlyRelevant.has(e.source);
const tDirect = directlyRelevant.has(e.target);
const sSec = secondaryRelevant.has(e.source);
const tSec = secondaryRelevant.has(e.target);
if ((sDirect || sSec) && (tDirect || tSec)) {
if (HUBS.has(sLower) && !tDirect && !tSec) return false;
if (HUBS.has(tLower) && !sDirect && !sSec) return false;
return true;
}
return false;
});
if (filteredEdges.length === 0) {
filteredEdges = data.edges.slice(0, 15);
}
const visibleNodes = new Set();
filteredEdges.forEach(e => {
visibleNodes.add(e.source);
visibleNodes.add(e.target);
});
if (data.entities) {
filteredEntities = data.entities.filter(ent => visibleNodes.has(ent) || directlyRelevant.has(ent));
if (filteredEntities.length === 0) {
filteredEntities = data.entities.slice(0, 10);
}
}
}
body += `<div class="nm-section"><div class="nm-section-title">🕸️ Knowledge Graph Lookups</div>`;
if (data.entities && data.entities.length) {
body += `<div class="nm-stat-row"><div class="nm-stat">Entities resolved: <span>${data.entities.length}</span> (showing relevant: ${filteredEntities.length})</div></div>`;
if (filteredEdges.length > 0) {
body += `<div class="nm-section-title" style="margin-top:10px; display:flex; justify-content:space-between; align-items:center;">
<span>📈 Local Graph Explorer (Relevant Nodes)</span>
<button onclick="toggleGraphFullscreen()" style="background:none;border:none;color:#38bdf8;cursor:pointer;font-size:0.8rem;padding:0;">⛶ Fullscreen</button>
</div>`;
body += `<svg id="nm-graph-viz" width="100%" height="250" style="background:#1e293b; border-radius:8px; margin-top:5px; border:1px solid #334155; transition: height 0.3s ease;"></svg>`;
}
body += `<div class="nm-section-title" style="margin-top:10px">📌 Fetched Entities</div>`;
body += filteredEntities.map(e => `<div class="nm-card info" style="font-size: 0.8rem;">${escHtml(e)}</div>`).join('');
} else if (data.entities && data.entities.length === 0) {
body += `<div class="nm-card info">No matching entities found in the knowledge graph.</div>`;
} else {
body += noData;
}
body += `</div>`;
// Save filtered edges globally so we can retrieve them in the D3 rendering phase below
window.lastFilteredEdges = filteredEdges;
}
else {
// Generic fallback: show description + static info
body += `<div class="nm-section"><div class="nm-section-title">📋 Node Information</div>
<div class="nm-card info" style="font-family:inherit;line-height:1.6">${escHtml(desc)}</div>
</div>`;
}
document.getElementById('nm-body').innerHTML = body;
document.getElementById('node-modal-backdrop').classList.add('open');
if (nodeId === 'graphdb' && window.lastFilteredEdges && window.lastFilteredEdges.length > 0) {
setTimeout(() => {
const container = document.getElementById('nm-graph-viz');
if (!container) return;
const width = container.clientWidth || 400;
const height = 250;
const svg = d3.select('#nm-graph-viz');
svg.selectAll('*').remove();
const nodesMap = {};
window.lastFilteredEdges.forEach(e => {
nodesMap[e.source] = { id: e.source };
nodesMap[e.target] = { id: e.target };
});
const nodes = Object.values(nodesMap);
const links = window.lastFilteredEdges.map(e => ({ source: e.source, target: e.target, label: e.relation }));
const simulation = d3.forceSimulation(nodes)
.force("link", d3.forceLink(links).id(d => d.id).distance(120))
.force("charge", d3.forceManyBody().strength(-200))
.force("center", d3.forceCenter(width / 2, height / 2));
window.graphSimulation = simulation; // Save for resize
svg.append("defs").selectAll("marker")
.data(["arrow"])
.join("marker")
.attr("id", String)
.attr("viewBox", "0 -5 10 10")
.attr("refX", 18)
.attr("refY", 0)
.attr("markerWidth", 6)
.attr("markerHeight", 6)
.attr("orient", "auto")
.append("path")
.attr("fill", "#64748b")
.attr("d", "M0,-5L10,0L0,5");
const link = svg.append("g")
.selectAll("line")
.data(links)
.join("line")
.attr("stroke", "#64748b")
.attr("stroke-opacity", 0.6)
.attr("stroke-width", 1.5)
.attr("marker-end", "url(#arrow)");
const linkLabel = svg.append("g")
.selectAll("text")
.data(links)
.join("text")
.text(d => d.label)
.attr("font-size", "9px")
.attr("fill", "#cbd5e1")
.attr("dy", "-4")
.attr("text-anchor", "middle");
const node = svg.append("g")
.selectAll("circle")
.data(nodes)
.join("circle")
.attr("r", 7)
.attr("fill", "#22d3ee")
.attr("stroke", "#0f172a")
.attr("stroke-width", 1.5)
.call(d3.drag()
.on("start", e => { if (!e.active) simulation.alphaTarget(0.3).restart(); e.subject.fx = e.subject.x; e.subject.fy = e.subject.y; })
.on("drag", e => { e.subject.fx = e.x; e.subject.fy = e.y; })
.on("end", e => { if (!e.active) simulation.alphaTarget(0); e.subject.fx = null; e.subject.fy = null; }));
const label = svg.append("g")
.selectAll("text")
.data(nodes)
.join("text")
.text(d => d.id)
.attr("font-size", "10px")
.attr("fill", "#f8fafc")
.attr("dx", 12)
.attr("dy", 4)
.style("pointer-events", "none");
simulation.on("tick", () => {
link
.attr("x1", d => d.source.x)
.attr("y1", d => d.source.y)
.attr("x2", d => d.target.x)
.attr("y2", d => d.target.y);
linkLabel
.attr("x", d => (d.source.x + d.target.x) / 2)
.attr("y", d => (d.source.y + d.target.y) / 2);
node
.attr("cx", d => d.x = Math.max(10, Math.min(width - 10, d.x)))
.attr("cy", d => d.y = Math.max(10, Math.min(height - 10, d.y)));
label
.attr("x", d => d.x)
.attr("y", d => d.y);
});
}, 100);
}
if ((nodeId === 'vectordb' || nodeId === 'bm25' || nodeId === 'ensemble' || nodeId === 'reranker') && data.retrieved_chunks && data.retrieved_chunks.length > 0) {
setTimeout(() => {
renderScoreGraph(data.retrieved_chunks);
}, 100);
}
}
window.renderScoreGraph = function(chunks) {
const container = document.getElementById('nm-score-viz');
if (!container) return;
const width = container.clientWidth || 400;
const height = container.clientHeight || 250;
const svg = d3.select('#nm-score-viz');
svg.selectAll('*').remove();
const data = chunks.filter(c => typeof c !== 'string').sort((a,b) => b.score - a.score);
if (data.length === 0) return;
const margin = {top: 20, right: 30, bottom: 40, left: 160};
const innerWidth = width - margin.left - margin.right;
const innerHeight = height - margin.top - margin.bottom;
// Use absolute value for max to handle both positive similarity (VectorDB) and negative ranks/bm25
const maxScore = d3.max(data, d => Math.abs(d.score)) || 1;
const minScore = d3.min(data, d => Math.min(0, d.score)) || 0;
const x = d3.scaleLinear()
.domain([minScore, maxScore * 1.1])
.range([0, innerWidth]);
const y = d3.scaleBand()
.domain(data.map((d, i) => `#${i+1} ${d.source}`))
.range([0, innerHeight])
.padding(0.2);
const g = svg.append("g").attr("transform", `translate(${margin.left},${margin.top})`);
g.selectAll("rect")
.data(data)
.join("rect")
.attr("y", (d, i) => y(`#${i+1} ${d.source}`))
.attr("height", y.bandwidth())
.attr("x", d => x(Math.min(0, d.score)))
.attr("width", d => Math.max(1, Math.abs(x(d.score) - x(0))))
.attr("fill", "#38bdf8")
.attr("rx", 4);
g.append("g")
.call(d3.axisLeft(y).tickSize(0))
.selectAll("text")
.attr("fill", "#f8fafc")
.style("font-size", "10px");
g.append("g")
.attr("transform", `translate(0,${innerHeight})`)
.call(d3.axisBottom(x).ticks(5))
.selectAll("text")
.attr("fill", "#94a3b8");
window.currentScoreData = chunks; // save for resize
}
window.toggleScoreFullscreen = function() {
const modal = document.getElementById('node-modal');
const svg = document.getElementById('nm-score-viz');
if (!modal || !svg) return;
if (modal.style.maxWidth === '95vw') {
modal.style.maxWidth = '600px';
modal.style.width = '90%';
modal.style.height = 'auto';
svg.style.height = '250px';
} else {
modal.style.maxWidth = '95vw';
modal.style.width = '95vw';
modal.style.height = '90vh';
svg.style.height = 'calc(100vh - 200px)';
}
if (window.currentScoreData) {
setTimeout(() => {
renderScoreGraph(window.currentScoreData);
}, 350);
}
}
window.toggleGraphFullscreen = function() {
const modal = document.getElementById('node-modal');
const svg = document.getElementById('nm-graph-viz');
if (!modal || !svg) return;
if (modal.style.maxWidth === '95vw') {
modal.style.maxWidth = '600px';
modal.style.width = '90%';
modal.style.height = 'auto';
svg.style.height = '250px';
} else {
modal.style.maxWidth = '95vw';
modal.style.width = '95vw';
modal.style.height = '90vh';
svg.style.height = 'calc(100vh - 200px)';
}
// Recenter simulation
if (window.graphSimulation) {
setTimeout(() => {
const width = svg.clientWidth;
const height = svg.clientHeight;
window.graphSimulation.force("center", d3.forceCenter(width / 2, height / 2));
window.graphSimulation.alpha(0.3).restart();
}, 350); // wait for CSS transition
}
};
function closeNodeModal(e) {
if (!e || e.target === document.getElementById('node-modal-backdrop')) {
document.getElementById('node-modal-backdrop').classList.remove('open');
const modal = document.getElementById('node-modal');
if (modal) {
modal.style.maxWidth = '600px';
modal.style.width = '90%';
modal.style.height = 'auto';
}
}
}
// ── KEYBOARD SHORTCUTS ────────────────────────────────────────
document.addEventListener('keydown', e => {
if (document.getElementById('login-overlay').style.display !== 'none') return;
// Cmd+K / Ctrl+K → palette
if ((e.metaKey || e.ctrlKey) && e.key === 'k') { e.preventDefault(); openPalette(); return; }
// Escape → close palette or node modal
if (e.key === 'Escape') {
if (document.getElementById('node-modal-backdrop').classList.contains('open')) {
closeNodeModal();
} else {
closePalette();
}
return;
}
// Only trigger shortcuts when NOT typing in textarea/input
if (['INPUT','TEXTAREA'].includes(document.activeElement?.tagName)) return;
if (e.key === 'r' || e.key === 'R') run();
if (e.key === 'c' || e.key === 'C') clearAll();
if (e.key === 'm' || e.key === 'M') toggleMemory();
if (e.key === 'e' || e.key === 'E') downloadTrace();
if (e.key === 'p' || e.key === 'P') togglePresets();
});
// Ctrl+Enter in textarea = run
document.getElementById('qinput').addEventListener('keydown', e => {
if (e.key === 'Enter' && (e.metaKey || e.ctrlKey)) run();
});
// Click outside palette to close
document.getElementById('palette-overlay').addEventListener('click', e => {
if (e.target === document.getElementById('palette-overlay')) closePalette();
});
</script>
</body>
</html>