feat(analytics): 增加总令牌数支持并优化相关逻辑
This commit is contained in:
1 parent
0258367a64
commit
141da9dcf0
7 files changed
+47
-47
No files matched your search
@@ -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 (
|
||||
<Card className="overflow-hidden border-0">
|
||||
@@ -61,8 +52,7 @@ export function BreakdownChartCard({
|
||||
</div>
|
||||
) : (
|
||||
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 (
|
||||
<div key={`${title}-${item.label}-${index}`}>
|
||||
@@ -81,17 +71,15 @@ export function BreakdownChartCard({
|
||||
{item.label}
|
||||
|
||||
<div className="flex flex-wrap gap-x-3 gap-y-1 text-xs text-muted-foreground">
|
||||
<span>请求 {formatCompactNumber(item.requests)}</span>
|
||||
<span>成功率 {formatPercent(item.successRate)}</span>
|
||||
<span>输出 {formatCompactNumber(item.completionTokens)}</span>
|
||||
<span>消耗 {formatCompactNumber(item.totalTokens)}</span>
|
||||
<span>费用 {formatCurrency(item.totalCost, displayDecimals)}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-right">
|
||||
<div className="font-semibold">
|
||||
{useCostScale ? formatCurrency(item.totalCost, displayDecimals) : formatCompactNumber(item.requests)}
|
||||
</div>
|
||||
<div className="font-semibold">{formatCompactNumber(item.requests)}</div>
|
||||
<div className="text-xs text-muted-foreground">{Math.round(item.avgLatencyMs)} ms</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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<void> {
|
||||
return navigator.clipboard.writeText(text)
|
||||
}
|
||||
|
||||
@@ -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 => {
|
||||
|
||||
@@ -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]);
|
||||
|
||||
@@ -109,6 +109,7 @@ export interface AnalyticsBreakdownItem {
|
||||
failures: number
|
||||
successRate: number
|
||||
totalCost: number
|
||||
totalTokens: number
|
||||
promptTokens: number
|
||||
completionTokens: number
|
||||
avgLatencyMs: number
|
||||
|
||||
@@ -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),
|
||||
|
||||
Reference in new issue
Block a user