diff --git a/frontend/src/components/analytics/BreakdownChartCard.tsx b/frontend/src/components/analytics/BreakdownChartCard.tsx index 968480f..53975fd 100644 --- a/frontend/src/components/analytics/BreakdownChartCard.tsx +++ b/frontend/src/components/analytics/BreakdownChartCard.tsx @@ -1,13 +1,6 @@ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; import { AnalyticsBreakdownItem } from "@/types"; -import { cn, formatCurrency } from "@/lib/utils"; - -const formatCompactNumber = (value: number): string => { - return new Intl.NumberFormat("zh-CN", { - notation: value >= 1000 ? "compact" : "standard", - maximumFractionDigits: value >= 1000 ? 1 : 0, - }).format(value); -}; +import { cn, formatCompactNumber, formatCurrency } from "@/lib/utils"; const formatPercent = (value: number): string => `${(value * 100).toFixed(1)}%`; @@ -33,9 +26,7 @@ export function BreakdownChartCard({ displayDecimals, }: BreakdownChartCardProps) { const visibleItems = items.slice(0, 6); - const maxCost = Math.max(...visibleItems.map((item) => item.totalCost), 0); - const useCostScale = maxCost > 0; - const maxScaleValue = useCostScale ? Math.max(maxCost, 1) : Math.max(...visibleItems.map((item) => item.requests), 1); + const maxRequests = Math.max(...visibleItems.map((item) => item.requests), 1); return ( @@ -61,8 +52,7 @@ export function BreakdownChartCard({ ) : ( visibleItems.map((item, index) => { - const scaleValue = useCostScale ? item.totalCost : item.requests; - const width = scaleValue > 0 ? Math.max((scaleValue / maxScaleValue) * 100, 6) : 0; + const width = item.requests > 0 ? Math.max((item.requests / maxRequests) * 100, 6) : 0; return (
@@ -81,17 +71,15 @@ export function BreakdownChartCard({ {item.label}
- 请求 {formatCompactNumber(item.requests)} 成功率 {formatPercent(item.successRate)} - 输出 {formatCompactNumber(item.completionTokens)} + 消耗 {formatCompactNumber(item.totalTokens)} + 费用 {formatCurrency(item.totalCost, displayDecimals)}
-
- {useCostScale ? formatCurrency(item.totalCost, displayDecimals) : formatCompactNumber(item.requests)} -
+
{formatCompactNumber(item.requests)}
{Math.round(item.avgLatencyMs)} ms
diff --git a/frontend/src/components/analytics/TrendBarChart.tsx b/frontend/src/components/analytics/TrendBarChart.tsx index 9f6140b..20f8912 100644 --- a/frontend/src/components/analytics/TrendBarChart.tsx +++ b/frontend/src/components/analytics/TrendBarChart.tsx @@ -1,5 +1,5 @@ import { AnalyticsRange, AnalyticsTrendPoint } from "@/types"; -import { formatCurrency, parseUtcTimestamp } from "@/lib/utils"; +import { formatCompactNumber, formatCurrency, parseUtcTimestamp } from "@/lib/utils"; import { Card } from "../ui/card"; const formatAxisLabel = (value: string, range: AnalyticsRange): string => { @@ -20,13 +20,6 @@ const formatAxisLabel = (value: string, range: AnalyticsRange): string => { }); }; -const formatCompactNumber = (value: number): string => { - return new Intl.NumberFormat("zh-CN", { - notation: value >= 1000 ? "compact" : "standard", - maximumFractionDigits: value >= 1000 ? 1 : 0, - }).format(value); -}; - export function TrendBarChart({ points, range, diff --git a/frontend/src/lib/utils.ts b/frontend/src/lib/utils.ts index bc90a46..57a6883 100644 --- a/frontend/src/lib/utils.ts +++ b/frontend/src/lib/utils.ts @@ -43,6 +43,36 @@ export function formatQuota(value: number, displayDecimals = DEFAULT_BILLING_DIS return formatCurrency(value, displayDecimals) } +export function formatCompactNumber(value: number): string { + const absValue = Math.abs(value) + + const formatWithSuffix = (divisor: number, suffix: string): string => { + const scaled = value / divisor + const formatted = new Intl.NumberFormat('en-US', { + minimumFractionDigits: 0, + maximumFractionDigits: Math.abs(scaled) >= 100 ? 0 : 1, + }).format(scaled) + + return `${formatted}${suffix}` + } + + if (absValue >= 1_000_000_000) { + return formatWithSuffix(1_000_000_000, 'B') + } + + if (absValue >= 1_000_000) { + return formatWithSuffix(1_000_000, 'M') + } + + if (absValue >= 1_000) { + return formatWithSuffix(1_000, 'K') + } + + return new Intl.NumberFormat('en-US', { + maximumFractionDigits: 0, + }).format(value) +} + export function copyToClipboard(text: string): Promise { return navigator.clipboard.writeText(text) } diff --git a/frontend/src/pages/Analytics.tsx b/frontend/src/pages/Analytics.tsx index ea0a191..dfaccc1 100644 --- a/frontend/src/pages/Analytics.tsx +++ b/frontend/src/pages/Analytics.tsx @@ -15,7 +15,7 @@ import { Button } from "@/components/ui/button"; import { Select } from "@/components/ui/select"; import { useBillingConfig } from "@/hooks/use-billing-config"; import { readScopedCache, writeScopedCache } from "@/lib/local-cache"; -import { cn, formatCurrency } from "@/lib/utils"; +import { cn, formatCompactNumber, formatCurrency } from "@/lib/utils"; import { Activity, CircleDollarSign, Clock3, DatabaseZap, RefreshCw, ShieldCheck } from "lucide-react"; import { useEffect, useMemo, useState } from "react"; @@ -36,40 +36,33 @@ const BREAKDOWN_CHARTS: Array<{ { dimension: "model", title: "模型排行", - description: "看清哪些模型是主要成本与流量入口。", + description: "看清哪些模型承载最多请求,并同步观察质量与成本。", barClassName: "from-amber-500 to-orange-400", badgeClassName: "bg-amber-500/12 text-amber-700 dark:text-amber-400", }, { dimension: "channel", title: "渠道排行", - description: "对比不同渠道承载的请求与成本消耗。", + description: "对比不同渠道的请求承载能力与稳定性表现。", barClassName: "from-emerald-500 to-teal-400", badgeClassName: "bg-emerald-500/12 text-emerald-600 dark:text-emerald-400", }, { dimension: "token", title: "令牌排行", - description: "按成本优先排序,识别最活跃的访问凭证。", + description: "按请求量识别最活跃的访问凭证。", barClassName: "from-sky-500 to-cyan-400", badgeClassName: "bg-sky-500/12 text-sky-600 dark:text-sky-400", }, { dimension: "provider", title: "服务商排行", - description: "衡量各上游服务商的占比与稳定性表现。", + description: "衡量各上游服务商承接的请求占比与稳定性表现。", barClassName: "from-rose-500 to-pink-400", badgeClassName: "bg-rose-500/12 text-rose-600 dark:text-rose-400", }, ]; -const formatCompactNumber = (value: number): string => { - return new Intl.NumberFormat("zh-CN", { - notation: value >= 1000 ? "compact" : "standard", - maximumFractionDigits: value >= 1000 ? 1 : 0, - }).format(value); -}; - const formatPercent = (value: number): string => `${(value * 100).toFixed(1)}%`; const formatDuration = (value: number): string => { diff --git a/frontend/src/pages/UsageLogs.tsx b/frontend/src/pages/UsageLogs.tsx index 900bb77..5148b1c 100644 --- a/frontend/src/pages/UsageLogs.tsx +++ b/frontend/src/pages/UsageLogs.tsx @@ -11,7 +11,7 @@ import { Input } from "@/components/ui/input"; import { Select } from "@/components/ui/select"; import { useBillingConfig } from "@/hooks/use-billing-config"; import { readScopedCache, writeScopedCache } from "@/lib/local-cache"; -import { cn, formatCurrency, parseUtcTimestamp } from "@/lib/utils"; +import { cn, formatCompactNumber, formatCurrency, parseUtcTimestamp } from "@/lib/utils"; import { Eye, RefreshCw, Search } from "lucide-react"; type UsageLogFilterState = { @@ -119,13 +119,6 @@ const formatDuration = (value: number): string => { return `${Math.round(value)} ms`; }; -const formatCompactNumber = (value: number): string => { - return new Intl.NumberFormat("zh-CN", { - notation: value >= 1000 ? "compact" : "standard", - maximumFractionDigits: value >= 1000 ? 1 : 0, - }).format(value); -}; - const getUsageLogWindow = (range: AnalyticsRange): { start: Date; end: Date } => { const end = new Date(); const start = new Date(end.getTime() - USAGE_LOG_RANGE_DURATION_MS[range]); diff --git a/frontend/src/types/index.ts b/frontend/src/types/index.ts index 3f8c628..df05548 100644 --- a/frontend/src/types/index.ts +++ b/frontend/src/types/index.ts @@ -109,6 +109,7 @@ export interface AnalyticsBreakdownItem { failures: number successRate: number totalCost: number + totalTokens: number promptTokens: number completionTokens: number avgLatencyMs: number diff --git a/src/analytics/query.ts b/src/analytics/query.ts index ef39389..b9f287a 100644 --- a/src/analytics/query.ts +++ b/src/analytics/query.ts @@ -723,6 +723,7 @@ SELECT sum(_sample_interval) AS requests, sum(${DOUBLE_FIELDS.successFlag} * _sample_interval) AS successes, sum(${normalizedTotalCostExpression} * _sample_interval) AS total_cost, + sum(${DOUBLE_FIELDS.totalTokens} * _sample_interval) AS total_tokens, sum(${DOUBLE_FIELDS.promptTokens} * _sample_interval) AS prompt_tokens, sum(${DOUBLE_FIELDS.completionTokens} * _sample_interval) AS completion_tokens, sum(${DOUBLE_FIELDS.latencyMs} * _sample_interval) / sum(_sample_interval) AS avg_latency_ms @@ -730,7 +731,7 @@ FROM ${dataset} WHERE ${buildRangeWhereClause(config)} AND ${dimensionField} != '' GROUP BY label -ORDER BY total_cost DESC, requests DESC +ORDER BY requests DESC, total_cost DESC LIMIT 12 `.trim()); @@ -748,6 +749,7 @@ LIMIT 12 failures: Math.max(0, requests - successes), successRate: requests > 0 ? successes / requests : 0, totalCost: toNumber(row.total_cost), + totalTokens: toNumber(row.total_tokens), promptTokens: toNumber(row.prompt_tokens), completionTokens: toNumber(row.completion_tokens), avgLatencyMs: toNumber(row.avg_latency_ms),