asdsadsad

This commit is contained in:
IGNY8 VPS (Salman)
2025-12-12 15:11:17 +00:00
parent f163a2e07d
commit 9cb0e05618
6 changed files with 722 additions and 351 deletions

View File

@@ -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>
);
}

View File

@@ -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>

View File

@@ -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>