468 lines
16 KiB
JavaScript
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;
|
|
} |