TuringToken/web/src/components/dashboard/BentoStats.jsx

144 lines
4.8 KiB
JavaScript

/*
Copyright (C) 2025 QuantumNous
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as
published by the Free Software Foundation, either version 3 of the
License, or (at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
For commercial licensing, please contact support@quantumnous.com
*/
import React from 'react';
import { Activity, CreditCard, Hash, Layers, Gauge } from 'lucide-react';
import { renderQuota } from '../../helpers';
const formatNumber = (n) => {
if (n == null || isNaN(n)) return '—';
const v = Number(n);
if (v >= 1_000_000) return (v / 1_000_000).toFixed(1) + 'M';
if (v >= 1_000) return (v / 1_000).toFixed(1) + 'K';
return v.toLocaleString();
};
const formatLatency = (v) => {
if (v == null) return '—';
if (v < 1) return `${Math.round(v * 1000)}ms`;
return `${v.toFixed(2)}s`;
};
const BentoStat = ({ label, value, unit, sub, delta, icon: Icon, className = '' }) => {
return (
<div
className={`relative flex flex-col gap-3 rounded-2xl border border-slate-200 bg-white p-5 transition-shadow hover:shadow-md dark:border-white/5 dark:bg-white/[0.02] dark:hover:shadow-black/20 ${className}`}
>
<div className='flex items-center gap-3'>
<div className='flex h-9 w-9 items-center justify-center rounded-lg bg-slate-100 text-slate-600 dark:bg-white/5 dark:text-white/70'>
<Icon size={16} />
</div>
<div className='text-sm font-semibold text-slate-900 dark:text-white'>{label}</div>
</div>
<div className='flex flex-1 flex-col justify-center'>
<div className='flex items-baseline gap-2'>
<span className='font-mono text-3xl font-bold tabular-nums leading-none text-slate-900 md:text-4xl dark:text-white'>
{value}
</span>
{unit && <span className='text-xs text-slate-400 dark:text-white/40'>{unit}</span>}
</div>
</div>
<div className='space-y-1.5'>
{delta != null && (
<div
className={`inline-flex items-center gap-1.5 text-xs tabular-nums ${
delta < 0
? 'text-emerald-600 dark:text-emerald-400'
: delta > 0
? 'text-red-500 dark:text-red-400'
: 'text-slate-500 dark:text-white/50'
}`}
>
<span className='font-semibold'>
{delta > 0 ? '↑' : delta < 0 ? '↓' : '·'} {Math.abs(delta).toFixed(0)}%
</span>
<span className='font-normal text-slate-400 dark:text-white/40'>对比上月</span>
</div>
)}
{sub && (
<div className='text-[11px] text-slate-500 tabular-nums dark:text-white/40'>{sub}</div>
)}
</div>
</div>
);
};
const BentoStats = ({ metrics, monthDelta }) => {
return (
<>
<BentoStat
label='今日请求'
value={metrics.today.requests}
unit='次'
sub={
<span>
<span className='font-semibold text-slate-900 dark:text-white'>
{formatNumber(metrics.today.tokens)}
</span>{' '}
tokens
<span className='mx-1.5 text-slate-300 dark:text-white/30'>·</span>
{renderQuota(metrics.today.cost)}
</span>
}
icon={Activity}
/>
<BentoStat
label='今日 token'
value={formatNumber(metrics.today.tokens)}
unit='tokens'
sub={`今日花费 ${renderQuota(metrics.today.cost)}`}
icon={Hash}
/>
<BentoStat
label='平均响应'
value={formatLatency(metrics.today.avgLatency)}
sub='今日成功调用'
icon={Gauge}
/>
<BentoStat
label='本月消费'
value={metrics.month.cost ? renderQuota(metrics.month.cost) : '—'}
sub={
<span>
<span className='font-semibold tabular-nums text-slate-900 dark:text-white'>
{metrics.month.requests}
</span>{' '}
次调用
<span className='mx-1.5 text-slate-300 dark:text-white/30'>·</span>
<span className='tabular-nums'>{formatNumber(metrics.month.tokens)}</span> tokens
</span>
}
delta={monthDelta}
icon={CreditCard}
/>
<BentoStat
label='累计 token'
value={formatNumber(metrics.month.tokens)}
unit='本月'
sub='输入+输出'
icon={Layers}
/>
</>
);
};
export default BentoStats;