From fc377dae3e3994dc4b076e678704e1c5ef7a5e90 Mon Sep 17 00:00:00 2001 From: CaIon Date: Tue, 28 Apr 2026 15:56:09 +0800 Subject: [PATCH] style: optimize UI --- .../components/dynamic-pricing-breakdown.tsx | 6 +- .../components/available-models-card.tsx | 234 ------------------ web/default/src/features/profile/index.tsx | 5 - .../columns/common-logs-columns.tsx | 194 ++++++++------- .../components/dialogs/details-dialog.tsx | 39 +-- 5 files changed, 137 insertions(+), 341 deletions(-) delete mode 100644 web/default/src/features/profile/components/available-models-card.tsx diff --git a/web/default/src/features/pricing/components/dynamic-pricing-breakdown.tsx b/web/default/src/features/pricing/components/dynamic-pricing-breakdown.tsx index 9498100c..90e6aeea 100644 --- a/web/default/src/features/pricing/components/dynamic-pricing-breakdown.tsx +++ b/web/default/src/features/pricing/components/dynamic-pricing-breakdown.tsx @@ -176,7 +176,7 @@ export function DynamicPricingBreakdown({ if (!hasTiers && !hasRules) { return ( -
+
@@ -201,7 +201,7 @@ export function DynamicPricingBreakdown({ }) return ( -
+
@@ -221,7 +221,7 @@ export function DynamicPricingBreakdown({
{t('Tiered price table')}
-
+
diff --git a/web/default/src/features/profile/components/available-models-card.tsx b/web/default/src/features/profile/components/available-models-card.tsx deleted file mode 100644 index 6c34ac8c..00000000 --- a/web/default/src/features/profile/components/available-models-card.tsx +++ /dev/null @@ -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 ( - - - - - {t('Available Models')} - - - {t('View all currently available models')} - - - -
- {Array.from({ length: 12 }).map((_, i) => ( - - ))} -
-
-
- ) - } - - if (models.length === 0) { - return ( - - - - - {t('Available Models')} - - - -

- {t('No available models')} -

-
-
- ) - } - - const needsExpand = filteredModels.length > MODELS_DISPLAY_COUNT - const displayModels = - needsExpand && !isExpanded - ? filteredModels.slice(0, MODELS_DISPLAY_COUNT) - : filteredModels - - return ( - - - - - {t('Available Models')} - - - {t('View all currently available models')} · {models.length}{' '} - {t('models')} - - - - - - {categoriesWithCounts.map((cat) => ( - - {cat.label} - - - ))} - - - -
- {displayModels.map((model) => ( - copyModel(model)} - copyable={false} - > - - {model} - - ))} - - {needsExpand && !isExpanded && ( - - )} - {needsExpand && isExpanded && ( - - )} -
- - {filteredModels.length === 0 && ( -

- {t('No models available in this category')} -

- )} -
-
- ) -} diff --git a/web/default/src/features/profile/index.tsx b/web/default/src/features/profile/index.tsx index 669d39e3..f2ee7ec7 100644 --- a/web/default/src/features/profile/index.tsx +++ b/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() { - - - -
diff --git a/web/default/src/features/usage-logs/components/columns/common-logs-columns.tsx b/web/default/src/features/usage-logs/components/columns/common-logs-columns.tsx index 7a14fce2..c36e4faf 100644 --- a/web/default/src/features/usage-logs/components/columns/common-logs-columns.tsx +++ b/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[] { { accessorKey: 'prompt_tokens', header: ({ column }) => ( - + ), cell: ({ row }) => { const log = row.original @@ -494,84 +565,41 @@ export function useCommonLogsColumns(isAdmin: boolean): ColumnDef[] { } const promptTokens = log.prompt_tokens || 0 - if (promptTokens === 0) { + const completionTokens = log.completion_tokens || 0 + if (promptTokens === 0 && completionTokens === 0) { return - } const cacheReadTokens = other?.cache_tokens || 0 - - return ( -
- - {promptTokens.toLocaleString()} - - {cacheReadTokens > 0 && ( - - - - {t('Cache Read')} {cacheReadTokens.toLocaleString()} - - - )} -
- ) - }, - meta: { label: t('Input'), mobileHidden: true }, - }, - - { - accessorKey: 'completion_tokens', - header: ({ column }) => ( - - ), - cell: ({ row }) => { - const log = row.original - if (!isDisplayableLogType(log.type)) return null - - const other = parseLogOther(log.other) - if (isPerCallBilling(other?.model_price)) { - return - - } - - const completionTokens = log.completion_tokens || 0 - if (completionTokens === 0) { - return - - } - 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 (
- {completionTokens.toLocaleString()} + {promptTokens.toLocaleString()} / {completionTokens.toLocaleString()} - {cacheWriteTokens > 0 && ( - - - - {hasSplitCache - ? `${t('Cache Write')} ${cacheWrite5m.toLocaleString()}/${cacheWrite1h.toLocaleString()}` - : `${t('Cache Write')} ${cacheWriteTokens.toLocaleString()}`} - + {cacheSegments.length > 0 && ( + + {cacheSegments.join(' · ')} )}
) }, - meta: { label: t('Output'), mobileHidden: true }, + meta: { label: 'Tokens', mobileHidden: true }, }, { diff --git a/web/default/src/features/usage-logs/components/dialogs/details-dialog.tsx b/web/default/src/features/usage-logs/components/dialogs/details-dialog.tsx index 1dd55056..874fef35 100644 --- a/web/default/src/features/usage-logs/components/dialogs/details-dialog.tsx +++ b/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 ( - + {t('Log Details')} @@ -462,8 +472,8 @@ export function DetailsDialog(props: DetailsDialogProps) { - -
+ +
{/* Overview section - key identifiers */}
{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 && ( -
- -
- )} + {isTieredBilling && other?.expr_b64 && ( +
+ +
+ )} {/* Admin billing mode indicator for non-consume */} {props.isAdmin &&