Files
GopherGate/static/js/pages/costs.js
2026-02-26 11:51:36 -05:00

468 lines
16 KiB
JavaScript

// Costs Page Module
class CostsPage {
constructor() {
this.costData = null;
this.init();
}
async init() {
// Load data
await this.loadCostStats();
await this.loadCostsChart();
await this.loadBudgetTracking();
await this.loadCostProjections();
await this.loadPricingTable();
// Setup event listeners
this.setupEventListeners();
}
async loadCostStats() {
try {
// In a real app, this would fetch from /api/costs/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
};
this.renderCostStats();
} catch (error) {
console.error('Error loading cost stats:', error);
}
}
renderCostStats() {
const container = document.getElementById('cost-stats');
if (!container) return;
container.innerHTML = `
<div class="stat-card">
<div class="stat-icon warning">
<i class="fas fa-dollar-sign"></i>
</div>
<div class="stat-content">
<div class="stat-value">$${this.costData.totalCost.toFixed(2)}</div>
<div class="stat-label">Total Cost</div>
<div class="stat-change positive">
<i class="fas fa-arrow-up"></i>
$${this.costData.todayCost.toFixed(2)} 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 class="stat-card">
<div class="stat-icon success">
<i class="fas fa-calendar-alt"></i>
</div>
<div class="stat-content">
<div class="stat-value">$${this.costData.monthCost.toFixed(2)}</div>
<div class="stat-label">This Month</div>
<div class="stat-change">
<i class="fas fa-chart-line"></i>
$${this.costData.avgDailyCost.toFixed(2)}/day avg
</div>
</div>
</div>
<div class="stat-card">
<div class="stat-icon danger">
<i class="fas fa-piggy-bank"></i>
</div>
<div class="stat-content">
<div class="stat-value">${this.costData.budgetUsed}%</div>
<div class="stat-label">Budget Used</div>
<div class="stat-change">
<i class="fas fa-project-diagram"></i>
$${this.costData.projectedMonthEnd.toFixed(2)} projected
</div>
</div>
</div>
`;
}
async loadCostsChart() {
try {
// Generate demo data
const data = {
labels: ['OpenAI', 'Gemini', 'DeepSeek', 'Grok'],
datasets: [{
label: 'Cost by Provider',
data: [65, 25, 8, 2],
color: '#3b82f6'
}]
};
window.chartManager.createBarChart('costs-chart', data, {
plugins: {
tooltip: {
callbacks: {
label: function(context) {
return `$${context.parsed.y.toFixed(2)} (${context.parsed.y}%)`;
}
}
}
}
});
} catch (error) {
console.error('Error loading costs chart:', error);
}
}
async loadBudgetTracking() {
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 => `
<div class="budget-item">
<div class="budget-header">
<span class="budget-name">${budget.name}</span>
<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
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);
}
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() {
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);
} catch (error) {
console.error('Error loading pricing data:', error);
}
}
renderPricingTable(data) {
const tableBody = document.querySelector('#pricing-table tbody');
if (!tableBody) return;
tableBody.innerHTML = data.map(row => `
<tr>
<td>${row.provider}</td>
<td>${row.model}</td>
<td>$${row.input.toFixed(5)}/1K tokens</td>
<td>$${row.output.toFixed(5)}/1K tokens</td>
<td>${row.updated}</td>
</tr>
`).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 = `
<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() {
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;
}