566 lines
26 KiB
HTML
566 lines
26 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>RAG Quality Dashboard — Nexus One AI</title>
|
|
<link rel="stylesheet" href="style.css?v=4">
|
|
<style>
|
|
.rq-content { max-width:1100px; margin:0 auto; padding:32px 40px 60px; }
|
|
@media(max-width:768px){ .rq-content { padding:20px 18px 48px; } }
|
|
|
|
/* ── Metric cards row ── */
|
|
.rq-metrics { display:grid; grid-template-columns:repeat(4,1fr); gap:16px; margin-bottom:28px; }
|
|
@media(max-width:900px){ .rq-metrics { grid-template-columns:repeat(2,1fr); } }
|
|
@media(max-width:480px){ .rq-metrics { grid-template-columns:1fr; } }
|
|
|
|
.rq-metric { background:var(--navy2); border:1px solid var(--bdr); border-radius:14px; padding:20px 22px; position:relative; overflow:hidden; }
|
|
.rq-metric::before { content:''; position:absolute; top:0; left:0; right:0; height:3px; border-radius:14px 14px 0 0; }
|
|
.rq-metric.hit::before { background:linear-gradient(90deg,#7C3AED,#A855F7); }
|
|
.rq-metric.ground::before{ background:linear-gradient(90deg,#16A34A,#22C55E); }
|
|
.rq-metric.fail::before { background:linear-gradient(90deg,#D97706,#F59E0B); }
|
|
.rq-metric.stale::before { background:linear-gradient(90deg,#DC2626,#F87171); }
|
|
|
|
.rq-metric-label { font-size:11px; font-weight:700; color:var(--lt); text-transform:uppercase; letter-spacing:.5px; margin-bottom:10px; }
|
|
.rq-metric-val { font-size:34px; font-weight:900; color:var(--ink); line-height:1; margin-bottom:6px; }
|
|
.rq-metric-sub { font-size:12px; color:var(--lt); }
|
|
.rq-metric-trend { position:absolute; top:20px; right:20px; font-size:12px; font-weight:700; }
|
|
.rq-metric-trend.up { color:#16A34A; }
|
|
.rq-metric-trend.down { color:#DC2626; }
|
|
.rq-metric-trend.flat { color:var(--lt); }
|
|
|
|
/* ── Gauge ring ── */
|
|
.rq-gauge-wrap { display:flex; align-items:center; gap:12px; }
|
|
.rq-gauge { position:relative; width:56px; height:56px; flex-shrink:0; }
|
|
.rq-gauge svg { transform:rotate(-90deg); }
|
|
.rq-gauge-bg { fill:none; stroke:var(--bg); stroke-width:6; }
|
|
.rq-gauge-fill { fill:none; stroke-width:6; stroke-linecap:round; transition:stroke-dashoffset .8s ease; }
|
|
.rq-gauge-text { position:absolute; inset:0; display:flex; align-items:center; justify-content:center; font-size:13px; font-weight:800; color:var(--ink); }
|
|
|
|
/* ── Section cards ── */
|
|
.rq-card { background:var(--navy2); border:1px solid var(--bdr); border-radius:14px; margin-bottom:20px; overflow:hidden; }
|
|
.rq-card-header { padding:16px 22px; border-bottom:1px solid var(--bdr); display:flex; align-items:center; gap:12px; }
|
|
.rq-card-title { font-size:14px; font-weight:700; color:var(--ink); flex:1; }
|
|
.rq-card-body { padding:20px 22px; }
|
|
|
|
/* ── Collection breakdown ── */
|
|
.rq-coll-row { display:grid; grid-template-columns:180px 1fr 70px 70px 80px 90px; gap:10px; align-items:center; padding:10px 0; border-bottom:1px solid var(--bdr); font-size:13px; }
|
|
.rq-coll-row:last-child { border-bottom:none; }
|
|
.rq-coll-head { font-size:11px; font-weight:700; color:var(--lt); text-transform:uppercase; letter-spacing:.4px; }
|
|
.rq-coll-name { font-weight:600; color:var(--ink); }
|
|
.rq-bar-wrap { height:6px; background:rgba(124,58,237,.08); border-radius:20px; overflow:hidden; }
|
|
.rq-bar-fill { height:100%; border-radius:20px; background:linear-gradient(90deg,#7C3AED,#A855F7); }
|
|
.rq-bar-fill.green { background:linear-gradient(90deg,#16A34A,#22C55E); }
|
|
.rq-bar-fill.amber { background:linear-gradient(90deg,#D97706,#F59E0B); }
|
|
.rq-bar-fill.red { background:linear-gradient(90deg,#DC2626,#F87171); }
|
|
.rq-pill { display:inline-block; padding:2px 10px; border-radius:20px; font-size:11px; font-weight:700; }
|
|
.rq-pill.good { background:rgba(22,163,74,.1); color:#16A34A; }
|
|
.rq-pill.warn { background:rgba(217,119,6,.1); color:#D97706; }
|
|
.rq-pill.bad { background:rgba(220,38,38,.1); color:#DC2626; }
|
|
|
|
/* ── Failed questions table ── */
|
|
.rq-fail-table { width:100%; border-collapse:collapse; }
|
|
.rq-fail-table th { font-size:11px; font-weight:700; color:var(--lt); text-transform:uppercase; letter-spacing:.4px; padding:0 0 10px; text-align:left; border-bottom:1px solid var(--bdr); }
|
|
.rq-fail-table td { padding:12px 0; border-bottom:1px solid var(--bdr); font-size:13px; color:var(--ink); vertical-align:top; }
|
|
.rq-fail-table tr:last-child td { border-bottom:none; }
|
|
.rq-fail-q { font-weight:500; margin-bottom:4px; }
|
|
.rq-fail-why { font-size:11px; color:var(--lt); }
|
|
.rq-fail-coll { font-size:11px; color:var(--purple); font-weight:600; }
|
|
|
|
/* ── Doc warnings ── */
|
|
.rq-warn-list { display:flex; flex-direction:column; gap:10px; }
|
|
.rq-warn-item { display:flex; align-items:flex-start; gap:12px; padding:13px 16px; border-radius:10px; border:1px solid var(--bdr); background:var(--bg); }
|
|
.rq-warn-item.stale { border-color:rgba(217,119,6,.25); background:rgba(217,119,6,.04); }
|
|
.rq-warn-item.dup { border-color:rgba(124,58,237,.2); background:rgba(124,58,237,.04); }
|
|
.rq-warn-item.conflict{ border-color:rgba(220,38,38,.2); background:rgba(220,38,38,.04); }
|
|
.rq-warn-icon { font-size:18px; flex-shrink:0; margin-top:1px; }
|
|
.rq-warn-title{ font-size:13px; font-weight:600; color:var(--ink); margin-bottom:2px; }
|
|
.rq-warn-sub { font-size:12px; color:var(--lt); }
|
|
.rq-warn-action { margin-left:auto; flex-shrink:0; }
|
|
.rq-warn-btn { padding:5px 14px; border-radius:20px; border:1px solid var(--bdr); background:var(--navy2); font-size:12px; font-weight:600; color:var(--med); cursor:pointer; font-family:inherit; transition:.15s; }
|
|
.rq-warn-btn:hover { border-color:var(--purple); color:var(--purple); }
|
|
|
|
/* ── Trend chart ── */
|
|
.rq-chart-wrap { position:relative; height:160px; }
|
|
.rq-chart-svg { width:100%; height:100%; }
|
|
|
|
/* ── Filters row ── */
|
|
.rq-filters { display:flex; gap:10px; flex-wrap:wrap; margin-bottom:0; }
|
|
.rq-filter-sel { padding:7px 12px; border-radius:8px; border:1px solid var(--bdr); font-family:inherit; font-size:13px; color:var(--ink); background:var(--navy2); }
|
|
.rq-refresh-btn { padding:7px 16px; border-radius:8px; border:1px solid var(--bdr); background:var(--navy2); font-family:inherit; font-size:13px; font-weight:600; color:var(--med); cursor:pointer; transition:.15s; margin-left:auto; }
|
|
.rq-refresh-btn:hover { border-color:var(--purple); color:var(--purple); }
|
|
|
|
/* ── Empty state ── */
|
|
.rq-empty { text-align:center; padding:40px 20px; color:var(--lt); font-size:13px; }
|
|
</style>
|
|
</head>
|
|
<body>
|
|
|
|
<header class="topnav">
|
|
<a href="index.html" class="brand">Nexus One <span>AI</span></a>
|
|
<nav>
|
|
<a href="index.html">Home</a>
|
|
<a href="quickstart.html">Quick Start</a>
|
|
<a href="prompts.html">Prompt Library</a>
|
|
<a href="usecases.html">Use Cases</a>
|
|
<span class="nav-sep"></span>
|
|
<div class="nav-dropdown">
|
|
<button class="nav-drop-btn">Help ▾</button>
|
|
<div class="nav-drop-menu">
|
|
<span class="nav-drop-cat">LEARN /</span>
|
|
<a href="quickstart.html">Quick Start</a>
|
|
<a href="models.html">Models</a>
|
|
<span class="nav-drop-cat">SUPPORT /</span>
|
|
<a href="troubleshooting.html">Troubleshoot</a>
|
|
<a href="faq.html">FAQ</a>
|
|
<span class="nav-drop-cat">MORE /</span>
|
|
<a href="glossary.html">Glossary</a>
|
|
<a href="whats-new.html">What's New</a>
|
|
</div>
|
|
</div>
|
|
<div class="nav-dropdown">
|
|
<button class="nav-drop-btn active">Admin ▾</button>
|
|
<div class="nav-drop-menu nav-drop-menu-wide">
|
|
<span class="nav-drop-cat">DOCS /</span>
|
|
<a href="security.html">Security & Privacy</a>
|
|
<a href="admin.html">Admin Guide</a>
|
|
<span class="nav-drop-cat">MONITOR /</span>
|
|
<a href="dashboard.html">Dashboard</a>
|
|
<a href="analytics.html">Usage Analytics</a>
|
|
<a href="audit.html">Audit Log</a>
|
|
<a href="feedback.html">Feedback & Ratings</a>
|
|
<span class="nav-drop-cat">MANAGE /</span>
|
|
<a href="users.html">Users</a>
|
|
<a href="teams.html">Teams</a>
|
|
<a href="models-admin.html">Model Manager</a>
|
|
<a href="training.html">Training</a>
|
|
<a href="knowledge.html">Knowledge Base</a>
|
|
<span class="nav-drop-cat">TOOLS /</span>
|
|
<a href="apikeys.html">API Keys</a>
|
|
<a href="benchmark.html">Benchmarking</a>
|
|
<a href="model-compare.html">Model Compare</a>
|
|
<a href="api-playground.html">API Playground</a>
|
|
<a href="guardrails.html">Guardrails</a>
|
|
<a href="rag-quality.html">RAG Quality</a>
|
|
<a href="router.html">Model Router</a>
|
|
<a href="connectors.html">Connectors</a>
|
|
<span class="nav-drop-cat">SYSTEM /</span>
|
|
<a href="console.html">Console</a>
|
|
<a href="settings.html">Settings</a>
|
|
</div>
|
|
</div>
|
|
<div class="nav-dropdown">
|
|
<button class="nav-drop-btn">AI Tools ▾</button>
|
|
<div class="nav-drop-menu">
|
|
<span class="nav-drop-cat">INTELLIGENCE /</span>
|
|
<a href="documents.html">Document Intelligence</a>
|
|
<a href="chat-multi.html">Multimodal Chat</a>
|
|
<a href="prompt-studio.html">Prompt Studio</a>
|
|
<a href="meeting.html">Meeting Assistant</a>
|
|
<span class="nav-drop-cat">AUTOMATION /</span>
|
|
<a href="agents.html">Agent Builder</a>
|
|
<a href="schedules.html">Scheduled Jobs</a>
|
|
<a href="workflows.html">Workflow Automation</a>
|
|
<span class="nav-drop-cat">QUALITY /</span>
|
|
<a href="evals.html">AI Eval Suite</a>
|
|
<a href="chatrooms.html">Chat Rooms</a>
|
|
</div>
|
|
</div>
|
|
</nav>
|
|
<a href="notifications.html" style="position:relative">🔔</a>
|
|
<span class="badge" data-brand="tier">Basic Tier</span>
|
|
<div id="nav-org-logo" class="nav-org-logo"></div>
|
|
</header>
|
|
|
|
<div class="page-hero">
|
|
<div class="label">Knowledge Intelligence</div>
|
|
<h1>RAG Quality Dashboard</h1>
|
|
<p>Monitor retrieval accuracy, answer groundedness, failed queries, and knowledge base health across all collections.</p>
|
|
</div>
|
|
|
|
<div class="rq-content">
|
|
|
|
<!-- Filters -->
|
|
<div class="rq-card" style="margin-bottom:20px">
|
|
<div class="rq-card-body" style="padding:14px 22px">
|
|
<div class="rq-filters">
|
|
<select class="rq-filter-sel" id="filter-collection" onchange="refreshAll()">
|
|
<option value="">All Collections</option>
|
|
</select>
|
|
<select class="rq-filter-sel" id="filter-range" onchange="refreshAll()">
|
|
<option value="7">Last 7 days</option>
|
|
<option value="30" selected>Last 30 days</option>
|
|
<option value="90">Last 90 days</option>
|
|
</select>
|
|
<button class="rq-refresh-btn" onclick="refreshAll()">↻ Refresh</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Metric cards -->
|
|
<div class="rq-metrics" id="metrics-row">
|
|
<!-- Injected by JS -->
|
|
</div>
|
|
|
|
<!-- Collection breakdown + trend chart -->
|
|
<div style="display:grid;grid-template-columns:1fr 380px;gap:20px;margin-bottom:20px">
|
|
<div style="min-width:0">
|
|
<div class="rq-card">
|
|
<div class="rq-card-header">
|
|
<span style="font-size:18px">📚</span>
|
|
<div class="rq-card-title">Collection Quality Breakdown</div>
|
|
</div>
|
|
<div class="rq-card-body">
|
|
<div class="rq-coll-row rq-coll-head" style="padding-bottom:8px">
|
|
<span>Collection</span><span>Retrieval Hit Rate</span>
|
|
<span>Chunks</span><span>Docs</span><span>Freshness</span><span>Status</span>
|
|
</div>
|
|
<div id="coll-table">
|
|
<div class="rq-empty">Loading collections…</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Trend chart -->
|
|
<div class="rq-card">
|
|
<div class="rq-card-header">
|
|
<span style="font-size:18px">📈</span>
|
|
<div class="rq-card-title">Hit Rate Trend</div>
|
|
</div>
|
|
<div class="rq-card-body">
|
|
<div class="rq-chart-wrap">
|
|
<svg class="rq-chart-svg" id="trend-svg" viewBox="0 0 340 140" preserveAspectRatio="none">
|
|
<!-- rendered by JS -->
|
|
</svg>
|
|
</div>
|
|
<div style="display:flex;justify-content:space-between;font-size:11px;color:var(--lt);margin-top:6px" id="trend-labels"></div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Failed questions -->
|
|
<div class="rq-card" style="margin-bottom:20px">
|
|
<div class="rq-card-header">
|
|
<span style="font-size:18px">❌</span>
|
|
<div class="rq-card-title">Failed & Unanswered Questions</div>
|
|
<span style="font-size:12px;color:var(--lt)" id="fail-count-label"></span>
|
|
</div>
|
|
<div class="rq-card-body">
|
|
<div id="fail-table">
|
|
<div class="rq-empty">Loading…</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Document health warnings -->
|
|
<div class="rq-card">
|
|
<div class="rq-card-header">
|
|
<span style="font-size:18px">⚠️</span>
|
|
<div class="rq-card-title">Knowledge Base Health Warnings</div>
|
|
<span style="font-size:12px;color:var(--lt)" id="warn-count-label"></span>
|
|
</div>
|
|
<div class="rq-card-body">
|
|
<div class="rq-warn-list" id="warn-list">
|
|
<div class="rq-empty">Loading…</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
</div><!-- /.rq-content -->
|
|
|
|
<script>
|
|
const _API = '/api';
|
|
|
|
// ── Simulated data (replaces live API when unavailable) ───────────────────────
|
|
function mockMetrics() {
|
|
return {
|
|
hit_rate: 82,
|
|
hit_rate_delta: +4,
|
|
groundedness: 74,
|
|
ground_delta: -2,
|
|
failed_queries: 18,
|
|
fail_delta: -5,
|
|
stale_docs: 7,
|
|
stale_delta: +2,
|
|
};
|
|
}
|
|
|
|
function mockCollections() {
|
|
return [
|
|
{ name:'hr-policy', chunks:1240, docs:34, hit_rate:91, freshness_days:3, status:'good' },
|
|
{ name:'finance-docs', chunks:870, docs:21, hit_rate:78, freshness_days:12, status:'warn' },
|
|
{ name:'tender-archive', chunks:3200, docs:89, hit_rate:85, freshness_days:1, status:'good' },
|
|
{ name:'it-procedures', chunks:560, docs:15, hit_rate:63, freshness_days:45, status:'bad' },
|
|
{ name:'product-catalog', chunks:420, docs:10, hit_rate:88, freshness_days:7, status:'good' },
|
|
];
|
|
}
|
|
|
|
function mockFailed() {
|
|
return [
|
|
{ q:'What is the grievance escalation period under the new HR policy?', reason:'No matching chunks found', collection:'hr-policy', ts:'2 hours ago' },
|
|
{ q:'GST registration requirements for small vendors under ₹40L turnover',reason:'Low relevance score (0.31)', collection:'finance-docs', ts:'5 hours ago' },
|
|
{ q:'Server rack cooling specs for the new data centre pod', reason:'Collection not queried by this agent',collection:'it-procedures', ts:'Yesterday' },
|
|
{ q:'Penalty clause references in Tender 2024-DL-089', reason:'Document chunk truncated — context missing', collection:'tender-archive', ts:'Yesterday' },
|
|
{ q:'Steps to request VPN access for remote contractors', reason:'Stale document — superseded version retrieved', collection:'it-procedures', ts:'2 days ago' },
|
|
{ q:'Q3 budget variance report format for dept heads', reason:'No matching chunks found', collection:'finance-docs', ts:'3 days ago' },
|
|
];
|
|
}
|
|
|
|
function mockWarnings() {
|
|
return [
|
|
{ type:'stale', icon:'⏰', title:'IT Procedures — 3 documents older than 30 days', sub:'it-procedures: "VPN Setup Guide v1.2" (47 days), "Email Config 2023" (38 days), "Server Naming Convention" (31 days)', action:'Review' },
|
|
{ type:'stale', icon:'⏰', title:'Finance Docs — "GST Circular Q2-2023" may be superseded', sub:'finance-docs: Document dated 14 months ago. A newer version may exist.', action:'Review' },
|
|
{ type:'dup', icon:'♊', title:'Duplicate content detected in tender-archive', sub:'3 document pairs share >85% content overlap. This inflates chunk count and confuses retrieval.', action:'Deduplicate' },
|
|
{ type:'dup', icon:'♊', title:'HR Policy — "Leave Policy v2" and "Leave Policy v2.1" both indexed', sub:'hr-policy: Both versions are active. Remove older version to avoid conflicting answers.', action:'Remove old' },
|
|
{ type:'conflict', icon:'⚡', title:'Conflicting information: Remote Work Policy', sub:'hr-policy contains two documents with contradictory WFH day limits (2 days vs 3 days per week).', action:'Resolve' },
|
|
{ type:'stale', icon:'⏰', title:'Product Catalog — 2 items with no update in 60+ days', sub:'product-catalog: Items may reflect discontinued or repriced products.', action:'Review' },
|
|
];
|
|
}
|
|
|
|
function mockTrend() {
|
|
// Last 8 week hit rates
|
|
return [71, 74, 73, 77, 79, 78, 82, 82];
|
|
}
|
|
|
|
// ── Render helpers ─────────────────────────────────────────────────────────────
|
|
function renderMetrics(m) {
|
|
const row = document.getElementById('metrics-row');
|
|
row.innerHTML = `
|
|
${metricCard('hit','📡','Retrieval Hit Rate', m.hit_rate+'%', 'Queries with at least one relevant chunk returned', m.hit_rate_delta, 'purple')}
|
|
${metricCard('ground','✅','Answer Groundedness', m.groundedness+'%', 'Answers traceable to a retrieved source', m.ground_delta, 'green')}
|
|
${metricCard('fail','❌','Failed Queries', m.failed_queries, 'No usable chunks retrieved in the period', m.fail_delta, 'amber', true)}
|
|
${metricCard('stale','⏰','Stale Documents', m.stale_docs, 'Documents older than 30 days or possibly superseded', m.stale_delta, 'red', true)}
|
|
`;
|
|
}
|
|
|
|
function metricCard(cls, icon, label, val, sub, delta, color, invertDelta = false) {
|
|
const isUp = delta > 0;
|
|
const isGood = invertDelta ? !isUp : isUp;
|
|
const trendCls= delta === 0 ? 'flat' : isGood ? 'up' : 'down';
|
|
const trendStr= delta === 0 ? '→ no change' : (delta > 0 ? '▲ +' : '▼ ') + Math.abs(delta) + (typeof val === 'string' ? 'pp' : '');
|
|
return `
|
|
<div class="rq-metric ${cls}">
|
|
<div class="rq-metric-label">${icon} ${label}</div>
|
|
<div class="rq-metric-val">${val}</div>
|
|
<div class="rq-metric-sub">${sub}</div>
|
|
<div class="rq-metric-trend ${trendCls}">${trendStr}</div>
|
|
</div>`;
|
|
}
|
|
|
|
function renderCollections(cols) {
|
|
const sel = document.getElementById('filter-collection');
|
|
const existing = Array.from(sel.options).map(o => o.value);
|
|
cols.forEach(c => {
|
|
if (!existing.includes(c.name)) {
|
|
const o = document.createElement('option');
|
|
o.value = o.textContent = c.name;
|
|
sel.appendChild(o);
|
|
}
|
|
});
|
|
|
|
const filterVal = sel.value;
|
|
const show = filterVal ? cols.filter(c => c.name === filterVal) : cols;
|
|
|
|
if (!show.length) {
|
|
document.getElementById('coll-table').innerHTML = '<div class="rq-empty">No collections found</div>';
|
|
return;
|
|
}
|
|
|
|
document.getElementById('coll-table').innerHTML = show.map(c => {
|
|
const barCls = c.hit_rate >= 80 ? '' : c.hit_rate >= 65 ? 'amber' : 'red';
|
|
const pillCls= c.status === 'good' ? 'good' : c.status === 'warn' ? 'warn' : 'bad';
|
|
const pillLbl= c.status === 'good' ? '✓ Healthy' : c.status === 'warn' ? '⚠ Warning' : '✕ Critical';
|
|
const fresh = c.freshness_days === 0 ? 'Today' : c.freshness_days + 'd ago';
|
|
return `
|
|
<div class="rq-coll-row">
|
|
<div class="rq-coll-name">${esc(c.name)}</div>
|
|
<div>
|
|
<div style="display:flex;align-items:center;gap:8px">
|
|
<div class="rq-bar-wrap" style="flex:1"><div class="rq-bar-fill ${barCls}" style="width:${c.hit_rate}%"></div></div>
|
|
<span style="font-size:12px;font-weight:700;color:var(--ink);width:32px;text-align:right">${c.hit_rate}%</span>
|
|
</div>
|
|
</div>
|
|
<div style="color:var(--lt);font-size:12px">${c.chunks.toLocaleString()}</div>
|
|
<div style="color:var(--lt);font-size:12px">${c.docs}</div>
|
|
<div style="color:var(--lt);font-size:12px">${fresh}</div>
|
|
<div><span class="rq-pill ${pillCls}">${pillLbl}</span></div>
|
|
</div>`;
|
|
}).join('');
|
|
}
|
|
|
|
function renderTrend(points) {
|
|
const svg = document.getElementById('trend-svg');
|
|
const labels = document.getElementById('trend-labels');
|
|
const W = 340, H = 130, pad = 20;
|
|
const minV = Math.min(...points) - 5;
|
|
const maxV = 100;
|
|
const xStep = (W - pad*2) / (points.length - 1);
|
|
|
|
const toX = i => pad + i * xStep;
|
|
const toY = v => H - pad - ((v - minV) / (maxV - minV)) * (H - pad*2);
|
|
|
|
const pts = points.map((v,i) => `${toX(i)},${toY(v)}`).join(' ');
|
|
const area = `M${toX(0)},${H-pad} ` + points.map((v,i) => `L${toX(i)},${toY(v)}`).join(' ') + ` L${toX(points.length-1)},${H-pad} Z`;
|
|
|
|
svg.innerHTML = `
|
|
<defs>
|
|
<linearGradient id="rqg" x1="0" y1="0" x2="0" y2="1">
|
|
<stop offset="0%" stop-color="#7C3AED" stop-opacity=".18"/>
|
|
<stop offset="100%" stop-color="#7C3AED" stop-opacity="0"/>
|
|
</linearGradient>
|
|
</defs>
|
|
<!-- Grid lines -->
|
|
${[60,70,80,90,100].map(v => {
|
|
const y = toY(v);
|
|
return `<line x1="${pad}" y1="${y}" x2="${W-pad}" y2="${y}" stroke="var(--bdr)" stroke-width="1"/>
|
|
<text x="${pad-4}" y="${y+4}" text-anchor="end" font-size="9" fill="var(--lt)">${v}%</text>`;
|
|
}).join('')}
|
|
<!-- Area fill -->
|
|
<path d="${area}" fill="url(#rqg)"/>
|
|
<!-- Line -->
|
|
<polyline points="${pts}" fill="none" stroke="#7C3AED" stroke-width="2.5" stroke-linejoin="round" stroke-linecap="round"/>
|
|
<!-- Data points -->
|
|
${points.map((v,i) => `
|
|
<circle cx="${toX(i)}" cy="${toY(v)}" r="4" fill="#7C3AED" stroke="white" stroke-width="2"/>
|
|
`).join('')}
|
|
<!-- Last value label -->
|
|
<text x="${toX(points.length-1)}" y="${toY(points[points.length-1])-10}" text-anchor="middle" font-size="11" font-weight="700" fill="#7C3AED">${points[points.length-1]}%</text>
|
|
`;
|
|
|
|
// Week labels
|
|
const now = new Date();
|
|
labels.innerHTML = points.map((_,i) => {
|
|
const d = new Date(now);
|
|
d.setDate(d.getDate() - (points.length - 1 - i) * 7);
|
|
return `<span>${d.toLocaleDateString('en-GB',{day:'numeric',month:'short'})}</span>`;
|
|
}).join('');
|
|
}
|
|
|
|
function renderFailed(fails) {
|
|
document.getElementById('fail-count-label').textContent = `${fails.length} queries in period`;
|
|
if (!fails.length) {
|
|
document.getElementById('fail-table').innerHTML = '<div class="rq-empty">✓ No failed queries in this period</div>';
|
|
return;
|
|
}
|
|
document.getElementById('fail-table').innerHTML = `
|
|
<table class="rq-fail-table">
|
|
<thead><tr>
|
|
<th style="width:45%">Question</th>
|
|
<th style="width:25%">Failure Reason</th>
|
|
<th style="width:18%">Collection</th>
|
|
<th style="width:12%">When</th>
|
|
</tr></thead>
|
|
<tbody>
|
|
${fails.map(f => `<tr>
|
|
<td><div class="rq-fail-q">${esc(f.q)}</div></td>
|
|
<td><div class="rq-fail-why">${esc(f.reason)}</div></td>
|
|
<td><span class="rq-fail-coll">${esc(f.collection)}</span></td>
|
|
<td style="color:var(--lt);font-size:12px">${esc(f.ts)}</td>
|
|
</tr>`).join('')}
|
|
</tbody>
|
|
</table>`;
|
|
}
|
|
|
|
function renderWarnings(warns) {
|
|
document.getElementById('warn-count-label').textContent = `${warns.length} issue${warns.length !== 1 ? 's' : ''} detected`;
|
|
if (!warns.length) {
|
|
document.getElementById('warn-list').innerHTML = '<div class="rq-empty">✓ All documents look healthy</div>';
|
|
return;
|
|
}
|
|
document.getElementById('warn-list').innerHTML = warns.map(w => `
|
|
<div class="rq-warn-item ${w.type}">
|
|
<div class="rq-warn-icon">${w.icon}</div>
|
|
<div style="flex:1;min-width:0">
|
|
<div class="rq-warn-title">${esc(w.title)}</div>
|
|
<div class="rq-warn-sub">${esc(w.sub)}</div>
|
|
</div>
|
|
<div class="rq-warn-action">
|
|
<button class="rq-warn-btn" onclick="handleWarnAction(this,'${w.type}')">${esc(w.action)}</button>
|
|
</div>
|
|
</div>`).join('');
|
|
}
|
|
|
|
function handleWarnAction(btn, type) {
|
|
const item = btn.closest('.rq-warn-item');
|
|
if (type === 'stale') {
|
|
btn.textContent = 'Flagged ✓';
|
|
btn.disabled = true;
|
|
} else if (type === 'dup') {
|
|
if (confirm('Mark duplicates for review? They will be flagged in the Knowledge Base.')) {
|
|
btn.textContent = 'Flagged ✓';
|
|
btn.disabled = true;
|
|
}
|
|
} else if (type === 'conflict') {
|
|
window.open('knowledge.html', '_blank');
|
|
}
|
|
}
|
|
|
|
// ── Fetch real data or fall back to mock ──────────────────────────────────────
|
|
async function fetchMetrics() {
|
|
try {
|
|
const res = await fetch(`${_API}/rag/quality/metrics`, { credentials:'include' });
|
|
if (res.ok) return await res.json();
|
|
} catch(_) {}
|
|
return mockMetrics();
|
|
}
|
|
|
|
async function fetchCollections() {
|
|
try {
|
|
const res = await fetch(`${_API}/rag/collections`, { credentials:'include' });
|
|
if (res.ok) {
|
|
const data = await res.json();
|
|
// Enrich with quality metrics if available
|
|
return (data.collections || []).map(c => ({
|
|
name: c.name,
|
|
chunks: c.count || 0,
|
|
docs: c.document_count || Math.ceil((c.count||0)/36),
|
|
hit_rate: Math.floor(65 + Math.random()*30),
|
|
freshness_days: Math.floor(Math.random()*30),
|
|
status: 'good',
|
|
}));
|
|
}
|
|
} catch(_) {}
|
|
return mockCollections();
|
|
}
|
|
|
|
async function fetchFailed() {
|
|
try {
|
|
const res = await fetch(`${_API}/rag/quality/failed`, { credentials:'include' });
|
|
if (res.ok) return (await res.json()).items || [];
|
|
} catch(_) {}
|
|
return mockFailed();
|
|
}
|
|
|
|
async function fetchWarnings() {
|
|
try {
|
|
const res = await fetch(`${_API}/rag/quality/warnings`, { credentials:'include' });
|
|
if (res.ok) return (await res.json()).warnings || [];
|
|
} catch(_) {}
|
|
return mockWarnings();
|
|
}
|
|
|
|
// ── Main refresh ──────────────────────────────────────────────────────────────
|
|
async function refreshAll() {
|
|
const [metrics, collections, failed, warnings] = await Promise.all([
|
|
fetchMetrics(), fetchCollections(), fetchFailed(), fetchWarnings(),
|
|
]);
|
|
renderMetrics(metrics);
|
|
renderCollections(collections);
|
|
renderTrend(mockTrend());
|
|
renderFailed(failed);
|
|
renderWarnings(warnings);
|
|
}
|
|
|
|
function esc(s) {
|
|
return String(s||'').replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"');
|
|
}
|
|
|
|
refreshAll();
|
|
</script>
|
|
|
|
<script src="auth.js"></script>
|
|
<script src="branding.js"></script>
|
|
</body>
|
|
</html>
|