CaIon 1 месяц назад
Родитель
Сommit
fc377dae3e

+ 3 - 3
web/default/src/features/pricing/components/dynamic-pricing-breakdown.tsx

@@ -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'>

+ 0 - 234
web/default/src/features/profile/components/available-models-card.tsx

@@ -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>
-  )
-}

+ 0 - 5
web/default/src/features/profile/index.tsx

@@ -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'>

+ 111 - 83
web/default/src/features/usage-logs/components/columns/common-logs-columns.tsx

@@ -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)) {
-    segments.push({
-      text: `${ratioLabel} ${formatRatioCompact(effectiveRatio)}x`,
-    })
-  }
-
   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) {
-    if (tieredSummary.tier.label) {
+    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: `${t('Tier')} ${tieredSummary.tier.label}`,
+        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,
       })
     }
-    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,15 +151,59 @@ 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`,
-        muted: true,
+        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`,
+        })
+      }
     }
   }
 
@@ -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) {
-          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) {
+        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
+        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()}
+              {promptTokens.toLocaleString()} / {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()}`}
-                </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 },
     },
 
     {

+ 23 - 16
web/default/src/features/usage-logs/components/dialogs/details-dialog.tsx

@@ -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,18 +807,15 @@ 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'>
-                  <DynamicPricingBreakdown
-                    billingExpr={decodeBillingExprB64(other.expr_b64)}
-                    matchedTierLabel={other.matched_tier}
-                    hideCacheColumns={!hasAnyCacheTokens(other)}
-                  />
-                </div>
-              )}
+            {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}
+                  hideCacheColumns={!hasAnyCacheTokens(other)}
+                />
+              </div>
+            )}
 
             {/* Admin billing mode indicator for non-consume */}
             {props.isAdmin &&