style: optimize UI
This commit is contained in:
@@ -176,7 +176,7 @@ export function DynamicPricingBreakdown({
|
||||
|
||||
if (!hasTiers && !hasRules) {
|
||||
return (
|
||||
<section className='py-4'>
|
||||
<section className='min-w-0 py-4'>
|
||||
<div className='mb-3 flex items-center gap-2'>
|
||||
<span className='inline-flex size-6 items-center justify-center rounded-full bg-amber-100 text-amber-700 shadow-sm dark:bg-amber-500/20 dark:text-amber-300'>
|
||||
<TagIcon className='size-3.5' />
|
||||
@@ -201,7 +201,7 @@ export function DynamicPricingBreakdown({
|
||||
})
|
||||
|
||||
return (
|
||||
<section className='py-4'>
|
||||
<section className='min-w-0 py-4'>
|
||||
<div className='mb-4 flex items-start gap-2'>
|
||||
<span className='mt-0.5 inline-flex size-6 items-center justify-center rounded-full bg-amber-100 text-amber-700 shadow-sm dark:bg-amber-500/20 dark:text-amber-300'>
|
||||
<TagIcon className='size-3.5' />
|
||||
@@ -221,7 +221,7 @@ export function DynamicPricingBreakdown({
|
||||
<div className='text-foreground mb-2 text-sm font-semibold'>
|
||||
{t('Tiered price table')}
|
||||
</div>
|
||||
<div className='-mx-4 sm:mx-0'>
|
||||
<div className='-mx-4 max-w-[calc(100%+2rem)] overflow-x-auto sm:mx-0 sm:max-w-full'>
|
||||
<Table className='text-sm'>
|
||||
<TableHeader>
|
||||
<TableRow className='hover:bg-transparent'>
|
||||
|
||||
@@ -1,234 +0,0 @@
|
||||
import { useMemo, useState } from 'react'
|
||||
import { useQuery } from '@tanstack/react-query'
|
||||
import { ChevronDown, ChevronUp, Copy, Settings } from 'lucide-react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { toast } from 'sonner'
|
||||
import { api } from '@/lib/api'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from '@/components/ui/card'
|
||||
import { Skeleton } from '@/components/ui/skeleton'
|
||||
import { Tabs, TabsList, TabsTrigger } from '@/components/ui/tabs'
|
||||
import { StatusBadge } from '@/components/status-badge'
|
||||
|
||||
const MODEL_CATEGORIES = [
|
||||
{ key: 'all', label: 'All', filter: () => true },
|
||||
{
|
||||
key: 'gpt',
|
||||
label: 'GPT',
|
||||
filter: (m: string) => /^(gpt|o[0-9]|chatgpt)/i.test(m),
|
||||
},
|
||||
{ key: 'claude', label: 'Claude', filter: (m: string) => /claude/i.test(m) },
|
||||
{
|
||||
key: 'gemini',
|
||||
label: 'Gemini',
|
||||
filter: (m: string) => /gemini|gemma/i.test(m),
|
||||
},
|
||||
{ key: 'llama', label: 'Llama', filter: (m: string) => /llama/i.test(m) },
|
||||
{
|
||||
key: 'mistral',
|
||||
label: 'Mistral',
|
||||
filter: (m: string) => /mistral|mixtral/i.test(m),
|
||||
},
|
||||
{
|
||||
key: 'deepseek',
|
||||
label: 'DeepSeek',
|
||||
filter: (m: string) => /deepseek/i.test(m),
|
||||
},
|
||||
{ key: 'qwen', label: 'Qwen', filter: (m: string) => /qwen/i.test(m) },
|
||||
{
|
||||
key: 'embedding',
|
||||
label: 'Embedding',
|
||||
filter: (m: string) => /embed/i.test(m),
|
||||
},
|
||||
{
|
||||
key: 'image',
|
||||
label: 'Image',
|
||||
filter: (m: string) =>
|
||||
/dall-e|stable-diffusion|midjourney|sd[x3]|flux|imagen/i.test(m),
|
||||
},
|
||||
{
|
||||
key: 'tts',
|
||||
label: 'TTS',
|
||||
filter: (m: string) => /tts|whisper|speech/i.test(m),
|
||||
},
|
||||
] as const
|
||||
|
||||
const MODELS_DISPLAY_COUNT = 25
|
||||
|
||||
export function AvailableModelsCard() {
|
||||
const { t } = useTranslation()
|
||||
const [activeCategory, setActiveCategory] = useState('all')
|
||||
const [isExpanded, setIsExpanded] = useState(() => {
|
||||
try {
|
||||
return JSON.parse(localStorage.getItem('modelsExpanded') ?? 'false')
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
})
|
||||
|
||||
const { data: models = [], isLoading } = useQuery({
|
||||
queryKey: ['user-available-models'],
|
||||
queryFn: async () => {
|
||||
const res = await api.get('/api/user/models')
|
||||
if (!res.data.success || !Array.isArray(res.data.data)) return []
|
||||
return res.data.data as string[]
|
||||
},
|
||||
staleTime: 5 * 60 * 1000,
|
||||
})
|
||||
|
||||
const toggleExpand = (val: boolean) => {
|
||||
setIsExpanded(val)
|
||||
localStorage.setItem('modelsExpanded', JSON.stringify(val))
|
||||
}
|
||||
|
||||
const copyModel = (model: string) => {
|
||||
navigator.clipboard.writeText(model)
|
||||
toast.success(t('Copied: {{model}}', { model }))
|
||||
}
|
||||
|
||||
const categoriesWithCounts = useMemo(
|
||||
() =>
|
||||
MODEL_CATEGORIES.map((cat) => ({
|
||||
...cat,
|
||||
count:
|
||||
cat.key === 'all' ? models.length : models.filter(cat.filter).length,
|
||||
})).filter((cat) => cat.key === 'all' || cat.count > 0),
|
||||
[models]
|
||||
)
|
||||
|
||||
const filteredModels = useMemo(() => {
|
||||
const cat = MODEL_CATEGORIES.find((c) => c.key === activeCategory)
|
||||
if (!cat || cat.key === 'all') return models
|
||||
return models.filter(cat.filter)
|
||||
}, [models, activeCategory])
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className='flex items-center gap-2'>
|
||||
<Settings className='h-4 w-4' />
|
||||
{t('Available Models')}
|
||||
</CardTitle>
|
||||
<CardDescription>
|
||||
{t('View all currently available models')}
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className='flex flex-wrap gap-2'>
|
||||
{Array.from({ length: 12 }).map((_, i) => (
|
||||
<Skeleton key={i} className='h-7 w-24 rounded-full' />
|
||||
))}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
|
||||
if (models.length === 0) {
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className='flex items-center gap-2'>
|
||||
<Settings className='h-4 w-4' />
|
||||
{t('Available Models')}
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<p className='text-muted-foreground text-sm'>
|
||||
{t('No available models')}
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
|
||||
const needsExpand = filteredModels.length > MODELS_DISPLAY_COUNT
|
||||
const displayModels =
|
||||
needsExpand && !isExpanded
|
||||
? filteredModels.slice(0, MODELS_DISPLAY_COUNT)
|
||||
: filteredModels
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className='flex items-center gap-2'>
|
||||
<Settings className='h-4 w-4' />
|
||||
{t('Available Models')}
|
||||
</CardTitle>
|
||||
<CardDescription>
|
||||
{t('View all currently available models')} · {models.length}{' '}
|
||||
{t('models')}
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className='space-y-4'>
|
||||
<Tabs value={activeCategory} onValueChange={setActiveCategory}>
|
||||
<TabsList className='h-auto flex-wrap'>
|
||||
{categoriesWithCounts.map((cat) => (
|
||||
<TabsTrigger key={cat.key} value={cat.key} className='text-xs'>
|
||||
{cat.label}
|
||||
<StatusBadge
|
||||
label={String(cat.count)}
|
||||
variant={activeCategory === cat.key ? 'info' : 'neutral'}
|
||||
className='ml-1'
|
||||
copyable={false}
|
||||
/>
|
||||
</TabsTrigger>
|
||||
))}
|
||||
</TabsList>
|
||||
</Tabs>
|
||||
|
||||
<div className='flex flex-wrap gap-1.5'>
|
||||
{displayModels.map((model) => (
|
||||
<StatusBadge
|
||||
key={model}
|
||||
variant='neutral'
|
||||
className='cursor-pointer font-normal transition-opacity hover:opacity-70'
|
||||
onClick={() => copyModel(model)}
|
||||
copyable={false}
|
||||
>
|
||||
<Copy className='h-2.5 w-2.5 opacity-50' />
|
||||
{model}
|
||||
</StatusBadge>
|
||||
))}
|
||||
|
||||
{needsExpand && !isExpanded && (
|
||||
<Button
|
||||
variant='ghost'
|
||||
size='sm'
|
||||
className='h-6 gap-1 text-xs'
|
||||
onClick={() => toggleExpand(true)}
|
||||
>
|
||||
<ChevronDown className='h-3 w-3' />
|
||||
{t('More')} {filteredModels.length - MODELS_DISPLAY_COUNT}{' '}
|
||||
{t('models')}
|
||||
</Button>
|
||||
)}
|
||||
{needsExpand && isExpanded && (
|
||||
<Button
|
||||
variant='ghost'
|
||||
size='sm'
|
||||
className='h-6 gap-1 text-xs'
|
||||
onClick={() => toggleExpand(false)}
|
||||
>
|
||||
<ChevronUp className='h-3 w-3' />
|
||||
{t('Collapse')}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{filteredModels.length === 0 && (
|
||||
<p className='text-muted-foreground py-4 text-center text-sm'>
|
||||
{t('No models available in this category')}
|
||||
</p>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
-5
@@ -5,7 +5,6 @@ import {
|
||||
CardStaggerContainer,
|
||||
CardStaggerItem,
|
||||
} from '@/components/page-transition'
|
||||
import { AvailableModelsCard } from './components/available-models-card'
|
||||
import { CheckinCalendarCard } from './components/checkin-calendar-card'
|
||||
import { PasskeyCard } from './components/passkey-card'
|
||||
import { ProfileHeader } from './components/profile-header'
|
||||
@@ -37,10 +36,6 @@ export function Profile() {
|
||||
<ProfileHeader profile={profile} loading={loading} />
|
||||
</CardStaggerItem>
|
||||
|
||||
<CardStaggerItem>
|
||||
<AvailableModelsCard />
|
||||
</CardStaggerItem>
|
||||
|
||||
<CardStaggerItem>
|
||||
<div className='grid gap-6 lg:grid-cols-2 lg:items-start'>
|
||||
<div className='space-y-6'>
|
||||
|
||||
+109
-81
@@ -33,6 +33,7 @@ import {
|
||||
getTimeColor,
|
||||
formatModelName,
|
||||
getTieredBillingSummary,
|
||||
hasAnyCacheTokens,
|
||||
parseLogOther,
|
||||
isViolationFeeLog,
|
||||
} from '../../lib/format'
|
||||
@@ -45,7 +46,6 @@ import {
|
||||
import type { LogOtherData } from '../../types'
|
||||
import { DetailsDialog } from '../dialogs/details-dialog'
|
||||
import { useUsageLogsContext } from '../usage-logs-provider'
|
||||
import { CacheTooltip } from './column-helpers'
|
||||
|
||||
interface DetailSegment {
|
||||
text: string
|
||||
@@ -90,33 +90,60 @@ function buildDetailSegments(
|
||||
|
||||
const segments: DetailSegment[] = []
|
||||
|
||||
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)) {
|
||||
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 tieredSummary = getTieredBillingSummary(other)
|
||||
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: `${ratioLabel} ${formatRatioCompact(effectiveRatio)}x`,
|
||||
text: `${tierLabel} · ${formatPriceList(baseEntries, true)}`,
|
||||
})
|
||||
}
|
||||
|
||||
const priceOpts = { digitsLarge: 4, digitsSmall: 6, abbreviate: false }
|
||||
const tieredSummary = getTieredBillingSummary(other)
|
||||
if (tieredSummary) {
|
||||
if (tieredSummary.tier.label) {
|
||||
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('Tier')} ${tieredSummary.tier.label}`,
|
||||
text: `${t('Cache')} ${formatPriceList(cacheEntries, false)}`,
|
||||
muted: true,
|
||||
})
|
||||
}
|
||||
for (const entry of tieredSummary.priceEntries) {
|
||||
|
||||
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: `${t(entry.shortLabel)} ${formatBillingCurrencyFromUSD(entry.price, priceOpts)}/M`,
|
||||
text: otherEntries.join(' · '),
|
||||
muted: true,
|
||||
})
|
||||
}
|
||||
@@ -124,17 +151,61 @@ function buildDetailSegments(
|
||||
const isPerCall = isPerCallBilling(other.model_price)
|
||||
if (isPerCall) {
|
||||
segments.push({
|
||||
text: `${t('Model Price')} ${formatBillingCurrencyFromUSD(other.model_price!, priceOpts)}`,
|
||||
muted: true,
|
||||
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('Input')} ${formatBillingCurrencyFromUSD(inputPriceUSD, priceOpts)}/M`,
|
||||
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({
|
||||
@@ -482,7 +553,7 @@ export function useCommonLogsColumns(isAdmin: boolean): ColumnDef<UsageLog>[] {
|
||||
{
|
||||
accessorKey: 'prompt_tokens',
|
||||
header: ({ column }) => (
|
||||
<DataTableColumnHeader column={column} title={t('Input')} />
|
||||
<DataTableColumnHeader column={column} title='Tokens' />
|
||||
),
|
||||
cell: ({ row }) => {
|
||||
const log = row.original
|
||||
@@ -494,84 +565,41 @@ export function useCommonLogsColumns(isAdmin: boolean): ColumnDef<UsageLog>[] {
|
||||
}
|
||||
|
||||
const promptTokens = log.prompt_tokens || 0
|
||||
if (promptTokens === 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
|
||||
|
||||
return (
|
||||
<div className='flex flex-col gap-0.5'>
|
||||
<span className='font-mono text-xs font-medium'>
|
||||
{promptTokens.toLocaleString()}
|
||||
</span>
|
||||
{cacheReadTokens > 0 && (
|
||||
<span className='flex items-center gap-1 text-[11px]'>
|
||||
<CacheTooltip
|
||||
tokens={cacheReadTokens}
|
||||
label={t('Cache Read')}
|
||||
color='fill-amber-500 text-amber-500'
|
||||
/>
|
||||
<span className='text-muted-foreground/60'>
|
||||
{t('Cache Read')} {cacheReadTokens.toLocaleString()}
|
||||
</span>
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
},
|
||||
meta: { label: t('Input'), mobileHidden: true },
|
||||
},
|
||||
|
||||
{
|
||||
accessorKey: 'completion_tokens',
|
||||
header: ({ column }) => (
|
||||
<DataTableColumnHeader column={column} title={t('Output')} />
|
||||
),
|
||||
cell: ({ row }) => {
|
||||
const log = row.original
|
||||
if (!isDisplayableLogType(log.type)) return null
|
||||
|
||||
const other = parseLogOther(log.other)
|
||||
if (isPerCallBilling(other?.model_price)) {
|
||||
return <span className='text-muted-foreground text-xs'>-</span>
|
||||
}
|
||||
|
||||
const completionTokens = log.completion_tokens || 0
|
||||
if (completionTokens === 0) {
|
||||
return <span className='text-muted-foreground text-xs'>-</span>
|
||||
}
|
||||
|
||||
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
|
||||
const cacheSegments = [
|
||||
cacheReadTokens > 0
|
||||
? `${t('Cache')}读 ${cacheReadTokens.toLocaleString()}`
|
||||
: null,
|
||||
cacheWriteTokens > 0
|
||||
? `写 ${cacheWriteTokens.toLocaleString()}`
|
||||
: null,
|
||||
].filter(Boolean)
|
||||
|
||||
return (
|
||||
<div className='flex flex-col gap-0.5'>
|
||||
<span className='font-mono text-xs font-medium'>
|
||||
{completionTokens.toLocaleString()}
|
||||
</span>
|
||||
{cacheWriteTokens > 0 && (
|
||||
<span className='flex items-center gap-1 text-[11px]'>
|
||||
<CacheTooltip
|
||||
tokens={cacheWriteTokens}
|
||||
label={t('Cache Write')}
|
||||
color='fill-blue-500 text-blue-500'
|
||||
/>
|
||||
<span className='text-muted-foreground/60'>
|
||||
{hasSplitCache
|
||||
? `${t('Cache Write')} ${cacheWrite5m.toLocaleString()}/${cacheWrite1h.toLocaleString()}`
|
||||
: `${t('Cache Write')} ${cacheWriteTokens.toLocaleString()}`}
|
||||
{promptTokens.toLocaleString()} / {completionTokens.toLocaleString()}
|
||||
</span>
|
||||
{cacheSegments.length > 0 && (
|
||||
<span className='text-muted-foreground/60 text-[11px]'>
|
||||
{cacheSegments.join(' · ')}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
},
|
||||
meta: { label: t('Output'), mobileHidden: true },
|
||||
meta: { label: 'Tokens', mobileHidden: true },
|
||||
},
|
||||
|
||||
{
|
||||
|
||||
@@ -175,7 +175,7 @@ function BillingBreakdown(props: {
|
||||
})
|
||||
}
|
||||
|
||||
if (!tieredSummary && isClaude) {
|
||||
if (!tieredSummary && isClaude && hasAnyCacheTokens(other)) {
|
||||
if (other.cache_ratio != null && other.cache_ratio !== 1) {
|
||||
rows.push({
|
||||
label: t('Cache Read'),
|
||||
@@ -376,6 +376,11 @@ export function DetailsDialog(props: DetailsDialogProps) {
|
||||
const isTopup = props.log.type === 1
|
||||
const isManage = props.log.type === 3
|
||||
const isSubscription = other?.billing_source === 'subscription'
|
||||
const isTieredBilling =
|
||||
isConsume &&
|
||||
!isViolation &&
|
||||
other?.billing_mode === 'tiered_expr' &&
|
||||
!!other?.expr_b64
|
||||
const hasAudioTokens = other?.ws || other?.audio
|
||||
const showTiming = isTimingLogType(props.log.type)
|
||||
const showAdminIp =
|
||||
@@ -446,7 +451,12 @@ export function DetailsDialog(props: DetailsDialogProps) {
|
||||
|
||||
return (
|
||||
<Dialog open={props.open} onOpenChange={props.onOpenChange}>
|
||||
<DialogContent className='sm:max-w-lg'>
|
||||
<DialogContent
|
||||
className={cn(
|
||||
'min-w-0',
|
||||
isTieredBilling ? 'sm:max-w-4xl lg:max-w-5xl' : 'sm:max-w-lg'
|
||||
)}
|
||||
>
|
||||
<DialogHeader>
|
||||
<DialogTitle className='flex items-center gap-2 text-base'>
|
||||
{t('Log Details')}
|
||||
@@ -462,8 +472,8 @@ export function DetailsDialog(props: DetailsDialogProps) {
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<ScrollArea className='max-h-[70vh] pr-4'>
|
||||
<div className='space-y-3 py-1'>
|
||||
<ScrollArea className='max-h-[70vh] min-w-0 pr-4'>
|
||||
<div className='min-w-0 space-y-3 py-1'>
|
||||
{/* Overview section - key identifiers */}
|
||||
<div className='space-y-1.5'>
|
||||
{props.log.request_id && (
|
||||
@@ -797,11 +807,8 @@ export function DetailsDialog(props: DetailsDialogProps) {
|
||||
)}
|
||||
|
||||
{/* Tiered pricing breakdown (when billing_mode is tiered_expr) */}
|
||||
{isConsume &&
|
||||
!isViolation &&
|
||||
other?.billing_mode === 'tiered_expr' &&
|
||||
other?.expr_b64 && (
|
||||
<div className='bg-muted/30 rounded-md border px-3'>
|
||||
{isTieredBilling && other?.expr_b64 && (
|
||||
<div className='bg-muted/30 min-w-0 rounded-md border px-3'>
|
||||
<DynamicPricingBreakdown
|
||||
billingExpr={decodeBillingExprB64(other.expr_b64)}
|
||||
matchedTierLabel={other.matched_tier}
|
||||
|
||||
Reference in New Issue
Block a user