/** * 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 = 'No interactions yet — go analyze some prompts!'; return; } tbody.innerHTML = rows.map(row => { const score = row.overall_score; const scoreClass = score >= 85 ? 'excellent' : score >= 65 ? 'good' : score >= 40 ? 'fair' : 'poor'; const source = row.source_agent ? `🤖 ${escapeHtml(row.source_agent)}` : '👤 Human'; const preview = escapeHtml((row.original_prompt || '').substring(0, 60)) + (row.original_prompt && row.original_prompt.length > 60 ? '...' : ''); const rewrite = row.rewrite_used === 1 ? '✅' : row.rewrite_used === 0 ? '❌' : '—'; const time = formatTime(row.timestamp); return ` ${time} ${source} ${escapeHtml(row.project_id || '—')} ${preview} ${score} ${row.mistake_count} ${row.token_savings_percent}% ${rewrite} `; }).join(''); } function filterFeed() { currentPage = 0; loadInteractions(); } function prevPage() { if (currentPage > 0) { currentPage--; loadInteractions(); } } function nextPage() { currentPage++; loadInteractions(); } // ── Agent Leaderboard ───────────────────────────────────────── async function loadAgents() { try { const res = await fetch(`${API}/dashboard/agents`); const data = await res.json(); const tbody = document.getElementById('agent-body'); if (!data || data.length === 0) { tbody.innerHTML = 'No agent data yet'; return; } tbody.innerHTML = data.map((agent, i) => { const scoreClass = agent.avg_score >= 85 ? 'excellent' : agent.avg_score >= 65 ? 'good' : agent.avg_score >= 40 ? 'fair' : 'poor'; return ` #${i + 1} 🤖 ${escapeHtml(agent.agent_id)} ${agent.total_prompts} ${agent.avg_score} ${agent.improvement_trend} `; }).join(''); } catch (e) { console.error('Failed to load agents:', e); } } // ── Detail Modal ────────────────────────────────────────────── async function viewDetail(id) { // Find the row in current data const row = allInteractions.find(r => r.id === id); if (!row) return; const fullResult = row.full_result_json ? JSON.parse(row.full_result_json) : null; const modal = document.getElementById('detail-modal'); const body = document.getElementById('modal-body'); let scoresHtml = ''; if (fullResult && fullResult.scores) { const dims = ['clarity', 'token_efficiency', 'goal_alignment', 'structure', 'vagueness_index']; scoresHtml = ``; } let mistakesHtml = ''; if (fullResult && fullResult.mistakes && fullResult.mistakes.length > 0) { mistakesHtml = ` `; } body.innerHTML = `
Overall: ${row.overall_score} ${formatTime(row.timestamp)} · ${row.source_agent ? '🤖 ' + row.source_agent : '👤 Human'} ${row.project_id ? ' · 📁 ' + row.project_id : ''}
${scoresHtml} ${row.rewritten_prompt ? ` ` : ''} ${mistakesHtml} `; modal.classList.remove('hidden'); } function closeModal(event) { if (event.target === event.currentTarget) { document.getElementById('detail-modal').classList.add('hidden'); } } function closeDetail() { document.getElementById('detail-modal').classList.add('hidden'); } // ── Helpers ─────────────────────────────────────────────────── function formatTime(ts) { if (!ts) return '—'; try { const d = new Date(ts); return d.toLocaleDateString('en-US', { month: 'short', day: 'numeric' }) + ' ' + d.toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit' }); } catch { return ts; } } function formatMistakeType(type) { return (type || 'unknown').replace(/_/g, ' ').replace(/\b\w/g, c => c.toUpperCase()); } function formatDimension(dim) { return dim.replace(/_/g, ' '); } function getScoreColor(score) { if (score >= 85) return '#10b981'; if (score >= 65) return '#3b82f6'; if (score >= 40) return '#f59e0b'; return '#ef4444'; } function escapeHtml(text) { if (!text) return ''; const div = document.createElement('div'); div.textContent = text; return div.innerHTML; }