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 @@
-
+
@@ -126,7 +130,7 @@
-
+
@@ -153,7 +157,7 @@
-
+
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 |
';