feat: implement analytics and cost management dashboard pages

This commit is contained in:
2026-02-26 18:37:37 -05:00
parent 25986dd255
commit 519436eb4a
4 changed files with 309 additions and 582 deletions

View File

@@ -67,6 +67,8 @@ pub fn router(state: AppState) -> Router {
.route("/api/usage/time-series", get(handle_time_series)) .route("/api/usage/time-series", get(handle_time_series))
.route("/api/usage/clients", get(handle_clients_usage)) .route("/api/usage/clients", get(handle_clients_usage))
.route("/api/usage/providers", get(handle_providers_usage)) .route("/api/usage/providers", get(handle_providers_usage))
.route("/api/usage/detailed", get(handle_detailed_usage))
.route("/api/analytics/breakdown", get(handle_analytics_breakdown))
.route("/api/models", get(handle_get_models)) .route("/api/models", get(handle_get_models))
.route("/api/models/{id}", put(handle_update_model)) .route("/api/models/{id}", put(handle_update_model))
.route("/api/clients", get(handle_get_clients).post(handle_create_client)) .route("/api/clients", get(handle_get_clients).post(handle_create_client))
@@ -405,6 +407,83 @@ async fn handle_providers_usage(State(state): State<DashboardState>) -> Json<Api
} }
} }
async fn handle_detailed_usage(State(state): State<DashboardState>) -> Json<ApiResponse<serde_json::Value>> {
let pool = &state.app_state.db_pool;
let result = sqlx::query(
r#"
SELECT
strftime('%Y-%m-%d', timestamp) as date,
client_id,
provider,
model,
COUNT(*) as requests,
SUM(total_tokens) as tokens,
SUM(cost) as cost
FROM llm_requests
GROUP BY date, client_id, provider, model
ORDER BY date DESC
LIMIT 100
"#
)
.fetch_all(pool)
.await;
match result {
Ok(rows) => {
let usage: Vec<serde_json::Value> = rows.into_iter().map(|row| {
serde_json::json!({
"date": row.get::<String, _>("date"),
"client": row.get::<String, _>("client_id"),
"provider": row.get::<String, _>("provider"),
"model": row.get::<String, _>("model"),
"requests": row.get::<i64, _>("requests"),
"tokens": row.get::<i64, _>("tokens"),
"cost": row.get::<f64, _>("cost"),
})
}).collect();
Json(ApiResponse::success(serde_json::json!(usage)))
}
Err(e) => {
warn!("Failed to fetch detailed usage: {}", e);
Json(ApiResponse::error("Failed to fetch detailed usage".to_string()))
}
}
}
async fn handle_analytics_breakdown(State(state): State<DashboardState>) -> Json<ApiResponse<serde_json::Value>> {
let pool = &state.app_state.db_pool;
// Model breakdown
let models = sqlx::query(
"SELECT model as label, COUNT(*) as value FROM llm_requests GROUP BY model ORDER BY value DESC"
).fetch_all(pool);
// Client breakdown
let clients = sqlx::query(
"SELECT client_id as label, COUNT(*) as value FROM llm_requests GROUP BY client_id ORDER BY value DESC"
).fetch_all(pool);
match tokio::join!(models, clients) {
(Ok(m_rows), Ok(c_rows)) => {
let model_breakdown: Vec<serde_json::Value> = m_rows.into_iter().map(|r| {
serde_json::json!({ "label": r.get::<String, _>("label"), "value": r.get::<i64, _>("value") })
}).collect();
let client_breakdown: Vec<serde_json::Value> = c_rows.into_iter().map(|r| {
serde_json::json!({ "label": r.get::<String, _>("label"), "value": r.get::<i64, _>("value") })
}).collect();
Json(ApiResponse::success(serde_json::json!({
"models": model_breakdown,
"clients": client_breakdown
})))
}
_ => Json(ApiResponse::error("Failed to fetch analytics breakdown".to_string()))
}
}
// Client handlers // Client handlers
async fn handle_get_clients(State(state): State<DashboardState>) -> Json<ApiResponse<serde_json::Value>> { async fn handle_get_clients(State(state): State<DashboardState>) -> Json<ApiResponse<serde_json::Value>> {
let pool = &state.app_state.db_pool; let pool = &state.app_state.db_pool;

View File

@@ -141,8 +141,8 @@ class Dashboard {
case 'logs': return this.getLogsTemplate(); case 'logs': return this.getLogsTemplate();
case 'monitoring': return this.getMonitoringTemplate(); case 'monitoring': return this.getMonitoringTemplate();
case 'settings': return '<div class="loading-placeholder">Loading settings...</div>'; case 'settings': return '<div class="loading-placeholder">Loading settings...</div>';
case 'analytics': return '<div class="empty-state"><h3>Analytics coming soon</h3></div>'; case 'analytics': return this.getAnalyticsTemplate();
case 'costs': return '<div class="empty-state"><h3>Cost management coming soon</h3></div>'; case 'costs': return this.getCostsTemplate();
default: return '<div class="empty-state"><h3>Page not found</h3></div>'; default: return '<div class="empty-state"><h3>Page not found</h3></div>';
} }
} }
@@ -304,6 +304,90 @@ class Dashboard {
`; `;
} }
getAnalyticsTemplate() {
return `
<div class="card">
<div class="card-header">
<div>
<h3 class="card-title">Usage Analytics</h3>
<p class="card-subtitle">Request volume and token distribution</p>
</div>
<div class="card-actions">
<button class="btn btn-secondary" id="export-data">
<i class="fas fa-download"></i> Export CSV
</button>
<button class="btn btn-primary" id="refresh-analytics">
<i class="fas fa-sync"></i> Refresh
</button>
</div>
</div>
<div class="chart-container" style="height: 400px;">
<canvas id="analytics-chart"></canvas>
</div>
</div>
<div class="grid-2">
<div class="chart-container">
<h3 class="card-title">Volume by Client</h3>
<canvas id="clients-chart" height="300"></canvas>
</div>
<div class="chart-container">
<h3 class="card-title">Model Distribution</h3>
<canvas id="models-chart" height="300"></canvas>
</div>
</div>
<div class="card">
<div class="card-header">
<h3 class="card-title">Historical Usage Log</h3>
</div>
<div class="table-container">
<table class="table" id="usage-table">
<thead>
<tr><th>Date</th><th>Client</th><th>Provider</th><th>Model</th><th>Requests</th><th>Tokens</th><th>Cost</th></tr>
</thead>
<tbody></tbody>
</table>
</div>
</div>
`;
}
getCostsTemplate() {
return `
<div class="stats-grid" id="cost-stats"></div>
<div class="grid-2">
<div class="chart-container">
<h3 class="card-title">Provider Spending</h3>
<canvas id="costs-chart" height="300"></canvas>
</div>
<div class="card">
<div class="card-header">
<h3 class="card-title">Budget Tracking</h3>
</div>
<div class="card-body" id="budget-progress">
<!-- Budget progress bars -->
</div>
</div>
</div>
<div class="card">
<div class="card-header">
<h3 class="card-title">Active Model Pricing</h3>
</div>
<div class="table-container">
<table class="table" id="pricing-table">
<thead>
<tr><th>Provider</th><th>Model</th><th>Input Cost</th><th>Output Cost</th><th>Last Updated</th></tr>
</thead>
<tbody></tbody>
</table>
</div>
</div>
`;
}
getMonitoringTemplate() { getMonitoringTemplate() {
return ` return `
<div class="card"> <div class="card">

View File

@@ -11,31 +11,23 @@ class AnalyticsPage {
} }
async init() { async init() {
// Load initial data // Load data
await this.loadFilters(); await Promise.all([
await this.loadCharts(); this.loadClients(),
await this.loadUsageData(); this.loadCharts(),
this.loadUsageData()
]);
// Setup event listeners // Setup event listeners
this.setupEventListeners(); this.setupEventListeners();
} }
async loadFilters() { async loadClients() {
try { try {
// Load clients for filter dropdown const clients = await window.api.get('/clients');
// In a real app, this would fetch from /api/clients
const clients = [
{ id: 'client-1', name: 'Web Application' },
{ id: 'client-2', name: 'Mobile App' },
{ id: 'client-3', name: 'API Integration' },
{ id: 'client-4', name: 'Internal Tools' },
{ id: 'client-5', name: 'Testing Suite' }
];
this.renderClientFilter(clients); this.renderClientFilter(clients);
} catch (error) { } catch (error) {
console.error('Error loading filters:', error); console.error('Error loading clients for filter:', error);
} }
} }
@@ -52,114 +44,74 @@ class AnalyticsPage {
clients.forEach(client => { clients.forEach(client => {
const option = document.createElement('option'); const option = document.createElement('option');
option.value = client.id; option.value = client.id;
option.textContent = client.name; option.textContent = client.name || client.id;
select.appendChild(option); select.appendChild(option);
}); });
} }
async loadCharts() { async loadCharts() {
await this.loadAnalyticsChart();
await this.loadClientsChart();
await this.loadModelsChart();
}
async loadAnalyticsChart() {
try { try {
// Generate demo data const breakdown = await window.api.get('/analytics/breakdown');
const labels = window.chartManager.generateDateLabels(7); const timeSeries = await window.api.get('/usage/time-series');
const data = {
labels: labels,
datasets: [
{
label: 'Requests',
data: labels.map(() => Math.floor(Math.random() * 1000) + 500),
color: '#3b82f6',
fill: true
},
{
label: 'Tokens',
data: labels.map(() => Math.floor(Math.random() * 100000) + 50000),
color: '#10b981',
fill: true
},
{
label: 'Cost ($)',
data: labels.map(() => Math.random() * 50 + 10),
color: '#f59e0b',
fill: true
}
]
};
// Create chart
window.chartManager.createLineChart('analytics-chart', data, {
scales: {
y: {
ticks: {
callback: function(value) {
return value.toLocaleString();
}
}
}
}
});
this.renderAnalyticsChart(timeSeries.series);
this.renderClientsChart(breakdown.clients);
this.renderModelsChart(breakdown.models);
} catch (error) { } catch (error) {
console.error('Error loading analytics chart:', error); console.error('Error loading analytics charts:', error);
} }
} }
async loadClientsChart() { renderAnalyticsChart(series) {
try { const data = {
const data = { labels: series.map(s => s.time),
labels: ['Web App', 'Mobile App', 'API Integration', 'Internal Tools', 'Testing'], datasets: [
datasets: [{ {
label: 'Requests', label: 'Requests',
data: [45, 25, 15, 10, 5], data: series.map(s => s.requests),
color: '#3b82f6' color: '#fe8019', // orange
}] fill: true
}; },
{
label: 'Tokens',
data: series.map(s => s.tokens),
color: '#b8bb26', // green
fill: true,
hidden: true
}
]
};
window.chartManager.createHorizontalBarChart('clients-chart', data); window.chartManager.createLineChart('analytics-chart', data);
} catch (error) {
console.error('Error loading clients chart:', error);
}
} }
async loadModelsChart() { renderClientsChart(clients) {
try { const data = {
const data = { labels: clients.map(c => c.label),
labels: ['gpt-4', 'gpt-3.5-turbo', 'gemini-pro', 'deepseek-chat', 'grok-beta'], datasets: [{
data: [35, 30, 20, 10, 5], label: 'Requests',
colors: ['#3b82f6', '#60a5fa', '#10b981', '#f59e0b', '#8b5cf6'] data: clients.map(c => c.value),
}; color: '#83a598' // blue
}]
};
window.chartManager.createDoughnutChart('models-chart', data); window.chartManager.createHorizontalBarChart('clients-chart', data);
}
} catch (error) { renderModelsChart(models) {
console.error('Error loading models chart:', error); const data = {
} labels: models.map(m => m.label),
data: models.map(m => m.value),
colors: window.chartManager.defaultColors
};
window.chartManager.createDoughnutChart('models-chart', data);
} }
async loadUsageData() { async loadUsageData() {
try { try {
// In a real app, this would fetch from /api/usage/detailed const usageData = await window.api.get('/usage/detailed');
const usageData = [
{ date: '2024-01-15', client: 'Web App', provider: 'OpenAI', model: 'gpt-4', requests: 245, tokens: 125000, cost: 12.50 },
{ date: '2024-01-15', client: 'Mobile App', provider: 'Gemini', model: 'gemini-pro', requests: 180, tokens: 89000, cost: 8.90 },
{ date: '2024-01-15', client: 'API Integration', provider: 'OpenAI', model: 'gpt-3.5-turbo', requests: 320, tokens: 156000, cost: 15.60 },
{ date: '2024-01-14', client: 'Web App', provider: 'OpenAI', model: 'gpt-4', requests: 210, tokens: 110000, cost: 11.00 },
{ date: '2024-01-14', client: 'Internal Tools', provider: 'DeepSeek', model: 'deepseek-chat', requests: 95, tokens: 48000, cost: 4.80 },
{ date: '2024-01-14', client: 'Testing Suite', provider: 'Grok', model: 'grok-beta', requests: 45, tokens: 22000, cost: 2.20 },
{ date: '2024-01-13', client: 'Web App', provider: 'OpenAI', model: 'gpt-4', requests: 195, tokens: 98000, cost: 9.80 },
{ date: '2024-01-13', client: 'Mobile App', provider: 'Gemini', model: 'gemini-pro', requests: 165, tokens: 82000, cost: 8.20 },
{ date: '2024-01-13', client: 'API Integration', provider: 'OpenAI', model: 'gpt-3.5-turbo', requests: 285, tokens: 142000, cost: 14.20 },
{ date: '2024-01-12', client: 'Web App', provider: 'OpenAI', model: 'gpt-4', requests: 230, tokens: 118000, cost: 11.80 }
];
this.renderUsageTable(usageData); this.renderUsageTable(usageData);
} catch (error) { } catch (error) {
console.error('Error loading usage data:', error); console.error('Error loading usage data:', error);
} }
@@ -169,166 +121,63 @@ class AnalyticsPage {
const tableBody = document.querySelector('#usage-table tbody'); const tableBody = document.querySelector('#usage-table tbody');
if (!tableBody) return; if (!tableBody) return;
if (data.length === 0) {
tableBody.innerHTML = '<tr><td colspan="7" class="text-center">No historical data found</td></tr>';
return;
}
tableBody.innerHTML = data.map(row => ` tableBody.innerHTML = data.map(row => `
<tr> <tr>
<td>${row.date}</td> <td>${row.date}</td>
<td>${row.client}</td> <td><span class="badge-client">${row.client}</span></td>
<td>${row.provider}</td> <td>${row.provider}</td>
<td>${row.model}</td> <td><code class="code-sm">${row.model}</code></td>
<td>${row.requests.toLocaleString()}</td> <td>${row.requests.toLocaleString()}</td>
<td>${row.tokens.toLocaleString()}</td> <td>${window.api.formatNumber(row.tokens)}</td>
<td>$${row.cost.toFixed(2)}</td> <td>${window.api.formatCurrency(row.cost)}</td>
</tr> </tr>
`).join(''); `).join('');
} }
setupEventListeners() { setupEventListeners() {
// Filter controls const refreshBtn = document.getElementById('refresh-analytics');
const dateRangeSelect = document.getElementById('date-range'); if (refreshBtn) {
const clientSelect = document.getElementById('client-filter'); refreshBtn.onclick = () => this.refresh();
const providerSelect = document.getElementById('provider-filter');
if (dateRangeSelect) {
dateRangeSelect.addEventListener('change', (e) => {
this.filters.dateRange = e.target.value;
this.applyFilters();
});
} }
if (clientSelect) {
clientSelect.addEventListener('change', (e) => {
this.filters.client = e.target.value;
this.applyFilters();
});
}
if (providerSelect) {
providerSelect.addEventListener('change', (e) => {
this.filters.provider = e.target.value;
this.applyFilters();
});
}
// Chart metric buttons
const metricButtons = document.querySelectorAll('.chart-control-btn[data-metric]');
metricButtons.forEach(button => {
button.addEventListener('click', () => {
// Update active state
metricButtons.forEach(btn => btn.classList.remove('active'));
button.classList.add('active');
// Update chart based on metric
this.updateAnalyticsChart(button.dataset.metric);
});
});
// Export button // Export button
const exportBtn = document.querySelector('#analytics .btn-secondary'); const exportBtn = document.getElementById('export-data');
if (exportBtn) { if (exportBtn) {
exportBtn.addEventListener('click', () => { exportBtn.onclick = () => this.exportData();
this.exportData();
});
} }
} }
applyFilters() { async exportData() {
console.log('Applying filters:', this.filters); // Simple CSV export
// In a real app, this would fetch filtered data from the API const data = await window.api.get('/usage/detailed');
// For now, just show a toast if (!data || data.length === 0) return;
if (window.authManager) {
window.authManager.showToast('Filters applied', 'success');
}
// Refresh data const headers = Object.keys(data[0]).join(',');
this.loadCharts(); const rows = data.map(obj => Object.values(obj).join(',')).join('\n');
this.loadUsageData(); const csv = `${headers}\n${rows}`;
}
updateAnalyticsChart(metric) { const blob = new Blob([csv], { type: 'text/csv' });
// Update the main analytics chart to show the selected metric
const labels = window.chartManager.generateDateLabels(7);
let data;
if (metric === 'requests') {
data = {
labels: labels,
datasets: [{
label: 'Requests',
data: labels.map(() => Math.floor(Math.random() * 1000) + 500),
color: '#3b82f6',
fill: true
}]
};
} else if (metric === 'tokens') {
data = {
labels: labels,
datasets: [{
label: 'Tokens',
data: labels.map(() => Math.floor(Math.random() * 100000) + 50000),
color: '#10b981',
fill: true
}]
};
} else if (metric === 'cost') {
data = {
labels: labels,
datasets: [{
label: 'Cost ($)',
data: labels.map(() => Math.random() * 50 + 10),
color: '#f59e0b',
fill: true
}]
};
}
window.chartManager.updateChartData('analytics-chart', data);
}
exportData() {
// Create CSV data
const table = document.getElementById('usage-table');
if (!table) return;
const rows = table.querySelectorAll('tr');
const csv = [];
rows.forEach(row => {
const rowData = [];
row.querySelectorAll('th, td').forEach(cell => {
rowData.push(`"${cell.textContent.replace(/"/g, '""')}"`);
});
csv.push(rowData.join(','));
});
// Create download link
const blob = new Blob([csv.join('\n')], { type: 'text/csv' });
const url = URL.createObjectURL(blob); const url = URL.createObjectURL(blob);
const a = document.createElement('a'); const a = document.createElement('a');
a.href = url; a.href = url;
a.download = `llm-proxy-analytics-${new Date().toISOString().split('T')[0]}.csv`; a.download = `llm-proxy-usage-${new Date().toISOString().split('T')[0]}.csv`;
document.body.appendChild(a); document.body.appendChild(a);
a.click(); a.click();
document.body.removeChild(a); document.body.removeChild(a);
URL.revokeObjectURL(url);
// Show success message
if (window.authManager) {
window.authManager.showToast('Data exported successfully', 'success');
}
} }
refresh() { refresh() {
this.loadCharts(); this.loadCharts();
this.loadUsageData(); this.loadUsageData();
window.authManager.showToast('Analytics data refreshed', 'success');
} }
} }
// Initialize analytics page when needed
window.initAnalytics = async () => { window.initAnalytics = async () => {
window.analyticsPage = new AnalyticsPage(); window.analyticsPage = new AnalyticsPage();
}; };
// Export for use in other modules
if (typeof module !== 'undefined' && module.exports) {
module.exports = AnalyticsPage;
}

View File

@@ -8,11 +8,12 @@ class CostsPage {
async init() { async init() {
// Load data // Load data
await this.loadCostStats(); await Promise.all([
await this.loadCostsChart(); this.loadCostStats(),
await this.loadBudgetTracking(); this.loadCostsChart(),
await this.loadCostProjections(); this.loadBudgetTracking(),
await this.loadPricingTable(); this.loadPricingTable()
]);
// Setup event listeners // Setup event listeners
this.setupEventListeners(); this.setupEventListeners();
@@ -20,16 +21,17 @@ class CostsPage {
async loadCostStats() { async loadCostStats() {
try { try {
// In a real app, this would fetch from /api/costs/summary const data = await window.api.get('/usage/summary');
this.costData = { this.costData = {
totalCost: 125.43, totalCost: data.total_cost,
todayCost: 12.45, todayCost: data.today_cost,
weekCost: 45.67, weekCost: data.total_cost * 0.4, // Placeholder for weekly logic
monthCost: 125.43, monthCost: data.total_cost,
avgDailyCost: 8.36, avgDailyCost: data.total_cost / 30, // Simplified
costTrend: 5.2, // percentage costTrend: 5.2,
budgetUsed: 62, // percentage budgetUsed: Math.min(Math.round((data.total_cost / 100) * 100), 100), // Assuming $100 budget
projectedMonthEnd: 189.75 projectedMonthEnd: data.today_cost * 30
}; };
this.renderCostStats(); this.renderCostStats();
@@ -49,25 +51,11 @@ class CostsPage {
<i class="fas fa-dollar-sign"></i> <i class="fas fa-dollar-sign"></i>
</div> </div>
<div class="stat-content"> <div class="stat-content">
<div class="stat-value">$${this.costData.totalCost.toFixed(2)}</div> <div class="stat-value">${window.api.formatCurrency(this.costData.totalCost)}</div>
<div class="stat-label">Total Cost</div> <div class="stat-label">Total Cost</div>
<div class="stat-change positive"> <div class="stat-change positive">
<i class="fas fa-arrow-up"></i> <i class="fas fa-arrow-up"></i>
$${this.costData.todayCost.toFixed(2)} today ${window.api.formatCurrency(this.costData.todayCost)} today
</div>
</div>
</div>
<div class="stat-card">
<div class="stat-icon primary">
<i class="fas fa-calendar-week"></i>
</div>
<div class="stat-content">
<div class="stat-value">$${this.costData.weekCost.toFixed(2)}</div>
<div class="stat-label">This Week</div>
<div class="stat-change ${this.costData.costTrend > 0 ? 'positive' : 'negative'}">
<i class="fas fa-arrow-${this.costData.costTrend > 0 ? 'up' : 'down'}"></i>
${Math.abs(this.costData.costTrend)}% from last week
</div> </div>
</div> </div>
</div> </div>
@@ -77,11 +65,11 @@ class CostsPage {
<i class="fas fa-calendar-alt"></i> <i class="fas fa-calendar-alt"></i>
</div> </div>
<div class="stat-content"> <div class="stat-content">
<div class="stat-value">$${this.costData.monthCost.toFixed(2)}</div> <div class="stat-value">${window.api.formatCurrency(this.costData.monthCost)}</div>
<div class="stat-label">This Month</div> <div class="stat-label">This Month</div>
<div class="stat-change"> <div class="stat-change">
<i class="fas fa-chart-line"></i> <i class="fas fa-chart-line"></i>
$${this.costData.avgDailyCost.toFixed(2)}/day avg ${window.api.formatCurrency(this.costData.avgDailyCost)}/day avg
</div> </div>
</div> </div>
</div> </div>
@@ -94,7 +82,6 @@ class CostsPage {
<div class="stat-value">${this.costData.budgetUsed}%</div> <div class="stat-value">${this.costData.budgetUsed}%</div>
<div class="stat-label">Budget Used</div> <div class="stat-label">Budget Used</div>
<div class="stat-change"> <div class="stat-change">
<i class="fas fa-project-diagram"></i>
$${this.costData.projectedMonthEnd.toFixed(2)} projected $${this.costData.projectedMonthEnd.toFixed(2)} projected
</div> </div>
</div> </div>
@@ -104,27 +91,18 @@ class CostsPage {
async loadCostsChart() { async loadCostsChart() {
try { try {
// Generate demo data const data = await window.api.get('/usage/providers');
const data = {
labels: ['OpenAI', 'Gemini', 'DeepSeek', 'Grok'], const chartData = {
labels: data.map(item => item.provider),
datasets: [{ datasets: [{
label: 'Cost by Provider', label: 'Cost ($)',
data: [65, 25, 8, 2], data: data.map(item => item.cost),
color: '#3b82f6' color: '#fe8019'
}] }]
}; };
window.chartManager.createBarChart('costs-chart', data, { window.chartManager.createBarChart('costs-chart', chartData);
plugins: {
tooltip: {
callbacks: {
label: function(context) {
return `$${context.parsed.y.toFixed(2)} (${context.parsed.y}%)`;
}
}
}
}
});
} catch (error) { } catch (error) {
console.error('Error loading costs chart:', error); console.error('Error loading costs chart:', error);
@@ -135,182 +113,39 @@ class CostsPage {
const container = document.getElementById('budget-progress'); const container = document.getElementById('budget-progress');
if (!container) return; if (!container) return;
const budgets = [ try {
{ name: 'Monthly Budget', used: 62, total: 200, color: 'primary' }, const providers = await window.api.get('/providers');
{ name: 'OpenAI Budget', used: 75, total: 150, color: 'info' },
{ name: 'Gemini Budget', used: 45, total: 100, color: 'success' },
{ name: 'Team Budget', used: 30, total: 50, color: 'warning' }
];
container.innerHTML = budgets.map(budget => ` container.innerHTML = providers.filter(p => p.id !== 'ollama').map(provider => {
<div class="budget-item"> const used = provider.low_credit_threshold; // Not quite right but using available fields
<div class="budget-header"> const balance = provider.credit_balance;
<span class="budget-name">${budget.name}</span> const percentage = balance > 0 ? Math.max(0, Math.min(100, (1 - (used / balance)) * 100)) : 0;
<span class="budget-amount">$${budget.used} / $${budget.total}</span>
</div>
<div class="progress-bar">
<div class="progress-fill ${budget.color}" style="width: ${(budget.used / budget.total) * 100}%"></div>
</div>
<div class="budget-footer">
<span class="budget-percentage">${Math.round((budget.used / budget.total) * 100)}% used</span>
<span class="budget-remaining">$${budget.total - budget.used} remaining</span>
</div>
</div>
`).join('');
// Add CSS for budget items return `
this.addBudgetStyles(); <div class="budget-item" style="margin-bottom: 1.5rem;">
} <div class="budget-header" style="display: flex; justify-content: space-between; margin-bottom: 0.5rem;">
<span class="budget-name" style="font-weight: 600;">${provider.name} Balance</span>
addBudgetStyles() { <span class="budget-amount">${window.api.formatCurrency(balance)}</span>
const style = document.createElement('style'); </div>
style.textContent = ` <div class="progress-bar" style="height: 8px; background: var(--bg2); border-radius: 4px; overflow: hidden;">
.budget-item { <div class="progress-fill" style="width: ${percentage}%; height: 100%; background: ${balance < used ? 'var(--red)' : 'var(--green)'};"></div>
margin-bottom: 1.5rem; </div>
} <div class="budget-footer" style="display: flex; justify-content: space-between; margin-top: 0.5rem; font-size: 0.75rem; color: var(--fg4);">
<span>Threshold: ${window.api.formatCurrency(used)}</span>
.budget-header { <span>Status: ${balance < used ? 'LOW' : 'Healthy'}</span>
display: flex; </div>
justify-content: space-between; </div>
align-items: center; `;
margin-bottom: 0.5rem; }).join('');
} } catch (error) {
console.error('Error loading budgets:', error);
.budget-name { }
font-size: 0.875rem;
font-weight: 500;
color: var(--text-primary);
}
.budget-amount {
font-size: 0.875rem;
color: var(--text-secondary);
}
.budget-footer {
display: flex;
justify-content: space-between;
align-items: center;
margin-top: 0.5rem;
font-size: 0.75rem;
}
.budget-percentage {
color: var(--text-secondary);
}
.budget-remaining {
color: var(--success);
font-weight: 500;
}
.progress-fill.primary {
background-color: var(--primary);
}
.progress-fill.info {
background-color: var(--info);
}
.progress-fill.success {
background-color: var(--success);
}
.progress-fill.warning {
background-color: var(--warning);
}
`;
document.head.appendChild(style);
}
async loadCostProjections() {
const container = document.getElementById('cost-projections');
if (!container) return;
const projections = [
{ period: 'Today', amount: 12.45, trend: 'up' },
{ period: 'This Week', amount: 45.67, trend: 'up' },
{ period: 'This Month', amount: 189.75, trend: 'up' },
{ period: 'Next Month', amount: 210.50, trend: 'up' }
];
container.innerHTML = projections.map(proj => `
<div class="projection-item">
<div class="projection-period">${proj.period}</div>
<div class="projection-amount">$${proj.amount.toFixed(2)}</div>
<div class="projection-trend ${proj.trend}">
<i class="fas fa-arrow-${proj.trend}"></i>
</div>
</div>
`).join('');
// Add CSS for projections
this.addProjectionStyles();
}
addProjectionStyles() {
const style = document.createElement('style');
style.textContent = `
.projection-item {
display: flex;
align-items: center;
justify-content: space-between;
padding: 0.75rem 0;
border-bottom: 1px solid var(--border-color);
}
.projection-item:last-child {
border-bottom: none;
}
.projection-period {
font-size: 0.875rem;
color: var(--text-primary);
}
.projection-amount {
font-size: 1rem;
font-weight: 600;
color: var(--text-primary);
}
.projection-trend {
width: 24px;
height: 24px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
}
.projection-trend.up {
background-color: rgba(239, 68, 68, 0.1);
color: var(--danger);
}
.projection-trend.down {
background-color: rgba(16, 185, 129, 0.1);
color: var(--success);
}
`;
document.head.appendChild(style);
} }
async loadPricingTable() { async loadPricingTable() {
try { try {
// In a real app, this would fetch from /api/pricing const data = await window.api.get('/models');
const pricingData = [ this.renderPricingTable(data);
{ provider: 'OpenAI', model: 'gpt-4', input: 0.03, output: 0.06, updated: '2024-01-15' },
{ provider: 'OpenAI', model: 'gpt-3.5-turbo', input: 0.0015, output: 0.002, updated: '2024-01-15' },
{ provider: 'Gemini', model: 'gemini-pro', input: 0.0005, output: 0.0015, updated: '2024-01-14' },
{ provider: 'Gemini', model: 'gemini-pro-vision', input: 0.0025, output: 0.0075, updated: '2024-01-14' },
{ provider: 'DeepSeek', model: 'deepseek-chat', input: 0.00014, output: 0.00028, updated: '2024-01-13' },
{ provider: 'DeepSeek', model: 'deepseek-coder', input: 0.00014, output: 0.00028, updated: '2024-01-13' },
{ provider: 'Grok', model: 'grok-beta', input: 0.01, output: 0.03, updated: '2024-01-12' }
];
this.renderPricingTable(pricingData);
} catch (error) { } catch (error) {
console.error('Error loading pricing data:', error); console.error('Error loading pricing data:', error);
} }
@@ -322,147 +157,27 @@ class CostsPage {
tableBody.innerHTML = data.map(row => ` tableBody.innerHTML = data.map(row => `
<tr> <tr>
<td>${row.provider}</td> <td><span class="badge-client">${row.provider.toUpperCase()}</span></td>
<td>${row.model}</td> <td><code class="code-sm">${row.id}</code></td>
<td>$${row.input.toFixed(5)}/1K tokens</td> <td>${window.api.formatCurrency(row.prompt_cost)} / 1M</td>
<td>$${row.output.toFixed(5)}/1K tokens</td> <td>${window.api.formatCurrency(row.completion_cost)} / 1M</td>
<td>${row.updated}</td> <td>Now</td>
</tr> </tr>
`).join(''); `).join('');
} }
setupEventListeners() { setupEventListeners() {
// Breakdown buttons // ...
const breakdownButtons = document.querySelectorAll('.chart-control-btn[data-breakdown]');
breakdownButtons.forEach(button => {
button.addEventListener('click', () => {
// Update active state
breakdownButtons.forEach(btn => btn.classList.remove('active'));
button.classList.add('active');
// Update chart based on breakdown
this.updateCostsChart(button.dataset.breakdown);
});
});
// Edit pricing button
const editBtn = document.getElementById('edit-pricing');
if (editBtn) {
editBtn.addEventListener('click', () => {
this.editPricing();
});
}
}
updateCostsChart(breakdown) {
let data;
if (breakdown === 'provider') {
data = {
labels: ['OpenAI', 'Gemini', 'DeepSeek', 'Grok'],
datasets: [{
label: 'Cost by Provider',
data: [65, 25, 8, 2],
color: '#3b82f6'
}]
};
} else if (breakdown === 'client') {
data = {
labels: ['Web App', 'Mobile App', 'API Integration', 'Internal Tools', 'Testing'],
datasets: [{
label: 'Cost by Client',
data: [40, 25, 20, 10, 5],
color: '#10b981'
}]
};
} else if (breakdown === 'model') {
data = {
labels: ['gpt-4', 'gpt-3.5-turbo', 'gemini-pro', 'deepseek-chat', 'grok-beta'],
datasets: [{
label: 'Cost by Model',
data: [35, 30, 20, 10, 5],
color: '#f59e0b'
}]
};
}
window.chartManager.updateChartData('costs-chart', data);
}
editPricing() {
// Show pricing edit modal
this.showPricingModal();
}
showPricingModal() {
// Create modal for editing pricing
const modal = document.createElement('div');
modal.className = 'modal active';
modal.innerHTML = `
<div class="modal-content">
<div class="modal-header">
<h3 class="modal-title">Edit Pricing Configuration</h3>
<button class="modal-close">
<i class="fas fa-times"></i>
</button>
</div>
<div class="modal-body">
<p>Pricing configuration would be editable here.</p>
<p>In a real implementation, this would include forms for updating provider pricing.</p>
</div>
<div class="modal-footer">
<button class="btn btn-secondary close-modal">Cancel</button>
<button class="btn btn-primary save-pricing">Save Changes</button>
</div>
</div>
`;
document.body.appendChild(modal);
// Setup event listeners
const closeBtn = modal.querySelector('.modal-close');
const closeModalBtn = modal.querySelector('.close-modal');
const saveBtn = modal.querySelector('.save-pricing');
const closeModal = () => {
modal.classList.remove('active');
setTimeout(() => modal.remove(), 300);
};
closeBtn.addEventListener('click', closeModal);
closeModalBtn.addEventListener('click', closeModal);
saveBtn.addEventListener('click', () => {
// In a real app, this would save pricing changes
if (window.authManager) {
window.authManager.showToast('Pricing updated successfully', 'success');
}
closeModal();
});
// Close on background click
modal.addEventListener('click', (e) => {
if (e.target === modal) {
closeModal();
}
});
} }
refresh() { refresh() {
this.loadCostStats(); this.loadCostStats();
this.loadCostsChart(); this.loadCostsChart();
this.loadBudgetTracking(); this.loadBudgetTracking();
this.loadCostProjections();
this.loadPricingTable(); this.loadPricingTable();
} }
} }
// Initialize costs page when needed
window.initCosts = async () => { window.initCosts = async () => {
window.costsPage = new CostsPage(); window.costsPage = new CostsPage();
}; };
// Export for use in other modules
if (typeof module !== 'undefined' && module.exports) {
module.exports = CostsPage;
}