// --- Config --- const API_KEY = localStorage.getItem('analyticsApiKey') || ''; const REFRESH_INTERVAL = 60000; let currentPeriod = '24h'; let charts = {}; // --- Cached data for CSV export --- let cachedData = { usageOverTime: null, topUsers: null, topModels: null, topAgents: null, topConversations: null, userDetail: null, }; let currentUserFilter = null; let selectedLookupUser = null; // --- API Helpers --- function apiHeaders() { const h = { 'Content-Type': 'application/json' }; if (API_KEY) h['x-api-key'] = API_KEY; return h; } function queryParams() { const params = new URLSearchParams({ period: currentPeriod }); if (currentPeriod === 'custom') { params.set('start', document.getElementById('startDate').value); params.set('end', document.getElementById('endDate').value); } return params.toString(); } // Detect base path from current URL (works behind nginx proxy) const BASE_PATH = window.location.pathname.replace(/\/$/, ''); async function fetchAPI(endpoint) { const sep = endpoint.includes('?') ? '&' : '?'; const url = `${BASE_PATH}/api/${endpoint}${sep}${queryParams()}&_t=${Date.now()}`; const res = await fetch(url, { headers: apiHeaders(), cache: 'no-store' }); if (!res.ok) throw new Error(`API error: ${res.status}`); return res.json(); } // --- Formatters --- function fmtCost(v) { if (v == null) return '--'; return '$' + v.toFixed(2); } function fmtTokens(v) { if (v == null) return '--'; if (v >= 1_000_000) return (v / 1_000_000).toFixed(1) + 'M'; if (v >= 1_000) return (v / 1_000).toFixed(1) + 'K'; return v.toLocaleString(); } function fmtNum(v) { if (v == null) return '--'; return v.toLocaleString(); } // --- Chart defaults --- Chart.defaults.color = '#94a3b8'; Chart.defaults.borderColor = 'rgba(255,255,255,0.06)'; Chart.defaults.font.family = 'Montserrat, system-ui, sans-serif'; const CHART_COLORS = [ '#FFC407', '#FFD54F', '#FFAB00', '#c084fc', '#f87171', '#a78bfa', '#4ade80', '#fb923c', '#e879f9', '#22d3ee', ]; // --- CSV Export --- function escCSV(val) { const s = String(val == null ? '' : val); if (s.includes(',') || s.includes('"') || s.includes('\n')) { return '"' + s.replace(/"/g, '""') + '"'; } return s; } function downloadCSV(headers, rows, filename) { const lines = [headers.map(escCSV).join(',')]; for (const row of rows) { lines.push(row.map(escCSV).join(',')); } const blob = new Blob([lines.join('\n')], { type: 'text/csv;charset=utf-8;' }); const a = document.createElement('a'); a.href = URL.createObjectURL(blob); a.download = filename; a.click(); URL.revokeObjectURL(a.href); } function exportOverview() { const d = cachedData.usageOverTime; if (!d || !d.data.length) return alert('No data to export'); downloadCSV( ['Time', 'Tokens', 'Cost (USD)'], d.data.map(r => [r.time, r.tokens, r.cost.toFixed(6)]), `usage-over-time-${currentPeriod}.csv` ); } function exportUsers() { const d = cachedData.topUsers; if (!d || !d.length) return alert('No data to export'); downloadCSV( ['Rank', 'Name', 'Email', 'Tokens', 'Cost (USD)', 'Conversations'], d.map((u, i) => [i + 1, u.name, u.email, u.totalTokens, u.totalCost.toFixed(4), u.conversationCount]), `top-users-${currentPeriod}.csv` ); } function exportModels() { const d = cachedData.topModels; if (!d || !d.length) return alert('No data to export'); downloadCSV( ['Rank', 'Model', 'Prompt Tokens', 'Completion Tokens', 'Prompt Cost (USD)', 'Completion Cost (USD)', 'Total Cost (USD)'], d.map((m, i) => [i + 1, m.model, m.promptTokens, m.completionTokens, m.promptCost.toFixed(4), m.completionCost.toFixed(4), m.totalCost.toFixed(4)]), `top-models-${currentPeriod}.csv` ); } function exportAgents() { const d = cachedData.topAgents; if (!d || !d.length) return alert('No data to export'); downloadCSV( ['Rank', 'Agent Name', 'Underlying Model', 'Provider', 'Tokens', 'Cost (USD)', 'Conversations'], d.map((a, i) => [i + 1, a.agentName, a.underlyingModel, a.provider, a.totalTokens, a.totalCost.toFixed(4), a.conversationCount]), `top-agents-${currentPeriod}.csv` ); } function exportConversations() { const d = cachedData.topConversations; if (!d || !d.length) return alert('No data to export'); downloadCSV( ['Rank', 'Title', 'User', 'Email', 'Models', 'Input Tokens', 'Output Tokens', 'Total Cost (USD)'], d.map((c, i) => [i + 1, c.title, c.userName, c.userEmail, c.models.join(', '), c.promptTokens, c.completionTokens, c.totalCost.toFixed(4)]), `top-conversations-${currentPeriod}.csv` ); } // --- Tab Navigation --- document.querySelectorAll('.nav-links li').forEach(li => { li.addEventListener('click', () => { document.querySelectorAll('.nav-links li').forEach(l => l.classList.remove('active')); document.querySelectorAll('.tab-content').forEach(t => t.classList.remove('active')); li.classList.add('active'); document.getElementById('tab-' + li.dataset.tab).classList.add('active'); }); }); // --- Period Selector --- document.querySelectorAll('.period-btn').forEach(btn => { btn.addEventListener('click', () => { document.querySelectorAll('.period-btn').forEach(b => b.classList.remove('active')); btn.classList.add('active'); currentPeriod = btn.dataset.period; document.getElementById('dateRangePicker').style.display = currentPeriod === 'custom' ? 'flex' : 'none'; if (currentPeriod !== 'custom') refreshAll(); }); }); // Custom date range change document.getElementById('startDate').addEventListener('change', refreshAll); document.getElementById('endDate').addEventListener('change', refreshAll); // --- Data Loading --- async function loadSummary() { try { const d = await fetchAPI('summary'); document.getElementById('totalCost').textContent = fmtCost(d.totalCost); document.getElementById('totalTokens').textContent = fmtTokens(d.totalTokens); document.getElementById('activeUsers').textContent = fmtNum(d.activeUsers); document.getElementById('conversations').textContent = fmtNum(d.conversations); document.getElementById('visits').textContent = fmtNum(d.visits); document.getElementById('uniqueUsers').textContent = fmtNum(d.uniqueUsers); } catch (e) { console.error('Summary:', e); } } async function loadUsageOverTime() { try { const d = await fetchAPI('usage-over-time'); cachedData.usageOverTime = d; const labels = d.data.map(p => { if (d.bucketType === 'hour') { const dt = new Date(p.time); return dt.toLocaleString([], { month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit' }); } return new Date(p.time).toLocaleDateString([], { month: 'short', day: 'numeric' }); }); const costData = d.data.map(p => p.cost); const tokenData = d.data.map(p => p.tokens); if (charts.usage) charts.usage.destroy(); charts.usage = new Chart(document.getElementById('usageChart'), { type: 'line', data: { labels, datasets: [ { label: 'Cost ($)', data: costData, borderColor: '#FFC407', backgroundColor: 'rgba(255,196,7,0.1)', fill: true, tension: 0.3, yAxisID: 'y', }, { label: 'Tokens', data: tokenData, borderColor: '#FFD54F', backgroundColor: 'rgba(255,213,79,0.1)', fill: true, tension: 0.3, yAxisID: 'y1', } ] }, options: { responsive: true, interaction: { mode: 'index', intersect: false }, scales: { y: { type: 'linear', position: 'left', title: { display: true, text: 'Cost ($)' } }, y1: { type: 'linear', position: 'right', grid: { drawOnChartArea: false }, title: { display: true, text: 'Tokens' } }, }, plugins: { legend: { position: 'top' } } } }); } catch (e) { console.error('Usage chart:', e); } } async function loadCostByModel() { try { const d = await fetchAPI('cost-breakdown'); const top = d.slice(0, 8); const labels = top.map(m => m.model); const costs = top.map(m => m.totalCost); if (charts.costByModel) charts.costByModel.destroy(); charts.costByModel = new Chart(document.getElementById('costByModelChart'), { type: 'doughnut', data: { labels, datasets: [{ data: costs, backgroundColor: CHART_COLORS.slice(0, top.length), borderWidth: 0, }] }, options: { responsive: true, plugins: { legend: { position: 'right', labels: { padding: 12, usePointStyle: true } }, tooltip: { callbacks: { label: ctx => `${ctx.label}: $${ctx.parsed.toFixed(2)}` } } } } }); } catch (e) { console.error('Cost by model:', e); } } async function loadCostBreakdown() { try { const d = await fetchAPI('cost-breakdown'); const top = d.slice(0, 10); const labels = top.map(m => m.model); if (charts.costBreakdown) charts.costBreakdown.destroy(); charts.costBreakdown = new Chart(document.getElementById('costBreakdownChart'), { type: 'bar', data: { labels, datasets: [ { label: 'Input Cost', data: top.map(m => m.inputCost), backgroundColor: '#FFC407' }, { label: 'Output Cost', data: top.map(m => m.outputCost), backgroundColor: '#FFAB00' }, ] }, options: { responsive: true, scales: { x: { stacked: true, ticks: { maxRotation: 45 } }, y: { stacked: true, title: { display: true, text: 'Cost ($)' } }, }, plugins: { tooltip: { callbacks: { label: ctx => `${ctx.dataset.label}: $${ctx.parsed.y.toFixed(4)}` } } } } }); } catch (e) { console.error('Cost breakdown:', e); } } async function loadTopUsers() { try { const d = await fetchAPI('top-users?limit=20'); cachedData.topUsers = d; const tbody = document.getElementById('usersTableBody'); tbody.innerHTML = d.map((u, i) => `