feat: spending — 2 separate provider blocks side by side

This commit is contained in:
Nik (Claude)
2026-06-16 14:11:25 +03:00
parent 3815ac767c
commit 69367da665
+50 -45
View File
@@ -339,68 +339,73 @@ function SpendingSection() {
))} ))}
</div> </div>
{/* По провайдерам — крупные карточки */} {/* 2 отдельных блока — aiprimetech и routerai */}
<div className="grid grid-cols-2 gap-4">
{/* aiprimetech — текст */}
{(() => { {(() => {
const providers = [ const d = byProv?.breakdown?.find(b => b.key === 'aiprimetech');
{ key: 'aiprimetech', label: 'aiprimetech.io', icon: '💬', desc: 'Текст — статьи и посты', color: 'text-blue-400', bg: 'bg-blue-500/10', border: 'border-blue-500/20' },
{ key: 'routerai', label: 'routerai.ru', icon: '🖼', desc: 'Картинки к постам', color: 'text-purple-400', bg: 'bg-purple-500/10', border: 'border-purple-500/20' },
];
const totalRub = providers.reduce((sum, p) => {
const d = byProv?.breakdown?.find(b => b.key === p.key);
return sum + Number(d?.cost_rub || 0);
}, 0);
return (
<div className="space-y-3">
<div className="text-xs text-gray-500 uppercase tracking-wide font-medium">Расходы по провайдерам</div>
{providers.map(({ key, label, icon, desc, color, bg, border }) => {
const d = byProv?.breakdown?.find(b => b.key === key);
const rub = Number(d?.cost_rub || 0); const rub = Number(d?.cost_rub || 0);
const pct = totalRub > 0 ? Math.round(rub / totalRub * 100) : 0;
const vol = key === 'routerai'
? `${fmtI(d?.image_count || 0)} картинок`
: `${fmtI((d?.prompt_tokens||0)+(d?.completion_tokens||0))} токенов`;
return ( return (
<div key={key} className={`card p-5 border ${border} ${bg}`}> <div className="card p-5 border border-blue-500/20 bg-blue-500/5 space-y-4">
<div className="flex items-start justify-between mb-3">
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<span className="text-2xl">{icon}</span> <div className="w-10 h-10 rounded-xl bg-blue-500/20 flex items-center justify-center text-xl">💬</div>
<div> <div>
<div className={`font-semibold text-sm ${color}`}>{label}</div> <div className="font-semibold text-sm text-blue-400">aiprimetech.io</div>
<div className="text-xs text-gray-400 mt-0.5">{desc}</div> <div className="text-xs text-gray-400">Текст статьи и посты</div>
</div> </div>
</div> </div>
<div className="text-right"> <div className="text-3xl font-bold text-blue-400"> {fmt(rub)}</div>
<div className={`text-2xl font-bold ${color}`}> {fmt(rub)}</div> <div className="grid grid-cols-3 gap-2 text-xs">
<div className="text-xs text-gray-500 mt-0.5">{pct}% от общего</div> <div className="bg-surface2/60 rounded-lg p-2.5 text-center">
</div> <div className="font-bold text-lg text-gray-200">{fmtI(d?.calls)}</div>
</div>
{/* Прогресс-бар */}
<div className="h-1.5 bg-surface2 rounded-full mb-3">
<div className={`h-1.5 rounded-full ${color.replace('text-','bg-')}`}
style={{ width: `${pct}%` }} />
</div>
{/* Метрики */}
<div className="grid grid-cols-3 gap-3 text-xs">
<div className="bg-surface2/50 rounded-lg p-2 text-center">
<div className="font-bold text-base text-gray-200">{fmtI(d?.calls)}</div>
<div className="text-gray-500 mt-0.5">запросов</div> <div className="text-gray-500 mt-0.5">запросов</div>
</div> </div>
<div className="bg-surface2/50 rounded-lg p-2 text-center"> <div className="bg-surface2/60 rounded-lg p-2.5 text-center">
<div className={`font-bold text-base ${(d?.failed||0) > 0 ? 'text-red-400' : 'text-gray-200'}`}>{d?.failed||0}</div> <div className={`font-bold text-lg ${(d?.failed||0) > 0 ? 'text-red-400' : 'text-gray-200'}`}>{d?.failed||0}</div>
<div className="text-gray-500 mt-0.5">ошибок</div> <div className="text-gray-500 mt-0.5">ошибок</div>
</div> </div>
<div className="bg-surface2/50 rounded-lg p-2 text-center"> <div className="bg-surface2/60 rounded-lg p-2.5 text-center">
<div className="font-bold text-sm text-gray-200">{vol}</div> <div className="font-bold text-sm text-gray-200">{fmtI((d?.prompt_tokens||0)+(d?.completion_tokens||0))}</div>
<div className="text-gray-500 mt-0.5">объём</div> <div className="text-gray-500 mt-0.5">токенов</div>
</div> </div>
</div> </div>
</div> </div>
); );
})}
</div>
);
})()} })()}
{/* routerai — картинки */}
{(() => {
const d = byProv?.breakdown?.find(b => b.key === 'routerai');
const rub = Number(d?.cost_rub || 0);
return (
<div className="card p-5 border border-purple-500/20 bg-purple-500/5 space-y-4">
<div className="flex items-center gap-3">
<div className="w-10 h-10 rounded-xl bg-purple-500/20 flex items-center justify-center text-xl">🖼</div>
<div>
<div className="font-semibold text-sm text-purple-400">routerai.ru</div>
<div className="text-xs text-gray-400">Картинки к постам</div>
</div>
</div>
<div className="text-3xl font-bold text-purple-400"> {fmt(rub)}</div>
<div className="grid grid-cols-3 gap-2 text-xs">
<div className="bg-surface2/60 rounded-lg p-2.5 text-center">
<div className="font-bold text-lg text-gray-200">{fmtI(d?.calls)}</div>
<div className="text-gray-500 mt-0.5">запросов</div>
</div>
<div className="bg-surface2/60 rounded-lg p-2.5 text-center">
<div className={`font-bold text-lg ${(d?.failed||0) > 0 ? 'text-red-400' : 'text-gray-200'}`}>{d?.failed||0}</div>
<div className="text-gray-500 mt-0.5">ошибок</div>
</div>
<div className="bg-surface2/60 rounded-lg p-2.5 text-center">
<div className="font-bold text-lg text-gray-200">{fmtI(d?.image_count||0)}</div>
<div className="text-gray-500 mt-0.5">картинок</div>
</div>
</div>
</div>
);
})()}
</div>
{/* Таблица по операциям */} {/* Таблица по операциям */}
<div className="card overflow-hidden"> <div className="card overflow-hidden">
<table className="w-full text-sm"> <table className="w-full text-sm">