feat(dashboard): 用量统计新增「昨天」范围 + 跨供应商「模型用量统计」选项卡
- 范围新增 yesterday:本地时区昨日 0 点 ~ 今日 0 点,range_end 引入上界过滤 - RangeStats 新增 modelsByName:按裸模型名聚合,同一模型跨供应商合并, 子代理记录归入 __secondary__ 单独成行,按 Token 降序 - 趋势选项卡第 4 个 tab「模型用量统计」:新增 ModelTotalsChart 横向条形图, 悬停显示请求数/Token/缓存命中/费用 - 概览卡网格扩展为 5 张;i18n 中英同步 - 顺带:pricing 测试基准从已下架的 moonshotai/kimi-k2.5 切换到 kimi-k3
This commit is contained in:
1 parent
86112ee90b
commit
35fd2e7b0e
7 files changed
+252
-29
No files matched your search
@@ -8,10 +8,11 @@ import { fmtInt, fmtPct, fmtTime, fmtTokens } from "../../lib/dashboard-format";
|
||||
import { DailyBars } from "./DailyBars";
|
||||
import { DailyDetailModal } from "./DailyDetailModal";
|
||||
import { Heatmap } from "./Heatmap";
|
||||
import { ModelTotalsChart } from "./ModelTotalsChart";
|
||||
import { TrendLineChart } from "./TrendLineChart";
|
||||
import type { DailyRow, HeatmapCell } from "../../types/dashboard";
|
||||
|
||||
const RANGES: DashboardRange[] = ["today", "7d", "30d", "all"];
|
||||
const RANGES: DashboardRange[] = ["today", "yesterday", "7d", "30d", "all"];
|
||||
|
||||
/** Per-request cache hit rate: cached tokens / total input tokens. */
|
||||
function cacheHitOf(r: { inputOther: number; inputCacheRead: number }): number {
|
||||
@@ -59,13 +60,14 @@ export function DashboardPage() {
|
||||
const { range, changeRange, data, loading, error, refresh, loadStats } = useDashboard();
|
||||
const [showAllModels, setShowAllModels] = useState(false);
|
||||
const [recentPage, setRecentPage] = useState(1);
|
||||
const [trendTab, setTrendTab] = useState<"daily" | "model" | "provider">("daily");
|
||||
const [trendTab, setTrendTab] = useState<"daily" | "model" | "provider" | "totals">("daily");
|
||||
const [heatmapSelected, setHeatmapSelected] = useState<DailyRow | null>(null);
|
||||
|
||||
const daily = data?.stats.daily ?? [];
|
||||
|
||||
const RANGE_LABELS: Record<DashboardRange, string> = {
|
||||
today: t("rangeToday"),
|
||||
yesterday: t("rangeYesterday"),
|
||||
"7d": t("range7d"),
|
||||
"30d": t("range30d"),
|
||||
all: t("rangeAll"),
|
||||
@@ -192,7 +194,7 @@ export function DashboardPage() {
|
||||
</div>
|
||||
|
||||
{/* Range overview cards */}
|
||||
<div className="grid grid-cols-2 gap-3 lg:grid-cols-4">
|
||||
<div className="grid grid-cols-2 gap-3 sm:grid-cols-3 lg:grid-cols-5">
|
||||
{RANGES.map((r) => {
|
||||
const tot = rangeTotals[r];
|
||||
if (!tot) return null;
|
||||
@@ -272,13 +274,26 @@ export function DashboardPage() {
|
||||
>
|
||||
{t("tabProviderTrend")}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setTrendTab("totals")}
|
||||
className={`px-3 py-2 -mb-px border-b-2 text-sm font-medium transition-colors ${
|
||||
trendTab === "totals"
|
||||
? "border-blue-500 text-content-primary"
|
||||
: "border-transparent text-content-muted hover:text-content-primary"
|
||||
}`}
|
||||
>
|
||||
{t("tabModelTotals")}
|
||||
</button>
|
||||
</div>
|
||||
<span className="pb-2 text-xs text-content-muted">
|
||||
{trendTab === "daily"
|
||||
? t("trendLineSub", { n: trendDaily.length })
|
||||
: daily.length > 30
|
||||
? t("cardDailyTrendSubCapped", { n: daily.length })
|
||||
: t("cardDailyTrendSub", { n: daily.length })}
|
||||
: trendTab === "totals"
|
||||
? t("cardModelUsageSub", { n: data.stats.modelsByName.length })
|
||||
: daily.length > 30
|
||||
? t("cardDailyTrendSubCapped", { n: daily.length })
|
||||
: t("cardDailyTrendSub", { n: daily.length })}
|
||||
</span>
|
||||
</div>
|
||||
{/* Active tab content */}
|
||||
@@ -287,6 +302,10 @@ export function DashboardPage() {
|
||||
<div className="flex-1 min-h-[240px]">
|
||||
<TrendLineChart daily={trendDaily} />
|
||||
</div>
|
||||
) : trendTab === "totals" ? (
|
||||
<div className="flex-1 min-h-[240px]">
|
||||
<ModelTotalsChart rows={data.stats.modelsByName} />
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex-1 min-h-[240px]">
|
||||
<DailyBars
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
import { useTranslation } from "../../i18n";
|
||||
import { useCurrency } from "../../hooks/useCurrency";
|
||||
import { fmtInt, fmtPct, fmtTokens } from "../../lib/dashboard-format";
|
||||
import { modelColor } from "./DailyBars";
|
||||
import type { ModelRow } from "../../types/dashboard";
|
||||
|
||||
interface ModelTotalsChartProps {
|
||||
/** Provider-agnostic model totals, pre-sorted by totalTokens desc. */
|
||||
rows: ModelRow[];
|
||||
}
|
||||
|
||||
/** Horizontal ranked bars: one row per bare model name, providers merged. */
|
||||
export function ModelTotalsChart({ rows }: ModelTotalsChartProps) {
|
||||
const { t } = useTranslation();
|
||||
const { money } = useCurrency();
|
||||
|
||||
if (!rows.length) {
|
||||
return (
|
||||
<div className="flex h-48 items-center justify-center text-sm text-content-muted">
|
||||
{t("noData")}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const max = Math.max(...rows.map((m) => m.totalTokens || 0), 1);
|
||||
|
||||
return (
|
||||
<div className="flex h-full flex-col gap-1 overflow-y-auto">
|
||||
{rows.map((m, i) => {
|
||||
const color = i < 10 ? modelColor(i) : "#6b7280";
|
||||
const name = m.isSecondary ? t("subagentModel") : m.modelDisplay || m.model;
|
||||
const tooltip = [
|
||||
name,
|
||||
`${t("kpiRequests")}: ${fmtInt(m.requests)}`,
|
||||
`${t("totalTokens")}: ${fmtInt(m.totalTokens)}`,
|
||||
`${t("colCacheHit")}: ${fmtPct(m.cacheHitRate)}`,
|
||||
`${t("cost")}: ${money(m.costUsd)}`,
|
||||
].join("\n");
|
||||
|
||||
return (
|
||||
<div
|
||||
key={m.model}
|
||||
title={tooltip}
|
||||
className="group flex items-center gap-2 rounded px-1 py-1 cursor-help hover:bg-hover"
|
||||
>
|
||||
<span
|
||||
className="inline-block h-2.5 w-2.5 rounded-sm shrink-0"
|
||||
style={{ backgroundColor: color }}
|
||||
/>
|
||||
<span className="w-40 shrink-0 truncate text-xs text-content-primary">
|
||||
{name}
|
||||
{m.costEstimated && (
|
||||
<span className="ml-1 text-[10px] text-yellow-600">({t("estimate")})</span>
|
||||
)}
|
||||
</span>
|
||||
<div className="flex h-4 flex-1 items-center overflow-hidden rounded-sm bg-hover-2/50">
|
||||
<div
|
||||
className="h-full rounded-sm transition-[width] group-hover:opacity-85"
|
||||
style={{
|
||||
width: `${Math.max(2, (m.totalTokens / max) * 100)}%`,
|
||||
backgroundColor: color,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<span className="w-16 shrink-0 text-right text-xs text-content-muted">
|
||||
{fmtTokens(m.totalTokens)}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user