diff --git a/public/css/style.css b/public/css/style.css index c403071..aa02bd9 100644 --- a/public/css/style.css +++ b/public/css/style.css @@ -329,6 +329,9 @@ body { .table-header { padding: 1.5rem 2rem; border-bottom: 1px solid var(--border-color); + display: flex; + justify-content: space-between; + align-items: center; } .table-header h3 { @@ -376,6 +379,35 @@ tr:hover td { font-weight: 500; } +/* Export Button */ +.btn-export { + display: inline-flex; + align-items: center; + gap: 0.5rem; + padding: 0.5rem 1rem; + background: rgba(255, 196, 7, 0.1); + color: var(--accent-primary); + border: 1px solid rgba(255, 196, 7, 0.2); + border-radius: 0.5rem; + font-family: "Montserrat", system-ui, sans-serif; + font-size: 0.8125rem; + font-weight: 600; + cursor: pointer; + transition: all 0.2s; +} + +.btn-export:hover { + background: rgba(255, 196, 7, 0.2); + border-color: rgba(255, 196, 7, 0.4); +} + +.tab-title-row { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 1.5rem; +} + /* Loading */ .loading { display: flex; diff --git a/public/index.html b/public/index.html index e1c55c2..758d2d0 100644 --- a/public/index.html +++ b/public/index.html @@ -69,6 +69,10 @@
+
+
+ +
@@ -106,7 +110,7 @@
-

Top Users by Cost

+

Top Users by Cost

@@ -126,7 +130,7 @@
-

Top Models by Cost

+

Top Models by Cost

@@ -153,7 +157,7 @@
-

Top Agents by Cost

+

Top Agents by Cost

diff --git a/public/js/app.js b/public/js/app.js index 56c5602..4813b9d 100644 --- a/public/js/app.js +++ b/public/js/app.js @@ -5,6 +5,14 @@ const REFRESH_INTERVAL = 60000; let currentPeriod = '24h'; let charts = {}; +// --- Cached data for CSV export --- +let cachedData = { + usageOverTime: null, + topUsers: null, + topModels: null, + topAgents: null, +}; + // --- API Helpers --- function apiHeaders() { const h = { 'Content-Type': 'application/json' }; @@ -59,6 +67,68 @@ const CHART_COLORS = [ '#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` + ); +} + // --- Tab Navigation --- document.querySelectorAll('.nav-links li').forEach(li => { li.addEventListener('click', () => { @@ -99,6 +169,7 @@ async function loadSummary() { 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); @@ -210,6 +281,7 @@ async function loadCostBreakdown() { 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) => ` @@ -227,6 +299,7 @@ async function loadTopUsers() { async function loadTopModels() { try { const d = await fetchAPI('top-models?limit=20'); + cachedData.topModels = d; const tbody = document.getElementById('modelsTableBody'); tbody.innerHTML = d.map((m, i) => ` @@ -270,6 +343,7 @@ async function loadTopModels() { async function loadTopAgents() { try { const d = await fetchAPI('top-agents?limit=20'); + cachedData.topAgents = d; const tbody = document.getElementById('agentsTableBody'); if (d.length === 0) { tbody.innerHTML = '';
No agent usage found in this period