/** * Dashboard — Frontend Logic * Fetches data from /dashboard/* API endpoints and renders charts + tables. */ const API = window.location.origin; let currentPage = 0; const PAGE_SIZE = 20; let allInteractions = []; let trendChart = null; let mistakesChart = null; // ── Initialize ──────────────────────────────────────────────── document.addEventListener('DOMContentLoaded', () => { refreshAll(); }); async function refreshAll() { await Promise.all([ loadOverview(), loadTrends(), loadMistakes(), loadInteractions(), loadAgents(), ]); } // ── Overview KPIs ───────────────────────────────────────────── async function loadOverview() { try { const res = await fetch(`${API}/dashboard/overview`); const data = await res.json(); document.getElementById('kpi-total').textContent = data.total_interactions.toLocaleString(); document.getElementById('kpi-avg-score').textContent = `${data.avg_overall_score}%`; document.getElementById('kpi-savings').textContent = `${data.avg_token_savings}%`; document.getElementById('kpi-rewrite').textContent = `${data.rewrite_acceptance_rate}%`; document.getElementById('kpi-split').textContent = `${data.human_count}H / ${data.agent_count}A`; document.getElementById('kpi-total-tokens').textContent = data.total_tokens.toLocaleString(); document.getElementById('kpi-avg-tokens').textContent = Math.round(data.avg_tokens_per_prompt).toLocaleString(); } catch (e) { console.error('Failed to load overview:', e); } } // ── Trends Chart ────────────────────────────────────────────── async function loadTrends(params = {}) { try { const url = new URL(`${API}/dashboard/trends`); if (params.hours) { url.searchParams.set('hours', params.hours); } else { url.searchParams.set('days', params.days || 30); } const res = await fetch(url); const data = await res.json(); if (!data || data.length === 0) { document.getElementById('trend-chart').style.display = 'none'; document.getElementById('trend-empty').classList.remove('hidden'); return; } document.getElementById('trend-chart').style.display = 'block'; document.getElementById('trend-empty').classList.add('hidden'); const ctx = document.getElementById('trend-chart').getContext('2d'); if (trendChart) trendChart.destroy(); // Format labels based on whether data is hourly or daily const labels = data.map(d => { if (d.date && d.date.includes(':')) { // Hourly format: show time const parts = d.date.split(' '); return parts.length > 1 ? parts[1] : d.date; } return d.date; }); // Calculate max interactions for axis scaling const maxCount = Math.max(...data.map(d => d.count), 1); trendChart = new Chart(ctx, { type: 'line', data: { labels: labels, datasets: [{ label: 'Avg Quality Score', data: data.map(d => d.avg_score), borderColor: '#3b82f6', backgroundColor: 'rgba(59, 130, 246, 0.1)', fill: true, tension: 0.4, pointRadius: 4, pointBackgroundColor: '#3b82f6', }, { label: 'Interactions', data: data.map(d => d.count), borderColor: '#8b5cf6', backgroundColor: 'rgba(139, 92, 246, 0.1)', fill: false, tension: 0.4, pointRadius: 3, pointBackgroundColor: '#8b5cf6', yAxisID: 'y1', }], }, options: { responsive: true, maintainAspectRatio: false, interaction: { mode: 'index', intersect: false }, plugins: { legend: { labels: { color: '#8899b4', font: { family: 'Inter', size: 12 } } }, }, scales: { x: { ticks: { color: '#5a6a85', font: { size: 11 }, maxRotation: 45 }, grid: { color: 'rgba(42, 52, 82, 0.5)' }, }, y: { min: 0, max: 100, ticks: { color: '#5a6a85', font: { size: 11 } }, grid: { color: 'rgba(42, 52, 82, 0.5)' }, }, y1: { position: 'right', min: 0, suggestedMax: maxCount + 1, ticks: { color: '#5a6a85', font: { size: 11 }, stepSize: 1, precision: 0, }, grid: { display: false }, }, }, }, }); } catch (e) { console.error('Failed to load trends:', e); } } function setTrendFilter(btn) { // Update active state document.querySelectorAll('.filter-btn').forEach(b => b.classList.remove('active')); btn.classList.add('active'); // Build params const params = {}; if (btn.dataset.hours) { params.hours = parseInt(btn.dataset.hours); } else if (btn.dataset.days) { params.days = parseInt(btn.dataset.days); } loadTrends(params); } // ── Mistakes Chart ──────────────────────────────────────────── async function loadMistakes() { try { const res = await fetch(`${API}/dashboard/mistakes?limit=6`); const data = await res.json(); if (!data || data.length === 0) { document.getElementById('mistakes-chart').style.display = 'none'; document.getElementById('mistakes-empty').classList.remove('hidden'); return; } document.getElementById('mistakes-chart').style.display = 'block'; document.getElementById('mistakes-empty').classList.add('hidden'); const ctx = document.getElementById('mistakes-chart').getContext('2d'); if (mistakesChart) mistakesChart.destroy(); const colors = ['#ef4444', '#f59e0b', '#3b82f6', '#8b5cf6', '#06b6d4', '#10b981']; mistakesChart = new Chart(ctx, { type: 'doughnut', data: { labels: data.map(d => formatMistakeType(d.type)), datasets: [{ data: data.map(d => d.count), backgroundColor: colors.slice(0, data.length), borderColor: '#1a2235', borderWidth: 3, }], }, options: { responsive: true, maintainAspectRatio: false, plugins: { legend: { position: 'right', labels: { color: '#8899b4', font: { family: 'Inter', size: 11 }, padding: 12 }, }, }, }, }); } catch (e) { console.error('Failed to load mistakes:', e); } } // ── Interactions Feed ───────────────────────────────────────── async function loadInteractions() { try { const projectFilter = document.getElementById('feed-filter').value.trim() || null; const url = new URL(`${API}/dashboard/interactions`); url.searchParams.set('limit', PAGE_SIZE); url.searchParams.set('offset', currentPage * PAGE_SIZE); if (projectFilter) url.searchParams.set('project_id', projectFilter); const res = await fetch(url); const data = await res.json(); allInteractions = data.interactions; const total = data.total; renderFeed(allInteractions); // Pagination const totalPages = Math.ceil(total / PAGE_SIZE) || 1; document.getElementById('page-info').textContent = `Page ${currentPage + 1} of ${totalPages}`; document.getElementById('prev-btn').disabled = currentPage === 0; document.getElementById('next-btn').disabled = (currentPage + 1) * PAGE_SIZE >= total; } catch (e) { console.error('Failed to load interactions:', e); } } function renderFeed(rows) { const tbody = document.getElementById('feed-body'); if (!rows || rows.length === 0) { tbody.innerHTML = '