forked from admin/zeropost-tool
feat: spending — beautiful provider cards with progress bar
This commit is contained in:
+52
-50
@@ -339,65 +339,67 @@ function SpendingSection() {
|
|||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* По провайдерам */}
|
{/* По провайдерам — крупные карточки */}
|
||||||
<div className="grid grid-cols-2 gap-3">
|
{(() => {
|
||||||
{[
|
const providers = [
|
||||||
{ key: 'aiprimetech', label: 'aiprimetech.io', icon: '💬', desc: 'Текст' },
|
{ 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: 'Картинки' },
|
{ key: 'routerai', label: 'routerai.ru', icon: '🖼', desc: 'Картинки к постам', color: 'text-purple-400', bg: 'bg-purple-500/10', border: 'border-purple-500/20' },
|
||||||
].map(({ key, label, icon, desc }) => {
|
];
|
||||||
const d = byProv?.breakdown?.find(b => b.key === key);
|
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 (
|
return (
|
||||||
<div key={key} className="card p-4">
|
<div className="space-y-3">
|
||||||
<div className="flex items-center gap-2 mb-2">
|
<div className="text-xs text-gray-500 uppercase tracking-wide font-medium">Расходы по провайдерам</div>
|
||||||
<span className="text-lg">{icon}</span>
|
{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 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 (
|
||||||
|
<div key={key} className={`card p-5 border ${border} ${bg}`}>
|
||||||
|
<div className="flex items-start justify-between mb-3">
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<span className="text-2xl">{icon}</span>
|
||||||
<div>
|
<div>
|
||||||
<div className="font-medium text-sm">{label}</div>
|
<div className={`font-semibold text-sm ${color}`}>{label}</div>
|
||||||
<div className="text-xs text-gray-500">{desc}</div>
|
<div className="text-xs text-gray-400 mt-0.5">{desc}</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="ml-auto text-accent font-bold">₽ {fmt(d?.cost_rub)}</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="grid grid-cols-3 gap-1 text-xs text-gray-400">
|
<div className="text-right">
|
||||||
<div><div className="font-medium text-gray-200">{fmtI(d?.calls)}</div>запросов</div>
|
<div className={`text-2xl font-bold ${color}`}>₽ {fmt(rub)}</div>
|
||||||
<div><div className="font-medium text-red-400">{d?.failed||0}</div>ошибок</div>
|
<div className="text-xs text-gray-500 mt-0.5">{pct}% от общего</div>
|
||||||
<div><div className="font-medium text-gray-200">{fmtI(d?.image_count || (d?.prompt_tokens||0)+(d?.completion_tokens||0))}</div>{key==='routerai'?'картинок':'токенов'}</div>
|
</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>
|
||||||
|
<div className="bg-surface2/50 rounded-lg p-2 text-center">
|
||||||
|
<div className={`font-bold text-base ${(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/50 rounded-lg p-2 text-center">
|
||||||
|
<div className="font-bold text-sm text-gray-200">{vol}</div>
|
||||||
|
<div className="text-gray-500 mt-0.5">объём</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
|
);
|
||||||
{/* Все провайдеры (включая fallback) */}
|
})()}
|
||||||
{byProv?.breakdown?.length > 2 && (
|
|
||||||
<div className="card overflow-hidden">
|
|
||||||
<div className="px-4 py-3 bg-surface2 text-xs text-gray-400 font-medium uppercase tracking-wide">
|
|
||||||
Все провайдеры
|
|
||||||
</div>
|
|
||||||
<table className="w-full text-sm">
|
|
||||||
<thead className="text-xs text-gray-500 bg-surface2/50">
|
|
||||||
<tr>
|
|
||||||
<th className="px-4 py-2 text-left">Провайдер</th>
|
|
||||||
<th className="px-4 py-2 text-right">Запросов</th>
|
|
||||||
<th className="px-4 py-2 text-right">Ошибок</th>
|
|
||||||
<th className="px-4 py-2 text-right">Объём</th>
|
|
||||||
<th className="px-4 py-2 text-right">Стоимость</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
{byProv.breakdown.map((row, i) => (
|
|
||||||
<tr key={i} className="border-t border-border hover:bg-surface2/50">
|
|
||||||
<td className="px-4 py-2.5 font-medium">{row.key}</td>
|
|
||||||
<td className="px-4 py-2.5 text-right text-gray-300">{fmtI(row.calls)}</td>
|
|
||||||
<td className="px-4 py-2.5 text-right text-red-400">{row.failed||0}</td>
|
|
||||||
<td className="px-4 py-2.5 text-right text-gray-400 text-xs">
|
|
||||||
{row.image_count > 0 ? `${row.image_count} шт.` : fmtI((row.prompt_tokens||0)+(row.completion_tokens||0))}
|
|
||||||
</td>
|
|
||||||
<td className="px-4 py-2.5 text-right font-medium text-accent">₽ {fmt(row.cost_rub)}</td>
|
|
||||||
</tr>
|
|
||||||
))}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Таблица по операциям */}
|
{/* Таблица по операциям */}
|
||||||
<div className="card overflow-hidden">
|
<div className="card overflow-hidden">
|
||||||
|
|||||||
Reference in New Issue
Block a user