asdsadsad
This commit is contained in:
@@ -7,12 +7,25 @@ import { useState, useEffect } from 'react';
|
||||
import { Card } from '../ui/card';
|
||||
import { DollarSign, TrendingUp, AlertCircle } from 'lucide-react';
|
||||
import Badge from '../ui/badge/Badge';
|
||||
import { getUsageAnalytics, type UsageAnalytics } from '../../services/billing.api';
|
||||
import { getCreditUsageSummary } from '../../services/billing.api';
|
||||
import { useToast } from '../ui/toast/ToastContainer';
|
||||
|
||||
interface OperationData {
|
||||
credits: number;
|
||||
cost: number;
|
||||
count: number;
|
||||
}
|
||||
|
||||
interface CreditUsageSummary {
|
||||
total_credits_used: number;
|
||||
total_cost_usd: string;
|
||||
by_operation: Record<string, OperationData>;
|
||||
by_model: Record<string, { credits: number; cost: number }>;
|
||||
}
|
||||
|
||||
export default function CreditCostBreakdownPanel() {
|
||||
const toast = useToast();
|
||||
const [analytics, setAnalytics] = useState<UsageAnalytics | null>(null);
|
||||
const [summary, setSummary] = useState<CreditUsageSummary | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string>('');
|
||||
const [period] = useState(30); // Last 30 days
|
||||
@@ -25,8 +38,17 @@ export default function CreditCostBreakdownPanel() {
|
||||
try {
|
||||
setLoading(true);
|
||||
setError('');
|
||||
const data = await getUsageAnalytics(period);
|
||||
setAnalytics(data);
|
||||
|
||||
// Calculate date range for last N days
|
||||
const endDate = new Date();
|
||||
const startDate = new Date();
|
||||
startDate.setDate(startDate.getDate() - period);
|
||||
|
||||
const data = await getCreditUsageSummary({
|
||||
start_date: startDate.toISOString(),
|
||||
end_date: endDate.toISOString(),
|
||||
});
|
||||
setSummary(data);
|
||||
} catch (err: any) {
|
||||
const message = err?.message || 'Failed to load cost analytics';
|
||||
setError(message);
|
||||
@@ -47,7 +69,7 @@ export default function CreditCostBreakdownPanel() {
|
||||
);
|
||||
}
|
||||
|
||||
if (error || !analytics) {
|
||||
if (error || !summary) {
|
||||
return (
|
||||
<Card className="p-6 text-center">
|
||||
<AlertCircle className="w-12 h-12 text-red-500 mx-auto mb-4" />
|
||||
@@ -65,63 +87,92 @@ export default function CreditCostBreakdownPanel() {
|
||||
);
|
||||
}
|
||||
|
||||
// Calculate metrics
|
||||
const totalCost = parseFloat(summary.total_cost_usd) || 0;
|
||||
const totalCredits = summary.total_credits_used || 0;
|
||||
const totalOperations = Object.values(summary.by_operation).reduce((sum, op) => sum + op.count, 0);
|
||||
const avgCostPerDay = totalCost / period;
|
||||
|
||||
// Convert by_operation to array and sort by cost
|
||||
const operationsList = Object.entries(summary.by_operation)
|
||||
.map(([type, data]) => ({
|
||||
operation_type: type,
|
||||
total: data.credits,
|
||||
count: data.count,
|
||||
cost_usd: data.cost,
|
||||
}))
|
||||
.sort((a, b) => b.cost_usd - a.cost_usd);
|
||||
|
||||
// Color palette for different operation types
|
||||
const operationColors = [
|
||||
{ bg: 'bg-[var(--color-brand-50)]', text: 'text-[var(--color-brand-500)]', border: 'border-[var(--color-brand-200)]' },
|
||||
{ bg: 'bg-[var(--color-success-50)]', text: 'text-[var(--color-success-500)]', border: 'border-[var(--color-success-200)]' },
|
||||
{ bg: 'bg-[var(--color-info-50)]', text: 'text-[var(--color-info-500)]', border: 'border-[var(--color-info-200)]' },
|
||||
{ bg: 'bg-[var(--color-purple-50)]', text: 'text-[var(--color-purple-500)]', border: 'border-[var(--color-purple-200)]' },
|
||||
{ bg: 'bg-[var(--color-warning-50)]', text: 'text-[var(--color-warning-500)]', border: 'border-[var(--color-warning-200)]' },
|
||||
{ bg: 'bg-[var(--color-teal-50)]', text: 'text-[var(--color-teal-500)]', border: 'border-[var(--color-teal-200)]' },
|
||||
{ bg: 'bg-brand-50 dark:bg-brand-900/20', text: 'text-brand-600 dark:text-brand-400', border: 'border-brand-500 dark:border-brand-400' },
|
||||
{ bg: 'bg-success-50 dark:bg-success-900/20', text: 'text-success-600 dark:text-success-400', border: 'border-success-500 dark:border-success-400' },
|
||||
{ bg: 'bg-info-50 dark:bg-info-900/20', text: 'text-info-600 dark:text-info-400', border: 'border-info-500 dark:border-info-400' },
|
||||
{ bg: 'bg-purple-50 dark:bg-purple-900/20', text: 'text-purple-600 dark:text-purple-400', border: 'border-purple-500 dark:border-purple-400' },
|
||||
{ bg: 'bg-warning-50 dark:bg-warning-900/20', text: 'text-warning-600 dark:text-warning-400', border: 'border-warning-500 dark:border-warning-400' },
|
||||
{ bg: 'bg-teal-50 dark:bg-teal-900/20', text: 'text-teal-600 dark:text-teal-400', border: 'border-teal-500 dark:border-teal-400' },
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
{/* Summary Cards */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||
<Card className="p-6 border-l-4 border-[var(--color-brand-500)]">
|
||||
{/* Summary Cards - 4 columns */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
|
||||
<Card className="p-6 border-l-4 border-brand-500 dark:border-brand-400">
|
||||
<div className="flex items-center gap-3 mb-2">
|
||||
<div className="p-2 bg-[var(--color-brand-50)] dark:bg-[var(--color-brand-900)]/20 rounded-lg">
|
||||
<DollarSign className="w-5 h-5 text-[var(--color-brand-500)]" />
|
||||
<div className="p-2 bg-brand-50 dark:bg-brand-900/20 rounded-lg">
|
||||
<DollarSign className="w-5 h-5 text-brand-500 dark:text-brand-400" />
|
||||
</div>
|
||||
<div className="text-sm text-gray-600 dark:text-gray-400">Total Cost</div>
|
||||
</div>
|
||||
<div className="text-3xl font-bold text-gray-900 dark:text-white">
|
||||
${((analytics.total_usage || 0) * 0.01).toFixed(2)}
|
||||
${totalCost.toFixed(2)}
|
||||
</div>
|
||||
<div className="text-xs text-gray-500 mt-1">Last {period} days</div>
|
||||
</Card>
|
||||
|
||||
<Card className="p-6 border-l-4 border-[var(--color-success-500)]">
|
||||
<Card className="p-6 border-l-4 border-success-500 dark:border-success-400">
|
||||
<div className="flex items-center gap-3 mb-2">
|
||||
<div className="p-2 bg-[var(--color-success-50)] dark:bg-[var(--color-success-900)]/20 rounded-lg">
|
||||
<TrendingUp className="w-5 h-5 text-[var(--color-success-500)]" />
|
||||
<div className="p-2 bg-success-50 dark:bg-success-900/20 rounded-lg">
|
||||
<TrendingUp className="w-5 h-5 text-success-500 dark:text-success-400" />
|
||||
</div>
|
||||
<div className="text-sm text-gray-600 dark:text-gray-400">Avg Cost/Day</div>
|
||||
</div>
|
||||
<div className="text-3xl font-bold text-gray-900 dark:text-white">
|
||||
${(((analytics.total_usage || 0) * 0.01) / period).toFixed(2)}
|
||||
${avgCostPerDay.toFixed(2)}
|
||||
</div>
|
||||
<div className="text-xs text-gray-500 mt-1">Daily average</div>
|
||||
</Card>
|
||||
|
||||
<Card className="p-6 border-l-4 border-[var(--color-info-500)]">
|
||||
<Card className="p-6 border-l-4 border-purple-500 dark:border-purple-400">
|
||||
<div className="flex items-center gap-3 mb-2">
|
||||
<div className="p-2 bg-[var(--color-info-50)] dark:bg-[var(--color-info-900)]/20 rounded-lg">
|
||||
<DollarSign className="w-5 h-5 text-[var(--color-info-500)]" />
|
||||
<div className="p-2 bg-purple-50 dark:bg-purple-900/20 rounded-lg">
|
||||
<TrendingUp className="w-5 h-5 text-purple-500 dark:text-purple-400" />
|
||||
</div>
|
||||
<div className="text-sm text-gray-600 dark:text-gray-400">Cost per Credit</div>
|
||||
<div className="text-sm text-gray-600 dark:text-gray-400">Total Operations</div>
|
||||
</div>
|
||||
<div className="text-3xl font-bold text-gray-900 dark:text-white">
|
||||
$0.01
|
||||
{totalOperations.toLocaleString()}
|
||||
</div>
|
||||
<div className="text-xs text-gray-500 mt-1">Standard rate</div>
|
||||
<div className="text-xs text-gray-500 mt-1">API calls</div>
|
||||
</Card>
|
||||
|
||||
<Card className="p-6 border-l-4 border-info-500 dark:border-info-400">
|
||||
<div className="flex items-center gap-3 mb-2">
|
||||
<div className="p-2 bg-info-50 dark:bg-info-900/20 rounded-lg">
|
||||
<DollarSign className="w-5 h-5 text-info-500 dark:text-info-400" />
|
||||
</div>
|
||||
<div className="text-sm text-gray-600 dark:text-gray-400">Total Credits</div>
|
||||
</div>
|
||||
<div className="text-3xl font-bold text-gray-900 dark:text-white">
|
||||
{totalCredits.toLocaleString()}
|
||||
</div>
|
||||
<div className="text-xs text-gray-500 mt-1">Credits used</div>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* Cost by Operation */}
|
||||
<Card className="p-6">
|
||||
<div className="flex items-center justify-between mb-6">
|
||||
{/* Cost by Operation - 4 columns */}
|
||||
<div>
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<div>
|
||||
<h3 className="text-lg font-semibold text-gray-900 dark:text-white">
|
||||
Cost by Operation Type
|
||||
@@ -135,53 +186,51 @@ export default function CreditCostBreakdownPanel() {
|
||||
</Badge>
|
||||
</div>
|
||||
|
||||
<div className="space-y-3">
|
||||
{analytics.usage_by_type.map((item: { transaction_type: string; total: number; count: number }, idx: number) => {
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
|
||||
{operationsList.map((item, idx) => {
|
||||
const colorScheme = operationColors[idx % operationColors.length];
|
||||
const costUSD = (item.total * 0.01).toFixed(2);
|
||||
const costUSD = item.cost_usd.toFixed(2);
|
||||
const avgPerOperation = item.count > 0 ? (item.total / item.count).toFixed(0) : '0';
|
||||
|
||||
return (
|
||||
<div
|
||||
key={idx}
|
||||
className={`flex items-center justify-between p-4 rounded-lg border-l-4 ${colorScheme.border} ${colorScheme.bg} dark:bg-opacity-10 transition-all hover:shadow-md`}
|
||||
<Card
|
||||
key={item.operation_type}
|
||||
className={`p-4 border-l-4 ${colorScheme.border} hover:shadow-lg transition-all`}
|
||||
>
|
||||
<div className="flex-1">
|
||||
<div className="flex items-center gap-3 mb-2">
|
||||
<h4 className={`font-semibold ${colorScheme.text}`}>
|
||||
{item.transaction_type}
|
||||
</h4>
|
||||
<Badge variant="soft" tone="neutral" size="xs">
|
||||
{item.count} ops
|
||||
</Badge>
|
||||
<div className="mb-3">
|
||||
<h4 className={`font-semibold ${colorScheme.text} mb-1`}>
|
||||
{item.operation_type}
|
||||
</h4>
|
||||
<Badge variant="soft" tone="neutral" size="xs">
|
||||
{item.count} ops
|
||||
</Badge>
|
||||
</div>
|
||||
<div className="space-y-2 text-sm">
|
||||
<div className="flex justify-between">
|
||||
<span className="text-gray-500 dark:text-gray-400">Credits:</span>
|
||||
<span className="font-medium text-gray-900 dark:text-white">
|
||||
{item.total.toLocaleString()}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-4 text-sm">
|
||||
<div>
|
||||
<span className="text-gray-500 dark:text-gray-400">Credits: </span>
|
||||
<span className="font-medium text-gray-900 dark:text-white">
|
||||
{item.total.toLocaleString()}
|
||||
</span>
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-gray-500 dark:text-gray-400">Avg/op: </span>
|
||||
<span className="font-medium text-gray-900 dark:text-white">
|
||||
{avgPerOperation} credits
|
||||
</span>
|
||||
<div className="flex justify-between">
|
||||
<span className="text-gray-500 dark:text-gray-400">Avg/op:</span>
|
||||
<span className="font-medium text-gray-900 dark:text-white">
|
||||
{avgPerOperation}
|
||||
</span>
|
||||
</div>
|
||||
<div className="pt-2 mt-2 border-t border-gray-200 dark:border-gray-700">
|
||||
<div className={`text-2xl font-bold ${colorScheme.text}`}>
|
||||
${costUSD}
|
||||
</div>
|
||||
<div className="text-xs text-gray-500">USD</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-right ml-4">
|
||||
<div className={`text-2xl font-bold ${colorScheme.text}`}>
|
||||
${costUSD}
|
||||
</div>
|
||||
<div className="text-xs text-gray-500 mt-1">USD</div>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
})}
|
||||
|
||||
{(!analytics.usage_by_type || analytics.usage_by_type.length === 0) && (
|
||||
<div className="text-center py-12">
|
||||
{operationsList.length === 0 && (
|
||||
<div className="col-span-4 text-center py-12">
|
||||
<DollarSign className="w-12 h-12 text-gray-300 dark:text-gray-600 mx-auto mb-3" />
|
||||
<p className="text-gray-500 dark:text-gray-400">
|
||||
No cost data available for this period
|
||||
@@ -189,7 +238,7 @@ export default function CreditCostBreakdownPanel() {
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ import { Card } from '../ui/card';
|
||||
import Badge from '../ui/badge/Badge';
|
||||
|
||||
// Credit costs per operation
|
||||
const CREDIT_COSTS: Record<string, { cost: number | string; description: string; color: string }> = {
|
||||
const CREDIT_COSTS: Record<string, { cost: number | string; description: string; color: 'brand' | 'success' | 'info' | 'warning' | 'purple' | 'indigo' | 'pink' | 'teal' | 'cyan' }> = {
|
||||
clustering: {
|
||||
cost: 10,
|
||||
description: 'Per clustering request',
|
||||
@@ -16,42 +16,42 @@ const CREDIT_COSTS: Record<string, { cost: number | string; description: string;
|
||||
idea_generation: {
|
||||
cost: 15,
|
||||
description: 'Per cluster → ideas request',
|
||||
color: 'brand'
|
||||
color: 'success'
|
||||
},
|
||||
content_generation: {
|
||||
cost: '1 per 100 words',
|
||||
description: 'Per 100 words generated',
|
||||
color: 'brand'
|
||||
color: 'purple'
|
||||
},
|
||||
image_prompt_extraction: {
|
||||
cost: 2,
|
||||
description: 'Per content piece',
|
||||
color: 'brand'
|
||||
color: 'info'
|
||||
},
|
||||
image_generation: {
|
||||
cost: 5,
|
||||
description: 'Per image generated',
|
||||
color: 'brand'
|
||||
color: 'indigo'
|
||||
},
|
||||
linking: {
|
||||
cost: 8,
|
||||
description: 'Per content piece',
|
||||
color: 'brand'
|
||||
color: 'teal'
|
||||
},
|
||||
optimization: {
|
||||
cost: '1 per 200 words',
|
||||
description: 'Per 200 words optimized',
|
||||
color: 'brand'
|
||||
color: 'warning'
|
||||
},
|
||||
site_structure_generation: {
|
||||
cost: 50,
|
||||
description: 'Per site blueprint',
|
||||
color: 'brand'
|
||||
color: 'pink'
|
||||
},
|
||||
site_page_generation: {
|
||||
cost: 20,
|
||||
description: 'Per page generated',
|
||||
color: 'brand'
|
||||
color: 'cyan'
|
||||
},
|
||||
};
|
||||
|
||||
@@ -80,7 +80,7 @@ export default function CreditCostsPanel() {
|
||||
</div>
|
||||
</div>
|
||||
<div className="ml-4 text-right flex-shrink-0">
|
||||
<Badge variant="soft" tone="brand" className="font-semibold">
|
||||
<Badge variant="soft" tone={info.color} className="font-semibold">
|
||||
{typeof info.cost === 'number' ? `${info.cost} credits` : info.cost}
|
||||
</Badge>
|
||||
</div>
|
||||
|
||||
@@ -24,24 +24,46 @@ function LimitCard({ title, icon, usage, type, daysUntilReset, accentColor = 'br
|
||||
const isWarning = percentage >= 80;
|
||||
const isDanger = percentage >= 95;
|
||||
|
||||
// Determine progress bar color
|
||||
let barColor = `bg-[var(--color-${accentColor}-500)]`;
|
||||
// Determine progress bar color - use inline styles for dynamic colors
|
||||
let barColor = 'var(--color-brand-500)';
|
||||
let badgeVariant: 'soft' = 'soft';
|
||||
let badgeTone: 'brand' | 'warning' | 'danger' | 'success' | 'info' | 'purple' | 'indigo' | 'pink' | 'teal' | 'cyan' = accentColor;
|
||||
|
||||
// Color mapping for progress bars
|
||||
const colorMap: Record<string, string> = {
|
||||
brand: '#0693e3',
|
||||
success: '#0bbf87',
|
||||
info: '#3b82f6',
|
||||
warning: '#ff7a00',
|
||||
danger: '#ef4444',
|
||||
purple: '#8b5cf6',
|
||||
indigo: '#6366f1',
|
||||
pink: '#ec4899',
|
||||
teal: '#14b8a6',
|
||||
cyan: '#06b6d4',
|
||||
};
|
||||
|
||||
if (isDanger) {
|
||||
barColor = 'bg-[var(--color-danger)]';
|
||||
barColor = colorMap.danger;
|
||||
badgeTone = 'danger';
|
||||
} else if (isWarning) {
|
||||
barColor = 'bg-[var(--color-warning)]';
|
||||
barColor = colorMap.warning;
|
||||
badgeTone = 'warning';
|
||||
} else {
|
||||
barColor = colorMap[accentColor] || colorMap.brand;
|
||||
}
|
||||
|
||||
return (
|
||||
<Card className="p-4 hover:shadow-md transition-shadow">
|
||||
<div className="flex items-start justify-between mb-3">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className={`p-2 bg-[var(--color-${accentColor}-50)] dark:bg-[var(--color-${accentColor}-900)]/20 rounded-lg text-[var(--color-${accentColor}-500)]`}>
|
||||
<div
|
||||
className="p-2 rounded-lg"
|
||||
style={{
|
||||
backgroundColor: isDanger ? 'rgba(239, 68, 68, 0.1)' : isWarning ? 'rgba(255, 122, 0, 0.1)' : `${barColor}15`,
|
||||
color: barColor
|
||||
}}
|
||||
>
|
||||
{icon}
|
||||
</div>
|
||||
<div>
|
||||
@@ -58,10 +80,13 @@ function LimitCard({ title, icon, usage, type, daysUntilReset, accentColor = 'br
|
||||
|
||||
{/* Progress Bar */}
|
||||
<div className="mb-3">
|
||||
<div className="h-2 bg-gray-200 dark:bg-gray-700 rounded-full overflow-hidden">
|
||||
<div className="h-2.5 bg-gray-200 dark:bg-gray-700 rounded-full overflow-hidden">
|
||||
<div
|
||||
className={`h-full ${barColor} transition-all duration-300`}
|
||||
style={{ width: `${Math.min(percentage, 100)}%` }}
|
||||
className="h-full transition-all duration-300 ease-out"
|
||||
style={{
|
||||
width: `${Math.min(percentage, 100)}%`,
|
||||
backgroundColor: barColor
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -228,9 +253,9 @@ export default function UsageLimitsPanel() {
|
||||
{/* Upgrade CTA if approaching limits */}
|
||||
{(Object.values(summary.hard_limits).some(u => u.percentage_used >= 80) ||
|
||||
Object.values(summary.monthly_limits).some(u => u.percentage_used >= 80)) && (
|
||||
<Card className="p-6 bg-gradient-to-r from-[var(--color-brand-50)] to-[var(--color-brand-100)] dark:from-[var(--color-brand-900)]/20 dark:to-[var(--color-brand-900)]/10 border-[var(--color-brand-200)] dark:border-[var(--color-brand-700)]">
|
||||
<Card className="p-6 bg-gradient-to-r from-brand-50 to-brand-100 dark:from-brand-900/20 dark:to-brand-800/10 border-brand-200 dark:border-brand-700">
|
||||
<div className="flex items-start gap-4">
|
||||
<div className="p-3 bg-[var(--color-brand-500)] rounded-lg text-white">
|
||||
<div className="p-3 bg-brand-500 rounded-lg text-white">
|
||||
<TrendingUp className="w-6 h-6" />
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
@@ -242,10 +267,10 @@ export default function UsageLimitsPanel() {
|
||||
and avoid interruptions.
|
||||
</p>
|
||||
<a
|
||||
href="/account/plans-and-billing?tab=purchase"
|
||||
className="inline-flex items-center px-4 py-2 bg-[var(--color-brand-500)] text-white rounded-lg hover:bg-[var(--color-brand-600)] transition-colors"
|
||||
href="/account/plans?tab=upgrade"
|
||||
className="inline-flex items-center px-4 py-2 bg-brand-500 text-white rounded-lg hover:bg-brand-600 transition-colors"
|
||||
>
|
||||
Purchase Credits
|
||||
Upgrade Plan
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user