Files
new-api/web/default/src/features/dashboard/components/models/log-stat-cards.tsx
T
t0ng7u d146e45e2f ⚖️ chore(web/default): add reusable copyright header tooling
Add a Bun script to apply and normalize AGPL copyright headers across the default frontend source files.

The script keeps headers idempotent, upgrades existing headers to the 2023-2026 QuantumNous range, and is exposed through `bun run copyright` for future maintenance.
2026-05-09 11:35:07 +08:00

161 lines
5.3 KiB
TypeScript
Vendored

/*
Copyright (C) 2023-2026 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 { useEffect, useState } from 'react'
import { useAuthStore } from '@/stores/auth-store'
import { formatNumber, formatQuota } from '@/lib/format'
import { computeTimeRange } from '@/lib/time'
import { Skeleton } from '@/components/ui/skeleton'
import { getUserQuotaDates } from '@/features/dashboard/api'
import { useModelStatCardsConfig } from '@/features/dashboard/hooks/use-dashboard-config'
import {
buildQueryParams,
calculateDashboardStats,
getDefaultDays,
} from '@/features/dashboard/lib'
import type {
QuotaDataItem,
DashboardFilters,
} from '@/features/dashboard/types'
interface LogStatCardsProps {
filters?: DashboardFilters
onDataUpdate?: (data: QuotaDataItem[], loading: boolean) => void
}
export function LogStatCards(props: LogStatCardsProps) {
const statCardsConfig = useModelStatCardsConfig()
const user = useAuthStore((state) => state.auth.user)
const isAdmin = !!(user?.role && user.role >= 10)
const [stats, setStats] = useState<{
totalQuota: number
totalCount: number
totalTokens: number
} | null>(null)
const [loading, setLoading] = useState(true)
const [error, setError] = useState(false)
const [timeRangeMinutes, setTimeRangeMinutes] = useState(0)
const { filters, onDataUpdate } = props
useEffect(() => {
const abortController = new AbortController()
// eslint-disable-next-line react-hooks/set-state-in-effect
setLoading(true)
setError(false)
onDataUpdate?.([], true)
const timeRange = computeTimeRange(
getDefaultDays(filters?.time_granularity),
filters?.start_timestamp,
filters?.end_timestamp
)
const timeDiff = (timeRange.end_timestamp - timeRange.start_timestamp) / 60
setTimeRangeMinutes(timeDiff)
getUserQuotaDates(buildQueryParams(timeRange, filters), isAdmin)
.then((res) => {
if (abortController.signal.aborted) return
const data = res?.data || []
setStats(calculateDashboardStats(data))
onDataUpdate?.(data, false)
})
.catch(() => {
if (abortController.signal.aborted) return
setStats(null)
setError(true)
onDataUpdate?.([], false)
})
.finally(() => {
if (!abortController.signal.aborted) {
setLoading(false)
}
})
return () => {
abortController.abort()
}
}, [filters, isAdmin, onDataUpdate])
const adaptedStats = {
rpm: stats?.totalCount ?? 0,
quota: stats?.totalQuota ?? 0,
tpm: stats?.totalTokens ?? 0,
}
const items = statCardsConfig.map((config) => ({
title: config.title,
value:
config.key === 'quota'
? formatQuota(config.getValue(adaptedStats, timeRangeMinutes))
: formatNumber(config.getValue(adaptedStats, timeRangeMinutes)),
desc: config.description,
icon: config.icon,
}))
return (
<div className='overflow-hidden rounded-lg border'>
<div className='divide-border/60 grid grid-cols-2 divide-x sm:grid-cols-3 lg:grid-cols-5'>
{items.map((it, idx) => {
const Icon = it.icon
return (
<div
key={it.title}
className={`px-3 py-2.5 sm:px-5 sm:py-4 ${idx === items.length - 1 && items.length % 2 !== 0 ? 'col-span-2 sm:col-span-1' : ''}`}
>
<div className='flex items-center gap-2'>
<Icon className='text-muted-foreground/60 size-3.5 shrink-0' />
<div className='text-muted-foreground truncate text-xs font-medium tracking-wider uppercase'>
{it.title}
</div>
</div>
{loading ? (
<div className='mt-2 space-y-1.5'>
<Skeleton className='h-7 w-20' />
<Skeleton className='h-3.5 w-28' />
</div>
) : error ? (
<>
<div className='text-muted-foreground mt-1.5 font-mono text-lg font-bold tracking-tight tabular-nums sm:mt-2 sm:text-2xl'>
--
</div>
<div className='text-muted-foreground/40 mt-1 hidden text-xs md:block'>
{it.desc}
</div>
</>
) : (
<>
<div className='text-foreground mt-1.5 font-mono text-lg font-bold tracking-tight tabular-nums sm:mt-2 sm:text-2xl'>
{it.value}
</div>
<div className='text-muted-foreground/60 mt-1 hidden text-xs md:block'>
{it.desc}
</div>
</>
)}
</div>
)
})}
</div>
</div>
)
}