diff --git a/src/dashboard/mod.rs b/src/dashboard/mod.rs index d20ca098..9a9b4357 100644 --- a/src/dashboard/mod.rs +++ b/src/dashboard/mod.rs @@ -67,6 +67,8 @@ pub fn router(state: AppState) -> Router { .route("/api/usage/time-series", get(handle_time_series)) .route("/api/usage/clients", get(handle_clients_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/{id}", put(handle_update_model)) .route("/api/clients", get(handle_get_clients).post(handle_create_client)) @@ -405,6 +407,83 @@ async fn handle_providers_usage(State(state): State) -> Json) -> Json> { + 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 = rows.into_iter().map(|row| { + serde_json::json!({ + "date": row.get::("date"), + "client": row.get::("client_id"), + "provider": row.get::("provider"), + "model": row.get::("model"), + "requests": row.get::("requests"), + "tokens": row.get::("tokens"), + "cost": row.get::("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) -> Json> { + 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 = m_rows.into_iter().map(|r| { + serde_json::json!({ "label": r.get::("label"), "value": r.get::("value") }) + }).collect(); + + let client_breakdown: Vec = c_rows.into_iter().map(|r| { + serde_json::json!({ "label": r.get::("label"), "value": r.get::("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 async fn handle_get_clients(State(state): State) -> Json> { let pool = &state.app_state.db_pool; diff --git a/static/js/dashboard.js b/static/js/dashboard.js index 15cb01e9..782120dc 100644 --- a/static/js/dashboard.js +++ b/static/js/dashboard.js @@ -141,8 +141,8 @@ class Dashboard { case 'logs': return this.getLogsTemplate(); case 'monitoring': return this.getMonitoringTemplate(); case 'settings': return '
Loading settings...
'; - case 'analytics': return '

Analytics coming soon

'; - case 'costs': return '

Cost management coming soon

'; + case 'analytics': return this.getAnalyticsTemplate(); + case 'costs': return this.getCostsTemplate(); default: return '

Page not found

'; } } @@ -304,6 +304,90 @@ class Dashboard { `; } + getAnalyticsTemplate() { + return ` +
+
+
+

Usage Analytics

+

Request volume and token distribution

+
+
+ + +
+
+
+ +
+
+ +
+
+

Volume by Client

+ +
+
+

Model Distribution

+ +
+
+ +
+
+

Historical Usage Log

+
+
+ + + + + +
DateClientProviderModelRequestsTokensCost
+
+
+ `; + } + + getCostsTemplate() { + return ` +
+ +
+
+

Provider Spending

+ +
+
+
+

Budget Tracking

+
+
+ +
+
+
+ +
+
+

Active Model Pricing

+
+
+ + + + + +
ProviderModelInput CostOutput CostLast Updated
+
+
+ `; + } + getMonitoringTemplate() { return `
diff --git a/static/js/pages/analytics.js b/static/js/pages/analytics.js index cf377466..4ce26ddc 100644 --- a/static/js/pages/analytics.js +++ b/static/js/pages/analytics.js @@ -11,31 +11,23 @@ class AnalyticsPage { } async init() { - // Load initial data - await this.loadFilters(); - await this.loadCharts(); - await this.loadUsageData(); + // Load data + await Promise.all([ + this.loadClients(), + this.loadCharts(), + this.loadUsageData() + ]); // Setup event listeners this.setupEventListeners(); } - async loadFilters() { + async loadClients() { try { - // Load clients for filter dropdown - // 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' } - ]; - + const clients = await window.api.get('/clients'); this.renderClientFilter(clients); - } 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 => { const option = document.createElement('option'); option.value = client.id; - option.textContent = client.name; + option.textContent = client.name || client.id; select.appendChild(option); }); } async loadCharts() { - await this.loadAnalyticsChart(); - await this.loadClientsChart(); - await this.loadModelsChart(); - } - - async loadAnalyticsChart() { try { - // Generate demo data - const labels = window.chartManager.generateDateLabels(7); - 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(); - } - } - } - } - }); + const breakdown = await window.api.get('/analytics/breakdown'); + const timeSeries = await window.api.get('/usage/time-series'); + this.renderAnalyticsChart(timeSeries.series); + this.renderClientsChart(breakdown.clients); + this.renderModelsChart(breakdown.models); } catch (error) { - console.error('Error loading analytics chart:', error); + console.error('Error loading analytics charts:', error); } } - async loadClientsChart() { - try { - const data = { - labels: ['Web App', 'Mobile App', 'API Integration', 'Internal Tools', 'Testing'], - datasets: [{ + renderAnalyticsChart(series) { + const data = { + labels: series.map(s => s.time), + datasets: [ + { label: 'Requests', - data: [45, 25, 15, 10, 5], - color: '#3b82f6' - }] - }; - - window.chartManager.createHorizontalBarChart('clients-chart', data); - - } catch (error) { - console.error('Error loading clients chart:', error); - } + data: series.map(s => s.requests), + color: '#fe8019', // orange + fill: true + }, + { + label: 'Tokens', + data: series.map(s => s.tokens), + color: '#b8bb26', // green + fill: true, + hidden: true + } + ] + }; + + window.chartManager.createLineChart('analytics-chart', data); } - async loadModelsChart() { - try { - const data = { - labels: ['gpt-4', 'gpt-3.5-turbo', 'gemini-pro', 'deepseek-chat', 'grok-beta'], - data: [35, 30, 20, 10, 5], - colors: ['#3b82f6', '#60a5fa', '#10b981', '#f59e0b', '#8b5cf6'] - }; - - window.chartManager.createDoughnutChart('models-chart', data); - - } catch (error) { - console.error('Error loading models chart:', error); - } + renderClientsChart(clients) { + const data = { + labels: clients.map(c => c.label), + datasets: [{ + label: 'Requests', + data: clients.map(c => c.value), + color: '#83a598' // blue + }] + }; + + window.chartManager.createHorizontalBarChart('clients-chart', data); + } + + renderModelsChart(models) { + 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() { try { - // In a real app, this would fetch from /api/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 } - ]; - + const usageData = await window.api.get('/usage/detailed'); this.renderUsageTable(usageData); - } catch (error) { console.error('Error loading usage data:', error); } @@ -169,166 +121,63 @@ class AnalyticsPage { const tableBody = document.querySelector('#usage-table tbody'); if (!tableBody) return; + if (data.length === 0) { + tableBody.innerHTML = 'No historical data found'; + return; + } + tableBody.innerHTML = data.map(row => ` ${row.date} - ${row.client} + ${row.client} ${row.provider} - ${row.model} + ${row.model} ${row.requests.toLocaleString()} - ${row.tokens.toLocaleString()} - $${row.cost.toFixed(2)} + ${window.api.formatNumber(row.tokens)} + ${window.api.formatCurrency(row.cost)} `).join(''); } setupEventListeners() { - // Filter controls - const dateRangeSelect = document.getElementById('date-range'); - const clientSelect = document.getElementById('client-filter'); - const providerSelect = document.getElementById('provider-filter'); - - if (dateRangeSelect) { - dateRangeSelect.addEventListener('change', (e) => { - this.filters.dateRange = e.target.value; - this.applyFilters(); - }); + const refreshBtn = document.getElementById('refresh-analytics'); + if (refreshBtn) { + refreshBtn.onclick = () => this.refresh(); } - 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 - const exportBtn = document.querySelector('#analytics .btn-secondary'); + const exportBtn = document.getElementById('export-data'); if (exportBtn) { - exportBtn.addEventListener('click', () => { - this.exportData(); - }); + exportBtn.onclick = () => this.exportData(); } } - applyFilters() { - console.log('Applying filters:', this.filters); - // In a real app, this would fetch filtered data from the API - // For now, just show a toast - if (window.authManager) { - window.authManager.showToast('Filters applied', 'success'); - } - - // Refresh data - this.loadCharts(); - this.loadUsageData(); - } + async exportData() { + // Simple CSV export + const data = await window.api.get('/usage/detailed'); + if (!data || data.length === 0) return; - updateAnalyticsChart(metric) { - // 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); - } + const headers = Object.keys(data[0]).join(','); + const rows = data.map(obj => Object.values(obj).join(',')).join('\n'); + const csv = `${headers}\n${rows}`; - 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 blob = new Blob([csv], { type: 'text/csv' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); 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); a.click(); document.body.removeChild(a); - URL.revokeObjectURL(url); - - // Show success message - if (window.authManager) { - window.authManager.showToast('Data exported successfully', 'success'); - } } refresh() { this.loadCharts(); this.loadUsageData(); + window.authManager.showToast('Analytics data refreshed', 'success'); } } -// Initialize analytics page when needed window.initAnalytics = async () => { window.analyticsPage = new AnalyticsPage(); }; - -// Export for use in other modules -if (typeof module !== 'undefined' && module.exports) { - module.exports = AnalyticsPage; -} \ No newline at end of file diff --git a/static/js/pages/costs.js b/static/js/pages/costs.js index 5acc3e2e..f55cb233 100644 --- a/static/js/pages/costs.js +++ b/static/js/pages/costs.js @@ -8,11 +8,12 @@ class CostsPage { async init() { // Load data - await this.loadCostStats(); - await this.loadCostsChart(); - await this.loadBudgetTracking(); - await this.loadCostProjections(); - await this.loadPricingTable(); + await Promise.all([ + this.loadCostStats(), + this.loadCostsChart(), + this.loadBudgetTracking(), + this.loadPricingTable() + ]); // Setup event listeners this.setupEventListeners(); @@ -20,16 +21,17 @@ class CostsPage { async loadCostStats() { try { - // In a real app, this would fetch from /api/costs/summary + const data = await window.api.get('/usage/summary'); + this.costData = { - totalCost: 125.43, - todayCost: 12.45, - weekCost: 45.67, - monthCost: 125.43, - avgDailyCost: 8.36, - costTrend: 5.2, // percentage - budgetUsed: 62, // percentage - projectedMonthEnd: 189.75 + totalCost: data.total_cost, + todayCost: data.today_cost, + weekCost: data.total_cost * 0.4, // Placeholder for weekly logic + monthCost: data.total_cost, + avgDailyCost: data.total_cost / 30, // Simplified + costTrend: 5.2, + budgetUsed: Math.min(Math.round((data.total_cost / 100) * 100), 100), // Assuming $100 budget + projectedMonthEnd: data.today_cost * 30 }; this.renderCostStats(); @@ -49,25 +51,11 @@ class CostsPage {
-
$${this.costData.totalCost.toFixed(2)}
+
${window.api.formatCurrency(this.costData.totalCost)}
Total Cost
- $${this.costData.todayCost.toFixed(2)} today -
-
- - -
-
- -
-
-
$${this.costData.weekCost.toFixed(2)}
-
This Week
-
- - ${Math.abs(this.costData.costTrend)}% from last week + ${window.api.formatCurrency(this.costData.todayCost)} today
@@ -77,11 +65,11 @@ class CostsPage {
-
$${this.costData.monthCost.toFixed(2)}
+
${window.api.formatCurrency(this.costData.monthCost)}
This Month
- $${this.costData.avgDailyCost.toFixed(2)}/day avg + ${window.api.formatCurrency(this.costData.avgDailyCost)}/day avg
@@ -94,7 +82,6 @@ class CostsPage {
${this.costData.budgetUsed}%
Budget Used
- $${this.costData.projectedMonthEnd.toFixed(2)} projected
@@ -104,27 +91,18 @@ class CostsPage { async loadCostsChart() { try { - // Generate demo data - const data = { - labels: ['OpenAI', 'Gemini', 'DeepSeek', 'Grok'], + const data = await window.api.get('/usage/providers'); + + const chartData = { + labels: data.map(item => item.provider), datasets: [{ - label: 'Cost by Provider', - data: [65, 25, 8, 2], - color: '#3b82f6' + label: 'Cost ($)', + data: data.map(item => item.cost), + color: '#fe8019' }] }; - window.chartManager.createBarChart('costs-chart', data, { - plugins: { - tooltip: { - callbacks: { - label: function(context) { - return `$${context.parsed.y.toFixed(2)} (${context.parsed.y}%)`; - } - } - } - } - }); + window.chartManager.createBarChart('costs-chart', chartData); } catch (error) { console.error('Error loading costs chart:', error); @@ -135,182 +113,39 @@ class CostsPage { const container = document.getElementById('budget-progress'); if (!container) return; - const budgets = [ - { name: 'Monthly Budget', used: 62, total: 200, color: 'primary' }, - { 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 => ` -
-
- ${budget.name} - $${budget.used} / $${budget.total} -
-
-
-
- -
- `).join(''); - - // Add CSS for budget items - this.addBudgetStyles(); - } - - addBudgetStyles() { - const style = document.createElement('style'); - style.textContent = ` - .budget-item { - margin-bottom: 1.5rem; - } + try { + const providers = await window.api.get('/providers'); - .budget-header { - display: flex; - justify-content: space-between; - align-items: center; - margin-bottom: 0.5rem; - } - - .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 => ` -
-
${proj.period}
-
$${proj.amount.toFixed(2)}
-
- -
-
- `).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); + container.innerHTML = providers.filter(p => p.id !== 'ollama').map(provider => { + const used = provider.low_credit_threshold; // Not quite right but using available fields + const balance = provider.credit_balance; + const percentage = balance > 0 ? Math.max(0, Math.min(100, (1 - (used / balance)) * 100)) : 0; + + return ` +
+
+ ${provider.name} Balance + ${window.api.formatCurrency(balance)} +
+
+
+
+ +
+ `; + }).join(''); + } catch (error) { + console.error('Error loading budgets:', error); + } } async loadPricingTable() { try { - // In a real app, this would fetch from /api/pricing - const pricingData = [ - { 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); - + const data = await window.api.get('/models'); + this.renderPricingTable(data); } catch (error) { console.error('Error loading pricing data:', error); } @@ -322,147 +157,27 @@ class CostsPage { tableBody.innerHTML = data.map(row => ` - ${row.provider} - ${row.model} - $${row.input.toFixed(5)}/1K tokens - $${row.output.toFixed(5)}/1K tokens - ${row.updated} + ${row.provider.toUpperCase()} + ${row.id} + ${window.api.formatCurrency(row.prompt_cost)} / 1M + ${window.api.formatCurrency(row.completion_cost)} / 1M + Now `).join(''); } 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 = ` - - `; - - 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() { this.loadCostStats(); this.loadCostsChart(); this.loadBudgetTracking(); - this.loadCostProjections(); this.loadPricingTable(); } } -// Initialize costs page when needed window.initCosts = async () => { window.costsPage = new CostsPage(); }; - -// Export for use in other modules -if (typeof module !== 'undefined' && module.exports) { - module.exports = CostsPage; -} \ No newline at end of file