Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions messages/en/dashboard.json
Original file line number Diff line number Diff line change
Expand Up @@ -704,6 +704,7 @@
"cacheHitRequests": "Cache-eligible Requests",
"cacheHitRate": "Cache Hit Rate",
"cacheCoefficient": "Cache Coefficient",
"cacheCoefficientTooltip": "A higher cache coefficient means fewer provider account switches and less noticeable cache degradation. 0.9+ is excellent, 0.8+ is good.",
Comment thread
ding113 marked this conversation as resolved.
Comment thread
ding113 marked this conversation as resolved.
"cacheReadTokens": "Cache Read Tokens",
"totalTokens": "Total Tokens",
"cacheCreationConsumedAmount": "Cache Creation Spend",
Expand Down
1 change: 1 addition & 0 deletions messages/ja/dashboard.json
Original file line number Diff line number Diff line change
Expand Up @@ -704,6 +704,7 @@
"cacheHitRequests": "キャッシュ対象リクエスト数(命中率計算対象)",
"cacheHitRate": "キャッシュ命中率",
"cacheCoefficient": "キャッシュ係数",
"cacheCoefficientTooltip": "キャッシュ係数が大きいほど、プロバイダーのアカウント切り替えが少なく、キャッシュ劣化が目立ちにくくなります。0.9 以上は優秀、0.8 以上は良好です。",
"cacheReadTokens": "キャッシュ読取トークン数",
"totalTokens": "総トークン数",
"cacheCreationConsumedAmount": "キャッシュ作成消費額",
Expand Down
1 change: 1 addition & 0 deletions messages/ru/dashboard.json
Original file line number Diff line number Diff line change
Expand Up @@ -704,6 +704,7 @@
"cacheHitRequests": "Запросы (учтены в hit rate)",
"cacheHitRate": "Попадания в кэш",
"cacheCoefficient": "Коэффициент кэша",
"cacheCoefficientTooltip": "Чем выше коэффициент кэша, тем реже переключаются аккаунты поставщика и тем менее заметна деградация кэша. 0.9 и выше — отлично, 0.8 и выше — хорошо.",
"cacheReadTokens": "Токены чтения из кэша",
"totalTokens": "Всего токенов",
"cacheCreationConsumedAmount": "Расход на создание кэша",
Expand Down
1 change: 1 addition & 0 deletions messages/zh-CN/dashboard.json
Original file line number Diff line number Diff line change
Expand Up @@ -704,6 +704,7 @@
"cacheHitRequests": "缓存触发请求数",
"cacheHitRate": "缓存命中率",
"cacheCoefficient": "缓存系数",
"cacheCoefficientTooltip": "缓存系数越大,供应商切号越少,失缓现象越不明显。0.9 以上为优秀,0.8 以上为良好。",
"cacheReadTokens": "缓存读取 Token 数",
"totalTokens": "总 Token 数",
"cacheCreationConsumedAmount": "缓存创建消耗金额",
Expand Down
1 change: 1 addition & 0 deletions messages/zh-TW/dashboard.json
Original file line number Diff line number Diff line change
Expand Up @@ -704,6 +704,7 @@
"cacheHitRequests": "快取命中請求數(納入快取命中率計算的請求總數)",
"cacheHitRate": "快取命中率",
"cacheCoefficient": "快取係數",
"cacheCoefficientTooltip": "快取係數越大,供應商切號越少,失緩現象越不明顯。0.9 以上為優秀,0.8 以上為良好。",
"cacheReadTokens": "快取讀取 Token 數",
"totalTokens": "總 Token 數",
"cacheCreationConsumedAmount": "快取建立消耗金額",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
Award,
ChevronDown,
ChevronRight,
CircleHelp,
Medal,
Trophy,
} from "lucide-react";
Expand All @@ -22,11 +23,14 @@ import {
TableHeader,
TableRow,
} from "@/components/ui/table";
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
import type { LeaderboardPeriod } from "@/repository/leaderboard";

// 支持动态列定义
export interface ColumnDef<T> {
header: string;
/** 表头帮助图标悬停时展示的说明文案 */
headerTooltip?: string;
className?: string;
/**
* index 语义:
Expand Down Expand Up @@ -245,6 +249,23 @@ export function LeaderboardTable<TParent, TSub = TParent>({
className={`flex items-center ${col.className?.includes("text-right") ? "justify-end" : ""} ${shouldBold ? "font-bold" : ""}`}
>
{col.header}
{col.headerTooltip && (
<Tooltip>
<TooltipTrigger asChild>
<button
type="button"
aria-label={col.headerTooltip}
className="ml-1 inline-flex cursor-help items-center text-muted-foreground/70 hover:text-muted-foreground"
onClick={(e) => e.stopPropagation()}
>
<CircleHelp className="h-3.5 w-3.5" />
Comment thread
ding113 marked this conversation as resolved.
</button>
</TooltipTrigger>
<TooltipContent className="max-w-64">
{col.headerTooltip}
</TooltipContent>
</Tooltip>
)}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
{col.sortKey && getSortIcon(col.sortKey)}
</div>
</TableHead>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,21 @@ function renderSuccessRateCell(

const VALID_PERIODS: LeaderboardPeriod[] = ["daily", "weekly", "monthly", "allTime", "custom"];

// 缓存系数分层配色(与缓存命中率同档):>=0.9 优秀(绿),>=0.8 良好(黄),其余橙色
function renderCacheCoefficientCell(bp: number | null) {
if (bp == null) {
return <span className="text-muted-foreground">–</span>;
}
const value = bp / 10000;
const colorClass =
value >= 0.9
? "text-green-600 dark:text-green-400"
: value >= 0.8
? "text-yellow-600 dark:text-yellow-400"
: "text-orange-600 dark:text-orange-400";
return <span className={colorClass}>{value.toFixed(2)}</span>;
Comment thread
ding113 marked this conversation as resolved.
Comment thread
ding113 marked this conversation as resolved.
}

export function LeaderboardView({ isAdmin }: LeaderboardViewProps) {
const t = useTranslations("dashboard.leaderboard");
const searchParams = useSearchParams();
Expand Down Expand Up @@ -385,11 +400,10 @@ export function LeaderboardView({ isAdmin }: LeaderboardViewProps) {
},
{
header: t("columns.cacheCoefficient"),
headerTooltip: t("columns.cacheCoefficientTooltip"),
className: "text-right",
cell: (row) => {
const bp = "cacheCoefficientBp" in row ? row.cacheCoefficientBp : null;
return bp == null ? "–" : (bp / 10000).toFixed(2);
},
cell: (row) =>
renderCacheCoefficientCell("cacheCoefficientBp" in row ? row.cacheCoefficientBp : null),
sortKey: "cacheCoefficientBp",
getValue: (row) => ("cacheCoefficientBp" in row ? row.cacheCoefficientBp : null),
},
Expand Down Expand Up @@ -430,11 +444,10 @@ export function LeaderboardView({ isAdmin }: LeaderboardViewProps) {
},
{
header: t("columns.cacheCoefficient"),
headerTooltip: t("columns.cacheCoefficientTooltip"),
className: "text-right",
cell: (row) => {
const bp = "cacheCoefficientBp" in row ? row.cacheCoefficientBp : null;
return bp == null ? "–" : (bp / 10000).toFixed(2);
},
cell: (row) =>
renderCacheCoefficientCell("cacheCoefficientBp" in row ? row.cacheCoefficientBp : null),
sortKey: "cacheCoefficientBp",
getValue: (row) => ("cacheCoefficientBp" in row ? row.cacheCoefficientBp : null),
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -349,7 +349,7 @@ export function SummaryTab({
</span>
<Link
href={buildLogsFilterHref(identity.value)}
className="text-xs font-mono break-all underline-offset-2 hover:underline"
className="text-xs font-mono truncate min-w-0 underline-offset-2 hover:underline"
>
{identity.value}
</Link>
Expand Down
98 changes: 98 additions & 0 deletions tests/unit/dashboard/leaderboard-view-cache-coefficient.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,104 @@ describe("LeaderboardView cache coefficient column", () => {
expect(text).toContain("–");
});

it("shows a tooltip trigger on the cache coefficient column header", async () => {
fetchMock.mockImplementation(async (input) => {
const url = String(input);
if (url.includes("scope=providerCacheHitRate")) {
return {
ok: true,
json: async () => [cacheHitEntry({ providerId: 1, cacheCoefficientBp: 9000 })],
} as Response;
}
return { ok: true, json: async () => [] } as Response;
});

await act(async () => {
root!.render(<LeaderboardView isAdmin />);
});

const coefficientHeader = Array.from(container!.querySelectorAll("th")).find((th) =>
th.textContent?.includes("columns.cacheCoefficient")
);
expect(coefficientHeader).toBeDefined();
const trigger = coefficientHeader!.querySelector('[data-slot="tooltip-trigger"]');
expect(trigger).not.toBeNull();
});

it("does not trigger column sorting when the help icon is clicked", async () => {
fetchMock.mockImplementation(async (input) => {
const url = String(input);
if (url.includes("scope=providerCacheHitRate")) {
return {
ok: true,
json: async () => [
cacheHitEntry({ providerId: 1, providerName: "high-first", cacheCoefficientBp: 9500 }),
cacheHitEntry({ providerId: 2, providerName: "low-second", cacheCoefficientBp: 5000 }),
],
} as Response;
}
return { ok: true, json: async () => [] } as Response;
});
Comment on lines +151 to +164

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

rg -n 'scope=(providerCacheHitRate|userCacheHitRate)' \
  tests/unit/dashboard/leaderboard-view-cache-coefficient.test.tsx

Repository: ding113/claude-code-hub

Length of output: 484


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "file_stats"
wc -l tests/unit/dashboard/leaderboard-view-cache-coefficient.test.tsx

echo
echo "outline"
ast-grep outline tests/unit/dashboard/leaderboard-view-cache-coefficient.test.tsx || true

echo
echo "scope/usages"
rg -n 'userCacheHitRate|providerCacheHitRate|scope=|cacheCoefficient|cacheCoefficientBp|Leaderboard' tests/unit/dashboard/leaderboard-view-cache-coefficient.test.tsx

echo
echo "sections"
sed -n '1,220p' tests/unit/dashboard/leaderboard-view-cache-coefficient.test.tsx

Repository: ding113/claude-code-hub

Length of output: 10217


scope=userCacheHitRate 补充 LeaderboardView 测试覆盖。

当前用例只使用 scope=providerCacheHitRate,缓存系数列的 Tooltip、排序隔离和分级显示在未指定 scope 或默认用户排行榜路径下未得到验证。请补充对应的 userCacheHitRate 测试。

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/unit/dashboard/leaderboard-view-cache-coefficient.test.tsx` around
lines 151 - 164, 在 “does not trigger column sorting when the help icon is
clicked” 相关测试中补充 scope=userCacheHitRate 的 LeaderboardView 覆盖,验证用户排行榜默认路径下缓存系数列的
Tooltip、排序隔离和分级显示行为。复用现有 providerCacheHitRate 测试的断言结构与测试数据模式,确保两种 scope 均得到验证。


await act(async () => {
root!.render(<LeaderboardView isAdmin />);
});

const coefficientHeader = Array.from(container!.querySelectorAll("th")).find((th) =>
th.textContent?.includes("columns.cacheCoefficient")
);
const trigger = coefficientHeader!.querySelector('[data-slot="tooltip-trigger"]');
expect(trigger).not.toBeNull();

await act(async () => {
trigger!.dispatchEvent(new MouseEvent("click", { bubbles: true }));
});

// 行顺序保持默认(未触发升序排序,否则 low-second 会排到第一行)
const bodyText = container!.querySelector("tbody")?.textContent ?? "";
expect(bodyText.indexOf("high-first")).toBeLessThan(bodyText.indexOf("low-second"));
});

it("colors the coefficient by tier: >=0.9 green, >=0.8 yellow, else orange", async () => {
fetchMock.mockImplementation(async (input) => {
const url = String(input);
if (url.includes("scope=providerCacheHitRate")) {
return {
ok: true,
json: async () => [
cacheHitEntry({ providerId: 1, providerName: "excellent", cacheCoefficientBp: 9500 }),
cacheHitEntry({
providerId: 2,
providerName: "edge-excellent",
cacheCoefficientBp: 9000,
}),
cacheHitEntry({ providerId: 3, providerName: "good", cacheCoefficientBp: 8600 }),
cacheHitEntry({ providerId: 4, providerName: "edge-good", cacheCoefficientBp: 8000 }),
cacheHitEntry({ providerId: 5, providerName: "poor", cacheCoefficientBp: 5000 }),
cacheHitEntry({ providerId: 6, providerName: "missing", cacheCoefficientBp: null }),
],
} as Response;
}
return { ok: true, json: async () => [] } as Response;
});

await act(async () => {
root!.render(<LeaderboardView isAdmin />);
});

const hasColoredValue = (selector: string, text: string) =>
Array.from(container!.querySelectorAll(selector)).some((el) => el.textContent === text);
expect(hasColoredValue("span.text-green-600", "0.95")).toBe(true);
// 边界值:0.90 仍属优秀档
expect(hasColoredValue("span.text-green-600", "0.90")).toBe(true);
expect(hasColoredValue("span.text-yellow-600", "0.86")).toBe(true);
// 边界值:0.80 仍属良好档
expect(hasColoredValue("span.text-yellow-600", "0.80")).toBe(true);
expect(hasColoredValue("span.text-orange-600", "0.50")).toBe(true);
// 缺失值:muted 样式展示占位符
expect(hasColoredValue("span.text-muted-foreground", "–")).toBe(true);
});

it("renders the coefficient column on the provider usage board too", async () => {
searchParamsState.value = new URLSearchParams("scope=provider");
fetchMock.mockImplementation(async (input) => {
Expand Down
Loading