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.",
"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" />
</button>
</TooltipTrigger>
<TooltipContent className="max-w-64">
{col.headerTooltip}
</TooltipContent>
</Tooltip>
)}
{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"
Comment on lines +110 to +114

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Match coefficient precision to the displayed tier

The repository computes cacheCoefficientBp as an arbitrary integer basis-point value, but this renderer assigns the tier using the unrounded value while displaying only two decimals. Values from 8950 through 8999 therefore appear as 0.90 in yellow rather than the documented green 0.9+ tier, and 7950 through 7999 similarly appear as 0.80 in orange. Either display enough precision to justify the tier or classify the same rounded value that users see.

Useful? React with 👍 / 👎.

: "text-orange-600 dark:text-orange-400";
return <span className={colorClass}>{value.toFixed(2)}</span>;
}

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
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,16 @@ import { cn } from "@/lib/utils";
export type FilterPreset = "today" | "this-week" | "errors-only" | "show-retries";

interface QuickFiltersBarProps {
activePreset: FilterPreset | null;
activePresets: ReadonlySet<FilterPreset>;
onPresetToggle: (preset: FilterPreset) => void;
className?: string;
}

export function QuickFiltersBar({ activePreset, onPresetToggle, className }: QuickFiltersBarProps) {
export function QuickFiltersBar({
activePresets,
onPresetToggle,
className,
}: QuickFiltersBarProps) {
const t = useTranslations("dashboard.logs.filters");

const timePresets: Array<{ id: FilterPreset; label: string; icon: typeof Calendar }> = [
Expand All @@ -40,10 +44,11 @@ export function QuickFiltersBar({ activePreset, onPresetToggle, className }: Qui
<Button
key={id}
type="button"
variant={activePreset === id ? "default" : "outline"}
variant={activePresets.has(id) ? "default" : "outline"}
size="sm"
onClick={() => onPresetToggle(id)}
className="shrink-0"
aria-pressed={activePresets.has(id)}
>
<Icon className="h-4 w-4 mr-1.5" />
{label}
Expand All @@ -58,10 +63,11 @@ export function QuickFiltersBar({ activePreset, onPresetToggle, className }: Qui
<Button
key={id}
type="button"
variant={activePreset === id ? "default" : "outline"}
variant={activePresets.has(id) ? "default" : "outline"}
size="sm"
onClick={() => onPresetToggle(id)}
className="shrink-0"
aria-pressed={activePresets.has(id)}
>
<Icon className="h-4 w-4 mr-1.5" />
{label}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -106,10 +106,15 @@ export function LogsDateRangePicker({

const handleQuickPeriodClick = useCallback(
(period: QuickPeriod) => {
// Toggle: clicking the active period again clears the date range
if (activeQuickPeriod === period) {
onDateRangeChange({ startDate: undefined, endDate: undefined });
Comment on lines +110 to +111

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Distinguish custom-clock ranges before toggling date presets

After selecting Today and then changing the start or end clock, activeQuickPeriod still reports today because it compares only the date strings. Clicking the Today button therefore enters this new branch and clears the entire range, even though the exact-range quick-filter logic correctly considers the custom-clock range inactive. This makes the two Today controls disagree and causes a preset click to discard the filter instead of applying the full-day preset; the toggle should use the same exact timestamp predicate or account for the clocks.

Useful? React with 👍 / 👎.

return;
}
const range = getDateRangeForPeriod(period, serverTimeZone);
onDateRangeChange(range);
},
[onDateRangeChange, serverTimeZone]
[activeQuickPeriod, onDateRangeChange, serverTimeZone]
);

const handleNavigate = useCallback(
Expand Down
104 changes: 54 additions & 50 deletions src/app/[locale]/dashboard/logs/_components/usage-logs-filters.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
"use client";

import { format, startOfDay, startOfWeek } from "date-fns";
import { format } from "date-fns";
import { ChevronDown, Clock, Download, Network, Server, User } from "lucide-react";
import { useTranslations } from "next-intl";
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
Expand All @@ -22,6 +22,7 @@ import {
import { getErrorMessage } from "@/lib/utils/error-messages";
import type { Key } from "@/types/key";
import type { ProviderDisplay } from "@/types/provider";
import { detectQuickTimePreset, getQuickTimeRange } from "../_utils/time-range";
import { ActiveFiltersDisplay } from "./filters/active-filters-display";
import { FilterSection } from "./filters/filter-section";
import { IdentityFilters } from "./filters/identity-filters";
Expand Down Expand Up @@ -87,7 +88,6 @@ export function UsageLogsFilters({
const [localFilters, setLocalFilters] = useState<UsageLogFilters>(filters);
const [isExporting, setIsExporting] = useState(false);
const [exportStatus, setExportStatus] = useState<UsageLogsExportStatus | null>(null);
const [activePreset, setActivePreset] = useState<FilterPreset | null>(null);
const exportRunIdRef = useRef(0);

// Track users and keys for display name resolution
Expand Down Expand Up @@ -159,6 +159,30 @@ export function UsageLogsFilters({
localFilters.replayFilter,
]);

// Quick filter highlight states are derived from the actual filter values, so the quick
// filters bar, date range picker and time inputs always stay in sync no matter which
// control changed the underlying range.
const activePresets = useMemo<ReadonlySet<FilterPreset>>(() => {
const presets = new Set<FilterPreset>();
const timePreset = detectQuickTimePreset(
localFilters.startTime,
localFilters.endTime,
serverTimeZone
);
if (timePreset) presets.add(timePreset);
if (localFilters.excludeStatusCode200) presets.add("errors-only");
if (localFilters.minRetryCount !== undefined && localFilters.minRetryCount > 0) {
presets.add("show-retries");
}
return presets;
}, [
localFilters.startTime,
localFilters.endTime,
localFilters.excludeStatusCode200,
localFilters.minRetryCount,
serverTimeZone,
]);

useEffect(() => {
setLocalFilters(filters);
}, [filters]);
Expand All @@ -177,7 +201,6 @@ export function UsageLogsFilters({
exportRunIdRef.current += 1;
setLocalFilters({});
setKeys([]);
setActivePreset(null);
setIsExporting(false);
setExportStatus(null);
onReset();
Expand Down Expand Up @@ -291,58 +314,40 @@ export function UsageLogsFilters({

const handlePresetToggle = useCallback(
(preset: FilterPreset) => {
const now = new Date();

if (preset === activePreset) {
// Toggle off - clear the preset-related filters
setActivePreset(null);
setLocalFilters((prev) => {
const next = { ...prev };
if (preset === "today" || preset === "this-week") {
const isActive = activePresets.has(preset);

setLocalFilters((prev) => {
const next = { ...prev };

if (preset === "today" || preset === "this-week") {
if (isActive) {
delete next.startTime;
delete next.endTime;
} else if (preset === "errors-only") {
} else {
const range = getQuickTimeRange(preset, serverTimeZone);
if (!range) return prev;
next.startTime = range.startTime;
next.endTime = range.endTime;
}
} else if (preset === "errors-only") {
if (isActive) {
delete next.excludeStatusCode200;
} else if (preset === "show-retries") {
} else {
next.excludeStatusCode200 = true;
next.statusCode = undefined;
}
} else if (preset === "show-retries") {
if (isActive) {
delete next.minRetryCount;
} else {
next.minRetryCount = 1;
}
return next;
});
return;
}
}

setActivePreset(preset);

if (preset === "today") {
const todayStart = startOfDay(now).getTime();
const todayEnd = todayStart + 24 * 60 * 60 * 1000;
setLocalFilters((prev) => ({
...prev,
startTime: todayStart,
endTime: todayEnd,
}));
} else if (preset === "this-week") {
const weekStart = startOfWeek(now, { weekStartsOn: 1 }).getTime();
const weekEnd = weekStart + 7 * 24 * 60 * 60 * 1000;
setLocalFilters((prev) => ({
...prev,
startTime: weekStart,
endTime: weekEnd,
}));
} else if (preset === "errors-only") {
setLocalFilters((prev) => ({
...prev,
excludeStatusCode200: true,
statusCode: undefined,
}));
} else if (preset === "show-retries") {
setLocalFilters((prev) => ({
...prev,
minRetryCount: 1,
}));
}
return next;
});
},
[activePreset]
[activePresets, serverTimeZone]
);

const handleRemoveFilter = useCallback((key: keyof UsageLogFilters) => {
Expand All @@ -351,13 +356,12 @@ export function UsageLogsFilters({
delete next[key];
return next;
});
setActivePreset(null);
}, []);

return (
<div className="space-y-4">
{/* Quick Filters Bar */}
<QuickFiltersBar activePreset={activePreset} onPresetToggle={handlePresetToggle} />
<QuickFiltersBar activePresets={activePresets} onPresetToggle={handlePresetToggle} />

{/* Active Filters Display */}
<ActiveFiltersDisplay
Expand Down
Loading
Loading