feat: implement analytics and cost management dashboard pages
This commit is contained in:
@@ -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;
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
@@ -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();
|
try {
|
||||||
await this.loadClientsChart();
|
const breakdown = await window.api.get('/analytics/breakdown');
|
||||||
await this.loadModelsChart();
|
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 charts:', error);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async loadAnalyticsChart() {
|
renderAnalyticsChart(series) {
|
||||||
try {
|
|
||||||
// Generate demo data
|
|
||||||
const labels = window.chartManager.generateDateLabels(7);
|
|
||||||
const data = {
|
const data = {
|
||||||
labels: labels,
|
labels: series.map(s => s.time),
|
||||||
datasets: [
|
datasets: [
|
||||||
{
|
{
|
||||||
label: 'Requests',
|
label: 'Requests',
|
||||||
data: labels.map(() => Math.floor(Math.random() * 1000) + 500),
|
data: series.map(s => s.requests),
|
||||||
color: '#3b82f6',
|
color: '#fe8019', // orange
|
||||||
fill: true
|
fill: true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'Tokens',
|
label: 'Tokens',
|
||||||
data: labels.map(() => Math.floor(Math.random() * 100000) + 50000),
|
data: series.map(s => s.tokens),
|
||||||
color: '#10b981',
|
color: '#b8bb26', // green
|
||||||
fill: true
|
fill: true,
|
||||||
},
|
hidden: true
|
||||||
{
|
|
||||||
label: 'Cost ($)',
|
|
||||||
data: labels.map(() => Math.random() * 50 + 10),
|
|
||||||
color: '#f59e0b',
|
|
||||||
fill: true
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
};
|
};
|
||||||
|
|
||||||
// Create chart
|
window.chartManager.createLineChart('analytics-chart', data);
|
||||||
window.chartManager.createLineChart('analytics-chart', data, {
|
|
||||||
scales: {
|
|
||||||
y: {
|
|
||||||
ticks: {
|
|
||||||
callback: function(value) {
|
|
||||||
return value.toLocaleString();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error loading analytics chart:', error);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async loadClientsChart() {
|
renderClientsChart(clients) {
|
||||||
try {
|
|
||||||
const data = {
|
const data = {
|
||||||
labels: ['Web App', 'Mobile App', 'API Integration', 'Internal Tools', 'Testing'],
|
labels: clients.map(c => c.label),
|
||||||
datasets: [{
|
datasets: [{
|
||||||
label: 'Requests',
|
label: 'Requests',
|
||||||
data: [45, 25, 15, 10, 5],
|
data: clients.map(c => c.value),
|
||||||
color: '#3b82f6'
|
color: '#83a598' // blue
|
||||||
}]
|
}]
|
||||||
};
|
};
|
||||||
|
|
||||||
window.chartManager.createHorizontalBarChart('clients-chart', data);
|
window.chartManager.createHorizontalBarChart('clients-chart', data);
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error loading clients chart:', error);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async loadModelsChart() {
|
renderModelsChart(models) {
|
||||||
try {
|
|
||||||
const data = {
|
const data = {
|
||||||
labels: ['gpt-4', 'gpt-3.5-turbo', 'gemini-pro', 'deepseek-chat', 'grok-beta'],
|
labels: models.map(m => m.label),
|
||||||
data: [35, 30, 20, 10, 5],
|
data: models.map(m => m.value),
|
||||||
colors: ['#3b82f6', '#60a5fa', '#10b981', '#f59e0b', '#8b5cf6']
|
colors: window.chartManager.defaultColors
|
||||||
};
|
};
|
||||||
|
|
||||||
window.chartManager.createDoughnutChart('models-chart', data);
|
window.chartManager.createDoughnutChart('models-chart', data);
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error loading models chart:', error);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
@@ -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>
|
|
||||||
|
return `
|
||||||
|
<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>
|
||||||
|
<span class="budget-amount">${window.api.formatCurrency(balance)}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="progress-bar">
|
<div class="progress-bar" style="height: 8px; background: var(--bg2); border-radius: 4px; overflow: hidden;">
|
||||||
<div class="progress-fill ${budget.color}" style="width: ${(budget.used / budget.total) * 100}%"></div>
|
<div class="progress-fill" style="width: ${percentage}%; height: 100%; background: ${balance < used ? 'var(--red)' : 'var(--green)'};"></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="budget-footer">
|
<div class="budget-footer" style="display: flex; justify-content: space-between; margin-top: 0.5rem; font-size: 0.75rem; color: var(--fg4);">
|
||||||
<span class="budget-percentage">${Math.round((budget.used / budget.total) * 100)}% used</span>
|
<span>Threshold: ${window.api.formatCurrency(used)}</span>
|
||||||
<span class="budget-remaining">$${budget.total - budget.used} remaining</span>
|
<span>Status: ${balance < used ? 'LOW' : 'Healthy'}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
`).join('');
|
|
||||||
|
|
||||||
// Add CSS for budget items
|
|
||||||
this.addBudgetStyles();
|
|
||||||
}
|
|
||||||
|
|
||||||
addBudgetStyles() {
|
|
||||||
const style = document.createElement('style');
|
|
||||||
style.textContent = `
|
|
||||||
.budget-item {
|
|
||||||
margin-bottom: 1.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.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);
|
}).join('');
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error loading budgets:', error);
|
||||||
}
|
}
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user