583da45296
Refactor the usage log filter toolbar into a shared reusable component for common, drawing, and task logs. Optimize desktop filters with a responsive grid, move secondary filters into a mobile drawer, standardize filter typography, remove redundant filter icons, and add the missing i18n translations for the new drawer description.
796 lines
26 KiB
TypeScript
Vendored
796 lines
26 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 { useState } from 'react'
|
|
import { type ColumnDef } from '@tanstack/react-table'
|
|
import { CircleAlert, Sparkles, KeyRound } from 'lucide-react'
|
|
import { useTranslation } from 'react-i18next'
|
|
import { getUserAvatarFallback, getUserAvatarStyle } from '@/lib/avatar'
|
|
import { formatBillingCurrencyFromUSD } from '@/lib/currency'
|
|
import {
|
|
formatUseTime,
|
|
formatLogQuota,
|
|
formatTimestampToDate,
|
|
} from '@/lib/format'
|
|
import { cn } from '@/lib/utils'
|
|
import { Avatar, AvatarFallback } from '@/components/ui/avatar'
|
|
import {
|
|
Tooltip,
|
|
TooltipContent,
|
|
TooltipProvider,
|
|
TooltipTrigger,
|
|
} from '@/components/ui/tooltip'
|
|
import { DataTableColumnHeader } from '@/components/data-table'
|
|
import { StatusBadge, type StatusBadgeProps } from '@/components/status-badge'
|
|
import { LOG_TYPE_ALL_VALUE } from '../../constants'
|
|
import type { UsageLog } from '../../data/schema'
|
|
import {
|
|
formatModelName,
|
|
getFirstResponseTimeColor,
|
|
getResponseTimeColor,
|
|
getTieredBillingSummary,
|
|
hasAnyCacheTokens,
|
|
parseLogOther,
|
|
isViolationFeeLog,
|
|
} from '../../lib/format'
|
|
import {
|
|
isDisplayableLogType,
|
|
isTimingLogType,
|
|
getLogTypeConfig,
|
|
isPerCallBilling,
|
|
} from '../../lib/utils'
|
|
import type { LogOtherData } from '../../types'
|
|
import { DetailsDialog } from '../dialogs/details-dialog'
|
|
import { ModelBadge } from '../model-badge'
|
|
import { useUsageLogsContext } from '../usage-logs-provider'
|
|
|
|
interface DetailSegment {
|
|
text: string
|
|
muted?: boolean
|
|
danger?: boolean
|
|
}
|
|
|
|
function formatRatioCompact(ratio: number | undefined): string {
|
|
if (ratio == null || !Number.isFinite(ratio)) return '-'
|
|
return ratio % 1 === 0
|
|
? String(ratio)
|
|
: ratio.toFixed(4).replace(/\.?0+$/, '')
|
|
}
|
|
|
|
function getGroupRatioText(other: LogOtherData | null): string | null {
|
|
const userGroupRatio = other?.user_group_ratio
|
|
if (
|
|
userGroupRatio != null &&
|
|
userGroupRatio !== -1 &&
|
|
Number.isFinite(userGroupRatio)
|
|
) {
|
|
return `${formatRatioCompact(userGroupRatio)}x`
|
|
}
|
|
|
|
const groupRatio = other?.group_ratio
|
|
if (groupRatio != null && groupRatio !== 1 && Number.isFinite(groupRatio)) {
|
|
return `${formatRatioCompact(groupRatio)}x`
|
|
}
|
|
|
|
return null
|
|
}
|
|
|
|
function buildDetailSegments(
|
|
log: UsageLog,
|
|
other: LogOtherData | null,
|
|
t: (key: string, opts?: Record<string, unknown>) => string
|
|
): DetailSegment[] {
|
|
if (log.type === 6) {
|
|
return [{ text: t('Async task refund') }]
|
|
}
|
|
|
|
if (log.type !== 2) return []
|
|
|
|
const isViolation = isViolationFeeLog(other)
|
|
if (isViolation) {
|
|
const segments: DetailSegment[] = []
|
|
segments.push({ text: t('Violation Fee'), danger: true })
|
|
if (other?.violation_fee_code) {
|
|
segments.push({
|
|
text: other.violation_fee_code,
|
|
muted: true,
|
|
})
|
|
}
|
|
segments.push({
|
|
text: `${t('Fee')}: ${formatLogQuota(other?.fee_quota ?? log.quota)}`,
|
|
muted: true,
|
|
})
|
|
return segments
|
|
}
|
|
|
|
if (!other) return []
|
|
|
|
const segments: DetailSegment[] = []
|
|
|
|
const priceOpts = { digitsLarge: 4, digitsSmall: 6, abbreviate: false }
|
|
const formatPrice = (price: number) =>
|
|
`${formatBillingCurrencyFromUSD(price, priceOpts)}/M`
|
|
const formatPriceCompact = (price: number) =>
|
|
formatBillingCurrencyFromUSD(price, priceOpts)
|
|
const formatPriceList = (prices: string[], showUnit: boolean) => {
|
|
const text = prices.join(' / ')
|
|
return showUnit ? `${text}/M` : text
|
|
}
|
|
const isTieredExpr = other.billing_mode === 'tiered_expr'
|
|
const tieredSummary = getTieredBillingSummary(other)
|
|
if (isTieredExpr) {
|
|
if (tieredSummary) {
|
|
const baseEntries = tieredSummary.priceEntries
|
|
.filter((entry) => ['inputPrice', 'outputPrice'].includes(entry.field))
|
|
.map((entry) => formatPriceCompact(entry.price))
|
|
if (baseEntries.length > 0) {
|
|
const tierLabel = tieredSummary.tier.label || t('Default')
|
|
segments.push({
|
|
text: `${tierLabel} · ${formatPriceList(baseEntries, true)}`,
|
|
})
|
|
}
|
|
|
|
const cacheEntries = tieredSummary.priceEntries
|
|
.filter((entry) =>
|
|
['cacheReadPrice', 'cacheCreatePrice', 'cacheCreate1hPrice'].includes(
|
|
entry.field
|
|
)
|
|
)
|
|
.map((entry) => {
|
|
return formatPriceCompact(entry.price)
|
|
})
|
|
if (cacheEntries.length > 0) {
|
|
segments.push({
|
|
text: `${t('Cache')} ${formatPriceList(cacheEntries, false)}`,
|
|
muted: true,
|
|
})
|
|
}
|
|
|
|
const otherEntries = tieredSummary.priceEntries
|
|
.filter(
|
|
(entry) =>
|
|
![
|
|
'inputPrice',
|
|
'outputPrice',
|
|
'cacheReadPrice',
|
|
'cacheCreatePrice',
|
|
'cacheCreate1hPrice',
|
|
].includes(entry.field)
|
|
)
|
|
.map((entry) => `${t(entry.shortLabel)} ${formatPrice(entry.price)}`)
|
|
if (otherEntries.length > 0) {
|
|
segments.push({
|
|
text: otherEntries.join(' · '),
|
|
muted: true,
|
|
})
|
|
}
|
|
} else {
|
|
segments.push({
|
|
text: `${t('Dynamic Pricing')} · ${t('No matching results')}`,
|
|
muted: true,
|
|
})
|
|
}
|
|
} else {
|
|
const isPerCall = isPerCallBilling(other.model_price)
|
|
if (isPerCall) {
|
|
segments.push({
|
|
text: `${t('Per-call')} · ${formatBillingCurrencyFromUSD(other.model_price!, priceOpts)}`,
|
|
})
|
|
} else if (other.model_ratio != null) {
|
|
const inputPriceUSD = other.model_ratio * 2.0
|
|
const baseEntries = [formatPriceCompact(inputPriceUSD)]
|
|
if (other.completion_ratio != null) {
|
|
baseEntries.push(
|
|
formatPriceCompact(inputPriceUSD * other.completion_ratio)
|
|
)
|
|
}
|
|
segments.push({
|
|
text: `${t('Standard')} · ${formatPriceList(baseEntries, true)}`,
|
|
})
|
|
|
|
if (hasAnyCacheTokens(other)) {
|
|
const cacheEntries = [
|
|
other.cache_ratio != null && other.cache_ratio !== 1
|
|
? formatPriceCompact(inputPriceUSD * other.cache_ratio)
|
|
: null,
|
|
other.cache_creation_ratio != null && other.cache_creation_ratio !== 1
|
|
? formatPriceCompact(inputPriceUSD * other.cache_creation_ratio)
|
|
: null,
|
|
other.cache_creation_ratio_1h != null &&
|
|
other.cache_creation_ratio_1h !== 0
|
|
? formatPriceCompact(inputPriceUSD * other.cache_creation_ratio_1h)
|
|
: null,
|
|
].filter(Boolean) as string[]
|
|
|
|
if (cacheEntries.length > 0) {
|
|
segments.push({
|
|
text: `${t('Cache')} ${formatPriceList(cacheEntries, false)}`,
|
|
muted: true,
|
|
})
|
|
}
|
|
}
|
|
} else {
|
|
const userGroupRatio = other.user_group_ratio
|
|
const groupRatio = other.group_ratio
|
|
const isUserGroup =
|
|
userGroupRatio != null &&
|
|
Number.isFinite(userGroupRatio) &&
|
|
userGroupRatio !== -1
|
|
const effectiveRatio = isUserGroup ? userGroupRatio : groupRatio
|
|
const ratioLabel = isUserGroup
|
|
? t('User Exclusive Ratio')
|
|
: t('Group Ratio')
|
|
|
|
if (effectiveRatio != null && Number.isFinite(effectiveRatio)) {
|
|
segments.push({
|
|
text: `${ratioLabel} ${formatRatioCompact(effectiveRatio)}x`,
|
|
})
|
|
}
|
|
}
|
|
}
|
|
|
|
if (other.is_system_prompt_overwritten) {
|
|
segments.push({
|
|
text: t('System Prompt Override'),
|
|
danger: true,
|
|
})
|
|
}
|
|
|
|
return segments
|
|
}
|
|
|
|
export function useCommonLogsColumns(isAdmin: boolean): ColumnDef<UsageLog>[] {
|
|
const { t } = useTranslation()
|
|
const columns: ColumnDef<UsageLog>[] = [
|
|
{
|
|
accessorKey: 'created_at',
|
|
header: ({ column }) => (
|
|
<DataTableColumnHeader column={column} title={t('Time')} />
|
|
),
|
|
cell: ({ row }) => {
|
|
const log = row.original
|
|
const timestamp = row.getValue('created_at') as number
|
|
const config = getLogTypeConfig(log.type)
|
|
|
|
return (
|
|
<div className='flex flex-col gap-0.5'>
|
|
<span className='font-mono text-xs tabular-nums'>
|
|
{formatTimestampToDate(timestamp)}
|
|
</span>
|
|
<StatusBadge
|
|
label={t(config.label)}
|
|
variant={config.color as StatusBadgeProps['variant']}
|
|
size='sm'
|
|
copyable={false}
|
|
/>
|
|
</div>
|
|
)
|
|
},
|
|
filterFn: (row, _id, value) => {
|
|
if (!Array.isArray(value) || value.length === 0) return true
|
|
if (value.includes(LOG_TYPE_ALL_VALUE)) return true
|
|
return value.includes(String(row.original.type))
|
|
},
|
|
enableHiding: false,
|
|
meta: { label: t('Time') },
|
|
},
|
|
]
|
|
|
|
if (isAdmin) {
|
|
columns.push(
|
|
{
|
|
id: 'channel',
|
|
header: ({ column }) => (
|
|
<DataTableColumnHeader column={column} title={t('Channel')} />
|
|
),
|
|
cell: function ChannelCell({ row }) {
|
|
const { sensitiveVisible, setAffinityTarget, setAffinityDialogOpen } =
|
|
useUsageLogsContext()
|
|
const log = row.original
|
|
|
|
if (!isDisplayableLogType(log.type)) return null
|
|
|
|
const other = parseLogOther(log.other)
|
|
const affinity = other?.admin_info?.channel_affinity
|
|
const useChannel = other?.admin_info?.use_channel
|
|
const channelChain =
|
|
useChannel && useChannel.length > 0
|
|
? useChannel.join(' → ')
|
|
: undefined
|
|
const channelDisplay = log.channel_name
|
|
? `${log.channel_name} #${log.channel}`
|
|
: `#${log.channel}`
|
|
const channelIdDisplay = `#${log.channel}`
|
|
const channelName = sensitiveVisible ? log.channel_name : '••••'
|
|
|
|
return (
|
|
<TooltipProvider>
|
|
<Tooltip>
|
|
<TooltipTrigger
|
|
render={
|
|
<div className='flex max-w-[160px] flex-col gap-0.5' />
|
|
}
|
|
>
|
|
<div className='relative inline-flex w-fit'>
|
|
<StatusBadge
|
|
label={channelIdDisplay}
|
|
autoColor={String(log.channel)}
|
|
copyText={String(log.channel)}
|
|
size='sm'
|
|
className='font-mono'
|
|
/>
|
|
{affinity && (
|
|
<button
|
|
type='button'
|
|
className='absolute -top-1 -right-1 leading-none text-amber-500'
|
|
onClick={(e) => {
|
|
e.stopPropagation()
|
|
setAffinityTarget({
|
|
rule_name: affinity.rule_name || '',
|
|
using_group:
|
|
affinity.using_group ||
|
|
affinity.selected_group ||
|
|
'',
|
|
key_hint: affinity.key_hint || '',
|
|
key_fp: affinity.key_fp || '',
|
|
})
|
|
setAffinityDialogOpen(true)
|
|
}}
|
|
>
|
|
<Sparkles className='size-3 fill-current' />
|
|
</button>
|
|
)}
|
|
</div>
|
|
{log.channel_name && (
|
|
<span className='text-muted-foreground/70 truncate text-[11px]'>
|
|
{channelName}
|
|
</span>
|
|
)}
|
|
</TooltipTrigger>
|
|
<TooltipContent>
|
|
<div className='space-y-1'>
|
|
<p>
|
|
{sensitiveVisible ? channelDisplay : channelIdDisplay}
|
|
</p>
|
|
{channelChain && (
|
|
<p className='text-muted-foreground text-xs'>
|
|
{t('Chain')}: {channelChain}
|
|
</p>
|
|
)}
|
|
{affinity && (
|
|
<div className='border-t pt-1 text-xs'>
|
|
<p className='font-medium'>{t('Channel Affinity')}</p>
|
|
<p>
|
|
{t('Rule')}: {affinity.rule_name || '-'}
|
|
</p>
|
|
<p>
|
|
{t('Group')}:{' '}
|
|
{sensitiveVisible
|
|
? affinity.using_group ||
|
|
affinity.selected_group ||
|
|
'-'
|
|
: '••••'}
|
|
</p>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</TooltipContent>
|
|
</Tooltip>
|
|
</TooltipProvider>
|
|
)
|
|
},
|
|
meta: { label: t('Channel'), mobileHidden: true },
|
|
},
|
|
{
|
|
id: 'user',
|
|
header: ({ column }) => (
|
|
<DataTableColumnHeader column={column} title={t('User')} />
|
|
),
|
|
cell: function UserCell({ row }) {
|
|
const { sensitiveVisible, setSelectedUserId, setUserInfoDialogOpen } =
|
|
useUsageLogsContext()
|
|
const log = row.original
|
|
|
|
if (!log.username) return null
|
|
|
|
return (
|
|
<button
|
|
type='button'
|
|
className='flex items-center gap-1.5 text-left'
|
|
onClick={(e) => {
|
|
e.stopPropagation()
|
|
setSelectedUserId(log.user_id)
|
|
setUserInfoDialogOpen(true)
|
|
}}
|
|
>
|
|
<Avatar className='ring-border/60 size-6 ring-1'>
|
|
<AvatarFallback
|
|
className={cn(
|
|
'text-[11px] font-semibold',
|
|
!sensitiveVisible && 'bg-muted text-muted-foreground'
|
|
)}
|
|
style={
|
|
sensitiveVisible
|
|
? getUserAvatarStyle(log.username)
|
|
: undefined
|
|
}
|
|
>
|
|
{sensitiveVisible ? getUserAvatarFallback(log.username) : '•'}
|
|
</AvatarFallback>
|
|
</Avatar>
|
|
<TooltipProvider delay={300}>
|
|
<Tooltip>
|
|
<TooltipTrigger
|
|
render={
|
|
<span className='text-muted-foreground max-w-[100px] truncate text-sm hover:underline' />
|
|
}
|
|
>
|
|
{sensitiveVisible ? log.username : '••••'}
|
|
</TooltipTrigger>
|
|
{sensitiveVisible && log.username.length > 12 && (
|
|
<TooltipContent side='top'>{log.username}</TooltipContent>
|
|
)}
|
|
</Tooltip>
|
|
</TooltipProvider>
|
|
</button>
|
|
)
|
|
},
|
|
meta: { label: t('User'), mobileHidden: true },
|
|
}
|
|
)
|
|
}
|
|
|
|
columns.push({
|
|
accessorKey: 'token_name',
|
|
header: ({ column }) => (
|
|
<DataTableColumnHeader column={column} title={t('Token')} />
|
|
),
|
|
cell: function TokenNameCell({ row }) {
|
|
const { sensitiveVisible } = useUsageLogsContext()
|
|
const log = row.original
|
|
if (!isDisplayableLogType(log.type)) return null
|
|
|
|
const tokenName = log.token_name
|
|
if (!tokenName) return null
|
|
|
|
const other = parseLogOther(log.other)
|
|
const displayName = sensitiveVisible ? tokenName : '••••'
|
|
let group = log.group
|
|
if (!group) group = other?.group || ''
|
|
|
|
const metaParts: string[] = []
|
|
const groupRatioText = getGroupRatioText(other)
|
|
if (group) {
|
|
metaParts.push(sensitiveVisible ? group : '••••')
|
|
}
|
|
if (groupRatioText) metaParts.push(groupRatioText)
|
|
|
|
return (
|
|
<div className='flex max-w-[200px] flex-col gap-0.5'>
|
|
<TooltipProvider delay={300}>
|
|
<Tooltip>
|
|
<TooltipTrigger render={<div className='max-w-full' />}>
|
|
<StatusBadge
|
|
label={displayName}
|
|
icon={KeyRound}
|
|
copyText={sensitiveVisible ? tokenName : undefined}
|
|
size='sm'
|
|
className='border-border/60 bg-muted/30 text-foreground max-w-full overflow-hidden rounded-md border px-1.5 py-0.5 font-mono'
|
|
/>
|
|
</TooltipTrigger>
|
|
{sensitiveVisible && tokenName.length > 16 && (
|
|
<TooltipContent side='top' className='max-w-xs break-all'>
|
|
{tokenName}
|
|
</TooltipContent>
|
|
)}
|
|
</Tooltip>
|
|
</TooltipProvider>
|
|
{metaParts.length > 0 && (
|
|
<span className='text-muted-foreground/60 truncate text-[11px]'>
|
|
{metaParts.join(' · ')}
|
|
</span>
|
|
)}
|
|
</div>
|
|
)
|
|
},
|
|
meta: { label: t('Token') },
|
|
size: 160,
|
|
})
|
|
|
|
columns.push(
|
|
{
|
|
accessorKey: 'model_name',
|
|
header: ({ column }) => (
|
|
<DataTableColumnHeader column={column} title={t('Model')} />
|
|
),
|
|
cell: function ModelCell({ row }) {
|
|
const log = row.original
|
|
if (!isDisplayableLogType(log.type)) return null
|
|
|
|
const modelInfo = formatModelName(log)
|
|
|
|
return (
|
|
<div className='flex max-w-[220px] flex-col gap-0.5'>
|
|
<ModelBadge
|
|
modelName={modelInfo.name}
|
|
actualModel={modelInfo.actualModel}
|
|
/>
|
|
</div>
|
|
)
|
|
},
|
|
meta: { label: t('Model'), mobileTitle: true },
|
|
},
|
|
|
|
{
|
|
accessorKey: 'use_time',
|
|
header: ({ column }) => (
|
|
<DataTableColumnHeader column={column} title={t('Timing')} />
|
|
),
|
|
cell: ({ row }) => {
|
|
const log = row.original
|
|
if (!isTimingLogType(log.type)) return null
|
|
|
|
const useTime = row.getValue('use_time') as number
|
|
const other = parseLogOther(log.other)
|
|
const frt = other?.frt
|
|
const tokensPerSecond =
|
|
useTime > 0 && log.completion_tokens > 0
|
|
? log.completion_tokens / useTime
|
|
: null
|
|
const timeVariant = getResponseTimeColor(useTime, log.completion_tokens)
|
|
const frtVariant = frt ? getFirstResponseTimeColor(frt / 1000) : null
|
|
|
|
return (
|
|
<div className='flex flex-col gap-1'>
|
|
<div className='flex items-center gap-1.5'>
|
|
<StatusBadge
|
|
label={formatUseTime(useTime)}
|
|
variant={timeVariant as StatusBadgeProps['variant']}
|
|
size='sm'
|
|
copyable={false}
|
|
className='font-mono'
|
|
/>
|
|
{log.is_stream &&
|
|
(frt != null && frt > 0 ? (
|
|
<StatusBadge
|
|
label={formatUseTime(frt / 1000)}
|
|
variant={frtVariant as StatusBadgeProps['variant']}
|
|
size='sm'
|
|
copyable={false}
|
|
className='font-mono'
|
|
/>
|
|
) : (
|
|
<StatusBadge
|
|
label='N/A'
|
|
variant='neutral'
|
|
size='sm'
|
|
copyable={false}
|
|
/>
|
|
))}
|
|
</div>
|
|
<div className='flex items-center gap-1 text-[11px]'>
|
|
<span className='text-muted-foreground/60'>
|
|
{log.is_stream ? t('Stream') : t('Non-stream')}
|
|
{tokensPerSecond != null && (
|
|
<>
|
|
{' · '}
|
|
<span className='font-mono tabular-nums'>
|
|
{Math.round(tokensPerSecond)}
|
|
</span>
|
|
{' t/s'}
|
|
</>
|
|
)}
|
|
</span>
|
|
{log.is_stream &&
|
|
other?.stream_status &&
|
|
other.stream_status.status !== 'ok' && (
|
|
<TooltipProvider>
|
|
<Tooltip>
|
|
<TooltipTrigger
|
|
render={<CircleAlert className='size-3 text-red-500' />}
|
|
></TooltipTrigger>
|
|
<TooltipContent>
|
|
<div className='space-y-0.5 text-xs'>
|
|
<p>
|
|
{t('Stream Status')}: {t('Error')}
|
|
</p>
|
|
<p>{other.stream_status.end_reason || 'unknown'}</p>
|
|
{(other.stream_status.error_count ?? 0) > 0 && (
|
|
<p>
|
|
{t('Soft Errors')}:{' '}
|
|
{other.stream_status.error_count}
|
|
</p>
|
|
)}
|
|
</div>
|
|
</TooltipContent>
|
|
</Tooltip>
|
|
</TooltipProvider>
|
|
)}
|
|
</div>
|
|
</div>
|
|
)
|
|
},
|
|
meta: { label: t('Timing'), mobileHidden: true },
|
|
},
|
|
|
|
{
|
|
accessorKey: 'prompt_tokens',
|
|
header: ({ column }) => (
|
|
<DataTableColumnHeader column={column} title='Tokens' />
|
|
),
|
|
cell: ({ row }) => {
|
|
const log = row.original
|
|
if (!isDisplayableLogType(log.type)) return null
|
|
|
|
const other = parseLogOther(log.other)
|
|
|
|
const promptTokens = log.prompt_tokens || 0
|
|
const completionTokens = log.completion_tokens || 0
|
|
if (promptTokens === 0 && completionTokens === 0) {
|
|
return <span className='text-muted-foreground text-xs'>-</span>
|
|
}
|
|
|
|
const cacheReadTokens = other?.cache_tokens || 0
|
|
const cacheWrite5m = other?.cache_creation_tokens_5m || 0
|
|
const cacheWrite1h = other?.cache_creation_tokens_1h || 0
|
|
const hasSplitCache = cacheWrite5m > 0 || cacheWrite1h > 0
|
|
const cacheWriteTokens = hasSplitCache
|
|
? cacheWrite5m + cacheWrite1h
|
|
: other?.cache_creation_tokens || 0
|
|
|
|
return (
|
|
<div className='flex flex-col gap-0.5'>
|
|
<span className='font-mono text-xs font-medium tabular-nums'>
|
|
{promptTokens.toLocaleString()} /{' '}
|
|
{completionTokens.toLocaleString()}
|
|
</span>
|
|
{(cacheReadTokens > 0 || cacheWriteTokens > 0) && (
|
|
<div className='flex items-center gap-1 text-[11px]'>
|
|
{cacheReadTokens > 0 && (
|
|
<span className='text-muted-foreground/60'>
|
|
{t('Cache')}↓ {cacheReadTokens.toLocaleString()}
|
|
</span>
|
|
)}
|
|
{cacheWriteTokens > 0 && (
|
|
<span className='text-muted-foreground/60'>
|
|
↑ {cacheWriteTokens.toLocaleString()}
|
|
</span>
|
|
)}
|
|
</div>
|
|
)}
|
|
</div>
|
|
)
|
|
},
|
|
meta: { label: 'Tokens', mobileHidden: true },
|
|
},
|
|
|
|
{
|
|
accessorKey: 'quota',
|
|
header: ({ column }) => (
|
|
<DataTableColumnHeader column={column} title={t('Cost')} />
|
|
),
|
|
cell: ({ row }) => {
|
|
const log = row.original
|
|
if (!isDisplayableLogType(log.type)) return null
|
|
|
|
const quota = row.getValue('quota') as number
|
|
const other = parseLogOther(log.other)
|
|
const isSubscription = other?.billing_source === 'subscription'
|
|
|
|
if (isSubscription) {
|
|
return (
|
|
<TooltipProvider>
|
|
<Tooltip>
|
|
<TooltipTrigger
|
|
render={
|
|
<StatusBadge
|
|
label={t('Subscription')}
|
|
variant='success'
|
|
size='sm'
|
|
copyable={false}
|
|
className='cursor-help'
|
|
/>
|
|
}
|
|
/>
|
|
<TooltipContent>
|
|
<span>
|
|
{t('Deducted by subscription')}: {formatLogQuota(quota)}
|
|
</span>
|
|
</TooltipContent>
|
|
</Tooltip>
|
|
</TooltipProvider>
|
|
)
|
|
}
|
|
|
|
const quotaStr = formatLogQuota(quota)
|
|
|
|
return (
|
|
<div className='flex flex-col gap-0.5'>
|
|
<span className='border-border/80 bg-muted/60 inline-flex w-fit items-center rounded-md border px-1.5 py-0.5 font-mono text-xs font-semibold tabular-nums'>
|
|
{quotaStr}
|
|
</span>
|
|
</div>
|
|
)
|
|
},
|
|
meta: { label: t('Cost') },
|
|
},
|
|
|
|
{
|
|
accessorKey: 'content',
|
|
header: t('Details'),
|
|
cell: function DetailsCell({ row }) {
|
|
const [dialogOpen, setDialogOpen] = useState(false)
|
|
const log = row.original
|
|
const other = parseLogOther(log.other)
|
|
|
|
const segments = buildDetailSegments(log, other, t)
|
|
const primary = segments[0]
|
|
const hasMore = segments.length > 1
|
|
|
|
return (
|
|
<>
|
|
<button
|
|
type='button'
|
|
className='group flex max-w-[200px] items-center gap-1 text-left text-xs'
|
|
onClick={() => setDialogOpen(true)}
|
|
title={t('Click to view full details')}
|
|
>
|
|
{primary ? (
|
|
<span
|
|
className={cn(
|
|
'truncate leading-snug group-hover:underline',
|
|
primary.muted
|
|
? 'text-muted-foreground/60'
|
|
: primary.danger
|
|
? 'text-red-600 dark:text-red-400'
|
|
: 'text-foreground'
|
|
)}
|
|
>
|
|
{primary.text}
|
|
{hasMore && (
|
|
<span className='text-muted-foreground/40 ml-0.5'>
|
|
+{segments.length - 1}
|
|
</span>
|
|
)}
|
|
</span>
|
|
) : log.content ? (
|
|
<span className='text-muted-foreground truncate group-hover:underline'>
|
|
{log.content}
|
|
</span>
|
|
) : (
|
|
<span className='text-muted-foreground/40'>—</span>
|
|
)}
|
|
</button>
|
|
<DetailsDialog
|
|
log={log}
|
|
isAdmin={isAdmin}
|
|
open={dialogOpen}
|
|
onOpenChange={setDialogOpen}
|
|
/>
|
|
</>
|
|
)
|
|
},
|
|
meta: { label: t('Details') },
|
|
size: 180,
|
|
maxSize: 200,
|
|
}
|
|
)
|
|
|
|
return columns
|
|
}
|