فهرست منبع

feat(ui): enhance ChannelsTable and CommonLogs components with improved UI elements

CaIon 1 ماه پیش
والد
کامیت
22ae14f0d7

+ 17 - 5
web/default/src/components/data-table/faceted-filter.tsx

@@ -28,6 +28,8 @@ type DataTableFacetedFilterProps<TData, TValue> = {
     label: string
     value: string
     icon?: React.ComponentType<{ className?: string }>
+    iconNode?: React.ReactNode
+    count?: number
   }[]
   /** Enable single select mode (only one option can be selected at a time) */
   singleSelect?: boolean
@@ -130,15 +132,25 @@ export function DataTableFacetedFilter<TData, TValue>({
                     >
                       <CheckIcon className={cn('text-background h-4 w-4')} />
                     </div>
-                    {option.icon && (
+                    {option.iconNode ? (
+                      <span className='text-muted-foreground flex size-4 items-center justify-center'>
+                        {option.iconNode}
+                      </span>
+                    ) : option.icon ? (
                       <option.icon className='text-muted-foreground size-4' />
-                    )}
-                    <span>{t(option.label)}</span>
-                    {facets?.get(option.value) && (
+                    ) : null}
+                    <span className='min-w-0 flex-1 truncate'>
+                      {t(option.label)}
+                    </span>
+                    {typeof option.count === 'number' ? (
+                      <span className='text-muted-foreground ms-auto flex h-4 min-w-4 items-center justify-center font-mono text-xs'>
+                        {option.count}
+                      </span>
+                    ) : facets?.get(option.value) ? (
                       <span className='ms-auto flex h-4 w-4 items-center justify-center font-mono text-xs'>
                         {facets.get(option.value)}
                       </span>
-                    )}
+                    ) : null}
                   </CommandItem>
                 )
               })}

+ 2 - 0
web/default/src/components/data-table/toolbar.tsx

@@ -20,6 +20,8 @@ type DataTableToolbarProps<TData> = {
       label: string
       value: string
       icon?: React.ComponentType<{ className?: string }>
+      iconNode?: React.ReactNode
+      count?: number
     }[]
     singleSelect?: boolean
   }[]

+ 58 - 14
web/default/src/features/channels/components/channels-table.tsx

@@ -13,6 +13,7 @@ import {
 } from '@tanstack/react-table'
 import { useDebounce, useMediaQuery } from '@/hooks'
 import { useTranslation } from 'react-i18next'
+import { getLobeIcon } from '@/lib/lobe-icon'
 import { cn } from '@/lib/utils'
 import { useTableUrlState } from '@/hooks/use-table-url-state'
 import { Input } from '@/components/ui/input'
@@ -37,12 +38,13 @@ import {
   DEFAULT_PAGE_SIZE,
   CHANNEL_STATUS,
   CHANNEL_STATUS_OPTIONS,
-  CHANNEL_TYPE_OPTIONS,
 } from '../constants'
 import {
   channelsQueryKeys,
   aggregateChannelsByTag,
   isTagAggregateRow,
+  getChannelTypeIcon,
+  getChannelTypeLabel,
 } from '../lib'
 import type { Channel } from '../types'
 import { useChannelsColumns } from './channels-columns'
@@ -52,7 +54,9 @@ import { DataTableBulkActions } from './data-table-bulk-actions'
 const route = getRouteApi('/_authenticated/channels/')
 
 function isDisabledChannelRow(channel: Channel) {
-  return !isTagAggregateRow(channel) && channel.status !== CHANNEL_STATUS.ENABLED
+  return (
+    !isTagAggregateRow(channel) && channel.status !== CHANNEL_STATUS.ENABLED
+  )
 }
 
 export function ChannelsTable() {
@@ -264,17 +268,57 @@ export function ChannelsTable() {
     ensurePageInRange(pageCount)
   }, [pageCount, ensurePageInRange])
 
-  // Prepare filter options (option.label are i18n keys; faceted-filter uses t(option.label))
-  const typeFilterOptions = [
-    {
-      label: `${t('All Types')}${typeCounts?.all ? ` (${typeCounts.all})` : ''}`,
-      value: 'all',
-    },
-    ...CHANNEL_TYPE_OPTIONS.map((option) => ({
-      label: `${t(option.label)}${typeCounts?.[option.value] ? ` (${typeCounts[option.value]})` : ''}`,
-      value: String(option.value),
-    })),
-  ]
+  // Prepare filter options from existing channel types only.
+  const typeFilterOptions = useMemo(() => {
+    const counts = typeCounts || {}
+    const typeIds = Object.entries(counts)
+      .map(([type, count]) => ({
+        type: Number(type),
+        count: Number(count) || 0,
+      }))
+      .filter((item) => item.type > 0 && item.count > 0)
+      .sort((a, b) => {
+        const labelA = t(getChannelTypeLabel(a.type))
+        const labelB = t(getChannelTypeLabel(b.type))
+        return labelA.localeCompare(labelB)
+      })
+
+    const selectedType = typeFilter.find((value) => value !== 'all')
+    if (selectedType) {
+      const selectedTypeId = Number(selectedType)
+      const alreadyIncluded = typeIds.some(
+        (item) => item.type === selectedTypeId
+      )
+      if (selectedTypeId > 0 && !alreadyIncluded) {
+        typeIds.push({
+          type: selectedTypeId,
+          count: Number(counts[selectedType]) || 0,
+        })
+      }
+    }
+
+    const totalTypes = Object.values(counts).reduce(
+      (sum, count) => sum + (Number(count) || 0),
+      0
+    )
+
+    return [
+      {
+        label: 'All Types',
+        value: 'all',
+        count: totalTypes,
+      },
+      ...typeIds.map((item) => {
+        const iconName = getChannelTypeIcon(item.type)
+        return {
+          label: getChannelTypeLabel(item.type),
+          value: String(item.type),
+          count: item.count,
+          iconNode: getLobeIcon(`${iconName}.Color`, 16),
+        }
+      }),
+    ]
+  }, [t, typeCounts, typeFilter])
 
   const groupFilterOptions = [
     { label: t('All Groups'), value: 'all' },
@@ -375,7 +419,7 @@ export function ChannelsTable() {
                         data-state={row.getIsSelected() && 'selected'}
                         className={cn(
                           isDisabledChannelRow(row.original) &&
-                            'bg-muted/85 hover:bg-muted dark:bg-zinc-700/55 dark:hover:bg-zinc-700/70 [&>td:first-child]:border-l-4 [&>td:first-child]:border-l-muted-foreground/35 [&>td:first-child]:pl-1 dark:[&>td:first-child]:border-l-zinc-300/70'
+                            'bg-muted/85 hover:bg-muted [&>td:first-child]:border-l-muted-foreground/35 dark:bg-zinc-700/55 dark:hover:bg-zinc-700/70 [&>td:first-child]:border-l-4 [&>td:first-child]:pl-1 dark:[&>td:first-child]:border-l-zinc-300/70'
                         )}
                       >
                         {row.getVisibleCells().map((cell) => (

+ 83 - 72
web/default/src/features/profile/components/profile-header.tsx

@@ -1,3 +1,4 @@
+import { Activity, BarChart3, WalletCards } from 'lucide-react'
 import { useTranslation } from 'react-i18next'
 import { formatCompactNumber, formatQuota } from '@/lib/format'
 import { getRoleLabel } from '@/lib/roles'
@@ -21,33 +22,34 @@ export function ProfileHeader({ profile, loading }: ProfileHeaderProps) {
 
   if (loading) {
     return (
-      <div className='bg-card overflow-hidden rounded-2xl border'>
-        <div className='p-5 sm:p-6'>
-          <div className='flex flex-col gap-5 lg:flex-row lg:items-center lg:justify-between'>
-            <div className='flex flex-col items-center gap-4 text-center sm:flex-row sm:text-left'>
-              <Skeleton className='h-20 w-20 rounded-2xl' />
-              <div className='space-y-3'>
-                <div className='flex flex-col items-center gap-2 sm:flex-row sm:justify-start'>
-                  <Skeleton className='h-8 w-48' />
-                  <Skeleton className='h-5 w-16' />
-                </div>
-                <div className='flex flex-col items-center gap-1 sm:flex-row sm:justify-start sm:gap-4'>
-                  <Skeleton className='h-4 w-24' />
-                  <Skeleton className='h-4 w-40' />
-                  <Skeleton className='h-4 w-20' />
-                </div>
+      <div className='bg-card overflow-hidden rounded-lg border'>
+        <div className='p-4 sm:p-5'>
+          <div className='flex flex-col items-center gap-4 text-center sm:flex-row sm:text-left'>
+            <Skeleton className='h-16 w-16 rounded-2xl' />
+            <div className='space-y-3'>
+              <div className='flex flex-col items-center gap-2 sm:flex-row sm:justify-start'>
+                <Skeleton className='h-8 w-48' />
+                <Skeleton className='h-5 w-16' />
+              </div>
+              <div className='flex flex-col items-center gap-1 sm:flex-row sm:justify-start sm:gap-4'>
+                <Skeleton className='h-4 w-24' />
+                <Skeleton className='h-4 w-40' />
+                <Skeleton className='h-4 w-20' />
               </div>
-            </div>
-            <div className='grid gap-3 sm:grid-cols-3 lg:w-[480px]'>
-              {Array.from({ length: 3 }).map((_, i) => (
-                <div key={i} className='rounded-xl border p-4'>
-                  <Skeleton className='mb-3 h-3 w-20' />
-                  <Skeleton className='h-7 w-24' />
-                </div>
-              ))}
             </div>
           </div>
         </div>
+        <div className='border-t'>
+          <div className='divide-border/60 grid grid-cols-1 divide-y sm:grid-cols-3 sm:divide-x sm:divide-y-0'>
+            {Array.from({ length: 3 }).map((_, i) => (
+              <div key={i} className='px-4 py-3.5 sm:px-5 sm:py-4'>
+                <Skeleton className='h-3.5 w-20' />
+                <Skeleton className='mt-2 h-7 w-28' />
+                <Skeleton className='mt-1.5 h-3.5 w-24' />
+              </div>
+            ))}
+          </div>
+        </div>
       </div>
     )
   }
@@ -61,73 +63,82 @@ export function ProfileHeader({ profile, loading }: ProfileHeaderProps) {
     {
       label: t('Current Balance'),
       value: formatQuota(profile.quota),
+      description: t('Remaining quota'),
+      icon: WalletCards,
     },
     {
       label: t('Total Usage'),
       value: formatQuota(profile.used_quota),
+      description: t('Total consumed quota'),
+      icon: BarChart3,
     },
     {
       label: t('API Requests'),
       value: formatCompactNumber(profile.request_count),
+      description: t('Total requests made'),
+      icon: Activity,
     },
   ]
 
   return (
-    <div className='bg-card relative overflow-hidden rounded-2xl border'>
-      <div className='relative p-5 sm:p-6'>
-        <div className='flex flex-col gap-5 lg:flex-row lg:items-center lg:justify-between'>
-          <div className='flex flex-col items-center gap-4 text-center sm:flex-row sm:text-left'>
-            <Avatar className='ring-background h-20 w-20 rounded-2xl text-xl ring-4'>
-              <AvatarFallback className='bg-primary/10 text-primary rounded-2xl'>
-                {initials}
-              </AvatarFallback>
-            </Avatar>
+    <div className='bg-card overflow-hidden rounded-lg border'>
+      <div className='p-4 sm:p-5'>
+        <div className='flex flex-col items-center gap-4 text-center sm:flex-row sm:text-left'>
+          <Avatar className='ring-background h-16 w-16 rounded-2xl text-lg ring-4'>
+            <AvatarFallback className='bg-primary/10 text-primary rounded-2xl'>
+              {initials}
+            </AvatarFallback>
+          </Avatar>
 
-            <div className='min-w-0 flex-1 space-y-3'>
-              <div className='flex flex-col items-center gap-2 sm:flex-row sm:justify-start'>
-                <h1 className='text-2xl font-semibold tracking-tight sm:text-3xl'>
-                  {displayName}
-                </h1>
-                <StatusBadge
-                  label={roleLabel}
-                  variant='neutral'
-                  copyable={false}
-                />
-              </div>
+          <div className='min-w-0 flex-1 space-y-3'>
+            <div className='flex flex-col items-center gap-2 sm:flex-row sm:justify-start'>
+              <h1 className='text-2xl font-semibold tracking-tight'>
+                {displayName}
+              </h1>
+              <StatusBadge
+                label={roleLabel}
+                variant='neutral'
+                copyable={false}
+              />
+            </div>
 
-              <div className='text-muted-foreground flex flex-col gap-1 text-sm sm:flex-row sm:flex-wrap sm:justify-start sm:gap-4'>
-                <span>@{profile.username}</span>
-                {profile.email && (
-                  <>
-                    <span className='hidden sm:inline'>•</span>
-                    <span>{profile.email}</span>
-                  </>
-                )}
-                {profile.group && (
-                  <>
-                    <span className='hidden sm:inline'>•</span>
-                    <span>{profile.group}</span>
-                  </>
-                )}
-              </div>
+            <div className='text-muted-foreground flex flex-col gap-1 text-sm sm:flex-row sm:flex-wrap sm:justify-start sm:gap-4'>
+              <span>@{profile.username}</span>
+              {profile.email && (
+                <>
+                  <span className='hidden sm:inline'>•</span>
+                  <span>{profile.email}</span>
+                </>
+              )}
+              {profile.group && (
+                <>
+                  <span className='hidden sm:inline'>•</span>
+                  <span>{profile.group}</span>
+                </>
+              )}
             </div>
           </div>
-
-          <div className='grid gap-3 sm:grid-cols-3 lg:w-[480px]'>
-            {stats.map((item) => (
-              <div
-                key={item.label}
-                className='bg-background/70 rounded-xl border p-4 backdrop-blur'
-              >
-                <p className='text-muted-foreground text-xs font-medium'>
+        </div>
+      </div>
+      <div className='border-t'>
+        <div className='divide-border/60 grid grid-cols-1 divide-y sm:grid-cols-3 sm:divide-x sm:divide-y-0'>
+          {stats.map((item) => (
+            <div key={item.label} className='px-4 py-3.5 sm:px-5 sm:py-4'>
+              <div className='flex items-center gap-2'>
+                <item.icon className='text-muted-foreground/60 size-3.5 shrink-0' />
+                <div className='text-muted-foreground truncate text-xs font-medium tracking-wider uppercase'>
                   {item.label}
-                </p>
-                <p className='mt-2 truncate text-xl font-semibold tracking-tight'>
-                  {item.value}
-                </p>
+                </div>
               </div>
-            ))}
-          </div>
+
+              <div className='text-foreground mt-2 font-mono text-2xl font-bold tracking-tight break-all tabular-nums'>
+                {item.value}
+              </div>
+              <div className='text-muted-foreground/60 mt-1 hidden text-xs md:block'>
+                {item.description}
+              </div>
+            </div>
+          ))}
         </div>
       </div>
     </div>

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

@@ -1,6 +1,6 @@
 import { useState } from 'react'
 import { type ColumnDef } from '@tanstack/react-table'
-import { Route, CircleAlert, Sparkles, KeyRound } from 'lucide-react'
+import { CircleAlert, Sparkles, KeyRound } from 'lucide-react'
 import { useTranslation } from 'react-i18next'
 import { formatBillingCurrencyFromUSD } from '@/lib/currency'
 import {
@@ -10,11 +10,6 @@ import {
 } from '@/lib/format'
 import { cn } from '@/lib/utils'
 import { Avatar, AvatarFallback } from '@/components/ui/avatar'
-import {
-  Popover,
-  PopoverContent,
-  PopoverTrigger,
-} from '@/components/ui/popover'
 import {
   Tooltip,
   TooltipContent,
@@ -25,8 +20,6 @@ import { DataTableColumnHeader } from '@/components/data-table'
 import {
   StatusBadge,
   type StatusBadgeProps,
-  dotColorMap,
-  textColorMap,
 } from '@/components/status-badge'
 import type { UsageLog } from '../../data/schema'
 import {
@@ -47,6 +40,7 @@ import {
 } 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 {
@@ -445,10 +439,20 @@ export function useCommonLogsColumns(isAdmin: boolean): ColumnDef<UsageLog>[] {
       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='max-w-[120px]'>
+        <div className='flex max-w-[150px] flex-col gap-0.5'>
           <StatusBadge
             label={displayName}
             icon={KeyRound}
@@ -457,6 +461,11 @@ export function useCommonLogsColumns(isAdmin: boolean): ColumnDef<UsageLog>[] {
             showDot={false}
             className='max-w-full overflow-hidden rounded-md border border-border/60 bg-muted/30 px-1.5 py-0.5 font-mono text-foreground'
           />
+          {metaParts.length > 0 && (
+            <span className='text-muted-foreground/60 truncate text-[11px]'>
+              {metaParts.join(' · ')}
+            </span>
+          )}
         </div>
       )
     },
@@ -471,81 +480,17 @@ export function useCommonLogsColumns(isAdmin: boolean): ColumnDef<UsageLog>[] {
         <DataTableColumnHeader column={column} title={t('Model')} />
       ),
       cell: function ModelCell({ row }) {
-        const { sensitiveVisible } = useUsageLogsContext()
         const log = row.original
         if (!isDisplayableLogType(log.type)) return null
 
         const modelInfo = formatModelName(log)
-        const other = parseLogOther(log.other)
-        let group = log.group
-        if (!group) group = other?.group || ''
-
-        const badgeClass =
-          'truncate rounded-md border border-border/60 bg-muted/30 px-1.5 py-0.5 font-mono'
-
-        const modelBadge = modelInfo.isMapped ? (
-          <Popover>
-            <PopoverTrigger asChild>
-              <button
-                type='button'
-                className='inline-flex items-center gap-1'
-              >
-                <StatusBadge
-                  label={modelInfo.name}
-                  autoColor={modelInfo.name}
-                  copyText={modelInfo.name}
-                  size='sm'
-                  className={badgeClass}
-                />
-                <Route className='text-muted-foreground size-3 shrink-0' />
-              </button>
-            </PopoverTrigger>
-            <PopoverContent className='w-72'>
-              <div className='space-y-2'>
-                <div className='flex items-start justify-between gap-3'>
-                  <span className='text-muted-foreground text-xs'>
-                    {t('Request Model:')}
-                  </span>
-                  <span className='truncate font-mono text-xs font-medium'>
-                    {modelInfo.name}
-                  </span>
-                </div>
-                <div className='flex items-start justify-between gap-3'>
-                  <span className='text-muted-foreground text-xs'>
-                    {t('Actual Model:')}
-                  </span>
-                  <span className='truncate font-mono text-xs font-medium'>
-                    {modelInfo.actualModel}
-                  </span>
-                </div>
-              </div>
-            </PopoverContent>
-          </Popover>
-        ) : (
-          <StatusBadge
-            label={modelInfo.name}
-            autoColor={modelInfo.name}
-            copyText={modelInfo.name}
-            size='sm'
-            className={badgeClass}
-          />
-        )
-
-        const metaParts: string[] = []
-        const groupRatioText = getGroupRatioText(other)
-        if (group) {
-          metaParts.push(sensitiveVisible ? group : '••••')
-        }
-        if (groupRatioText) metaParts.push(groupRatioText)
 
         return (
           <div className='flex max-w-[220px] flex-col gap-0.5'>
-            {modelBadge}
-            {metaParts.length > 0 && (
-              <span className='text-muted-foreground/60 truncate text-[11px]'>
-                {metaParts.join(' · ')}
-              </span>
-            )}
+            <ModelBadge
+              modelName={modelInfo.name}
+              actualModel={modelInfo.actualModel}
+            />
           </div>
         )
       },
@@ -576,11 +521,21 @@ export function useCommonLogsColumns(isAdmin: boolean): ColumnDef<UsageLog>[] {
 
         const pillBg: Record<string, string> = {
           success:
-            'border border-emerald-200/60 bg-emerald-50/50 dark:border-emerald-800/50 dark:bg-emerald-950/20',
+            'border border-emerald-200/40 bg-emerald-50/35 dark:border-emerald-900/40 dark:bg-emerald-950/15',
           warning:
-            'border border-amber-200/60 bg-amber-50/50 dark:border-amber-800/50 dark:bg-amber-950/20',
+            'border border-amber-200/45 bg-amber-50/35 dark:border-amber-900/40 dark:bg-amber-950/15',
           danger:
-            'border border-rose-200/70 bg-rose-50/60 dark:border-rose-800/50 dark:bg-rose-950/25',
+            'border border-rose-200/50 bg-rose-50/35 dark:border-rose-900/40 dark:bg-rose-950/15',
+        }
+        const pillText: Record<string, string> = {
+          success: 'text-emerald-700/85 dark:text-emerald-400/85',
+          warning: 'text-amber-700/85 dark:text-amber-400/85',
+          danger: 'text-rose-700/85 dark:text-rose-400/85',
+        }
+        const pillDot: Record<string, string> = {
+          success: 'bg-emerald-500/80',
+          warning: 'bg-amber-500/80',
+          danger: 'bg-rose-500/80',
         }
 
         return (
@@ -590,13 +545,13 @@ export function useCommonLogsColumns(isAdmin: boolean): ColumnDef<UsageLog>[] {
                 className={cn(
                   'inline-flex items-center gap-1 rounded-md px-1.5 py-0.5 font-mono text-xs font-medium',
                   pillBg[timeVariant],
-                  textColorMap[timeVariant]
+                  pillText[timeVariant]
                 )}
               >
                 <span
                   className={cn(
                     'size-1.5 shrink-0 rounded-full',
-                    dotColorMap[timeVariant]
+                    pillDot[timeVariant]
                   )}
                   aria-hidden='true'
                 />
@@ -607,7 +562,7 @@ export function useCommonLogsColumns(isAdmin: boolean): ColumnDef<UsageLog>[] {
                   className={cn(
                     'inline-flex items-center rounded-md px-1.5 py-0.5 font-mono text-xs font-medium',
                     pillBg[frtVariant!],
-                    textColorMap[frtVariant!]
+                    pillText[frtVariant!]
                   )}
                 >
                   {formatUseTime(frt / 1000)}

+ 31 - 28
web/default/src/features/usage-logs/components/common-logs-stats.tsx

@@ -12,6 +12,22 @@ import { useUsageLogsContext } from './usage-logs-provider'
 
 const route = getRouteApi('/_authenticated/usage-logs/$section')
 
+function StatBadge(props: {
+  label: string
+  value: string | number
+  accent: string
+}) {
+  return (
+    <span className='inline-flex h-7 items-center gap-2 rounded-md border border-border/60 bg-muted/25 px-2.5 text-xs shadow-xs'>
+      <span className={cn('h-3.5 w-0.5 rounded-full', props.accent)} />
+      <span className='text-muted-foreground'>{props.label}</span>
+      <span className='font-mono font-semibold tabular-nums text-foreground/85'>
+        {props.value}
+      </span>
+    </span>
+  )
+}
+
 export function CommonLogsStats() {
   const { t } = useTranslation()
   const isAdmin = useIsAdmin()
@@ -50,36 +66,23 @@ export function CommonLogsStats() {
     )
   }
 
-  const tagClass =
-    'inline-flex h-7 items-center rounded-md border px-3 py-1 text-xs font-medium shadow-xs'
-
   return (
     <div className='flex flex-wrap items-center gap-2'>
-      <span
-        className={cn(
-          tagClass,
-          'border-blue-200/70 bg-blue-50 text-blue-700 dark:border-blue-500/20 dark:bg-blue-500/10 dark:text-blue-300'
-        )}
-      >
-        {t('Usage')}:{' '}
-        {sensitiveVisible ? formatLogQuota(stats?.quota || 0) : '••••'}
-      </span>
-      <span
-        className={cn(
-          tagClass,
-          'border-pink-200/70 bg-pink-50 text-pink-700 dark:border-pink-500/20 dark:bg-pink-500/10 dark:text-pink-300'
-        )}
-      >
-        {t('RPM')}: {stats?.rpm || 0}
-      </span>
-      <span
-        className={cn(
-          tagClass,
-          'border-border bg-background text-muted-foreground'
-        )}
-      >
-        {t('TPM')}: {stats?.tpm || 0}
-      </span>
+      <StatBadge
+        label={t('Usage')}
+        value={sensitiveVisible ? formatLogQuota(stats?.quota || 0) : '••••'}
+        accent='bg-sky-500/70'
+      />
+      <StatBadge
+        label={t('RPM')}
+        value={stats?.rpm || 0}
+        accent='bg-rose-500/65'
+      />
+      <StatBadge
+        label={t('TPM')}
+        value={stats?.tpm || 0}
+        accent='bg-slate-400/70'
+      />
     </div>
   )
 }

+ 144 - 0
web/default/src/features/usage-logs/components/model-badge.tsx

@@ -0,0 +1,144 @@
+import { Route } from 'lucide-react'
+import { useTranslation } from 'react-i18next'
+import { getLobeIcon } from '@/lib/lobe-icon'
+import { cn } from '@/lib/utils'
+import {
+  Popover,
+  PopoverContent,
+  PopoverTrigger,
+} from '@/components/ui/popover'
+import { StatusBadge } from '@/components/status-badge'
+
+interface ModelBadgeProps {
+  modelName: string
+  actualModel?: string
+  className?: string
+}
+
+interface ModelProvider {
+  icon: string
+  label: string
+}
+
+function resolveModelProvider(modelName: string): ModelProvider | null {
+  const model = modelName.toLowerCase()
+  const hasAny = (keywords: string[]) =>
+    keywords.some((keyword) => model.includes(keyword))
+
+  if (
+    hasAny([
+      'gpt-',
+      'chatgpt-',
+      'text-embedding-',
+      'omni-moderation',
+      'dall-e',
+      'whisper',
+      'tts-',
+    ]) ||
+    /\bo[134](?:-|$)/.test(model)
+  ) {
+    return { icon: 'OpenAI.Color', label: 'OpenAI' }
+  }
+  if (hasAny(['claude-', 'anthropic'])) {
+    return { icon: 'Claude.Color', label: 'Claude' }
+  }
+  if (hasAny(['gemini-', 'learnlm-'])) {
+    return { icon: 'Gemini.Color', label: 'Gemini' }
+  }
+  if (hasAny(['grok-', 'xai-'])) {
+    return { icon: 'Grok.Color', label: 'Grok' }
+  }
+  if (hasAny(['deepseek-'])) {
+    return { icon: 'DeepSeek.Color', label: 'DeepSeek' }
+  }
+  if (hasAny(['qwen', 'qwq-'])) {
+    return { icon: 'Qwen.Color', label: 'Qwen' }
+  }
+  if (hasAny(['doubao-', 'volcengine'])) {
+    return { icon: 'Doubao.Color', label: 'Doubao' }
+  }
+  if (hasAny(['moonshot-', 'kimi-'])) {
+    return { icon: 'Moonshot.Color', label: 'Moonshot' }
+  }
+  if (hasAny(['mistral-', 'mixtral-'])) {
+    return { icon: 'Mistral.Color', label: 'Mistral' }
+  }
+  if (hasAny(['llama-', 'meta-'])) {
+    return { icon: 'Meta.Color', label: 'Meta' }
+  }
+  if (hasAny(['command-', 'cohere-'])) {
+    return { icon: 'Cohere.Color', label: 'Cohere' }
+  }
+
+  return null
+}
+
+function ModelBadgeContent(props: ModelBadgeProps) {
+  const provider = resolveModelProvider(props.modelName)
+
+  return (
+    <StatusBadge
+      copyText={props.modelName}
+      size='sm'
+      showDot={!provider}
+      autoColor={provider ? undefined : props.modelName}
+      className={cn(
+        'rounded-md border border-border/60 bg-muted/30 px-1.5 py-0.5 font-mono',
+        provider && 'text-foreground',
+        props.className
+      )}
+    >
+      <span className='flex items-center gap-1.5'>
+        {provider && (
+          <span
+            className='flex size-3.5 shrink-0 items-center justify-center'
+            title={provider.label}
+            aria-label={provider.label}
+          >
+            {getLobeIcon(provider.icon, 14)}
+          </span>
+        )}
+        <span>{props.modelName}</span>
+      </span>
+    </StatusBadge>
+  )
+}
+
+export function ModelBadge(props: ModelBadgeProps) {
+  const { t } = useTranslation()
+
+  if (!props.actualModel) {
+    return <ModelBadgeContent {...props} />
+  }
+
+  return (
+    <Popover>
+      <PopoverTrigger asChild>
+        <button type='button' className='inline-flex items-center gap-1'>
+          <ModelBadgeContent {...props} />
+          <Route className='text-muted-foreground size-3 shrink-0' />
+        </button>
+      </PopoverTrigger>
+      <PopoverContent className='w-72'>
+        <div className='space-y-2'>
+          <div className='flex items-start justify-between gap-3'>
+            <span className='text-muted-foreground text-xs'>
+              {t('Request Model:')}
+            </span>
+            <span className='truncate font-mono text-xs font-medium'>
+              {props.modelName}
+            </span>
+          </div>
+          <div className='flex items-start justify-between gap-3'>
+            <span className='text-muted-foreground text-xs'>
+              {t('Actual Model:')}
+            </span>
+            <span className='truncate font-mono text-xs font-medium'>
+              {props.actualModel}
+            </span>
+          </div>
+        </div>
+      </PopoverContent>
+    </Popover>
+  )
+}

+ 33 - 45
web/default/src/features/wallet/components/wallet-stats-card.tsx

@@ -1,8 +1,6 @@
 import { Activity, BarChart3, WalletCards } from 'lucide-react'
 import { useTranslation } from 'react-i18next'
 import { formatQuota } from '@/lib/format'
-import { cn } from '@/lib/utils'
-import { Card, CardContent } from '@/components/ui/card'
 import { Skeleton } from '@/components/ui/skeleton'
 import type { UserWalletData } from '../types'
 
@@ -15,26 +13,17 @@ export function WalletStatsCard(props: WalletStatsCardProps) {
   const { t } = useTranslation()
   if (props.loading) {
     return (
-      <Card className='overflow-hidden'>
-        <CardContent className='p-0'>
-          <div className='grid grid-cols-1 sm:grid-cols-3'>
-            {Array.from({ length: 3 }).map((_, i) => (
-              <div
-                key={i}
-                className={cn(
-                  'flex items-center justify-center px-4 py-3 sm:px-5 sm:py-4',
-                  i > 0 && 'border-t sm:border-t-0 sm:border-l'
-                )}
-              >
-                <div className='w-full max-w-44'>
-                  <Skeleton className='h-4 w-24' />
-                  <Skeleton className='mt-2 h-7 w-32' />
-                </div>
-              </div>
-            ))}
-          </div>
-        </CardContent>
-      </Card>
+      <div className='overflow-hidden rounded-lg border'>
+        <div className='divide-border/60 grid grid-cols-1 divide-y sm:grid-cols-3 sm:divide-x sm:divide-y-0'>
+          {Array.from({ length: 3 }).map((_, i) => (
+            <div key={i} className='px-4 py-3.5 sm:px-5 sm:py-4'>
+              <Skeleton className='h-3.5 w-20' />
+              <Skeleton className='mt-2 h-7 w-28' />
+              <Skeleton className='mt-1.5 h-3.5 w-24' />
+            </div>
+          ))}
+        </div>
+      </div>
     )
   }
 
@@ -42,45 +31,44 @@ export function WalletStatsCard(props: WalletStatsCardProps) {
     {
       label: t('Current Balance'),
       value: formatQuota(props.user?.quota ?? 0),
+      description: t('Remaining quota'),
       icon: WalletCards,
     },
     {
       label: t('Total Usage'),
       value: formatQuota(props.user?.used_quota ?? 0),
+      description: t('Total consumed quota'),
       icon: BarChart3,
     },
     {
       label: t('API Requests'),
       value: (props.user?.request_count ?? 0).toLocaleString(),
+      description: t('Total requests made'),
       icon: Activity,
     },
   ]
 
   return (
-    <Card className='overflow-hidden'>
-      <CardContent className='p-0'>
-        <div className='grid grid-cols-1 sm:grid-cols-3'>
-          {stats.map((item, index) => (
-            <div
-              key={item.label}
-              className={cn(
-                'flex min-w-0 justify-center px-4 py-3 sm:px-5 sm:py-4',
-                index > 0 && 'border-t sm:border-t-0 sm:border-l'
-              )}
-            >
-              <div className='min-w-0 text-center'>
-                <div className='text-muted-foreground flex items-center justify-center gap-1.5 text-xs font-medium'>
-                  <item.icon className='h-3.5 w-3.5' />
-                  {item.label}
-                </div>
-                <div className='mt-1 text-xl leading-tight font-semibold tracking-tight break-all lg:text-2xl'>
-                  {item.value}
-                </div>
+    <div className='overflow-hidden rounded-lg border'>
+      <div className='divide-border/60 grid grid-cols-1 divide-y sm:grid-cols-3 sm:divide-x sm:divide-y-0'>
+        {stats.map((item) => (
+          <div key={item.label} className='px-4 py-3.5 sm:px-5 sm:py-4'>
+            <div className='flex items-center gap-2'>
+              <item.icon className='text-muted-foreground/60 size-3.5 shrink-0' />
+              <div className='text-muted-foreground truncate text-xs font-medium tracking-wider uppercase'>
+                {item.label}
               </div>
             </div>
-          ))}
-        </div>
-      </CardContent>
-    </Card>
+
+            <div className='text-foreground mt-2 font-mono text-2xl font-bold tracking-tight break-all tabular-nums'>
+              {item.value}
+            </div>
+            <div className='text-muted-foreground/60 mt-1 hidden text-xs md:block'>
+              {item.description}
+            </div>
+          </div>
+        ))}
+      </div>
+    </div>
   )
 }