import { useMemo, useState } from "react"; import { useTranslation } from "react-i18next"; import { UsageHero } from "./UsageHero"; import { UsageTrendChart } from "./UsageTrendChart"; import { RequestLogTable } from "./RequestLogTable"; import { ProviderStatsTable } from "./ProviderStatsTable"; import { ModelStatsTable } from "./ModelStatsTable"; import { KNOWN_APP_TYPES, type AppType, type AppTypeFilter, type UsageRangeSelection, } from "@/types/usage"; import { motion } from "framer-motion"; import { BarChart3, ListFilter, Activity, RefreshCw, Coins, LayoutGrid, } from "lucide-react"; import { ProviderIcon } from "@/components/ProviderIcon"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "@/components/ui/select"; import { useQueryClient } from "@tanstack/react-query"; import { usageKeys, useModelStats, useProviderStats } from "@/lib/query/usage"; import { useUsageEventBridge } from "@/hooks/useUsageEventBridge"; import { Accordion, AccordionContent, AccordionItem, AccordionTrigger, } from "@/components/ui/accordion"; import { PricingConfigPanel } from "@/components/usage/PricingConfigPanel"; import { cn } from "@/lib/utils"; import { getLocaleFromLanguage } from "./format"; import { getUsageRangePresetLabel, resolveUsageRange } from "@/lib/usageRange"; import { UsageDateRangePicker } from "./UsageDateRangePicker"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; const APP_FILTER_OPTIONS: AppTypeFilter[] = ["all", ...KNOWN_APP_TYPES]; // 0 表示关闭自动刷新(refetchInterval=false) const REFRESH_INTERVAL_OPTIONS_MS = [0, 5000, 10000, 30000, 60000] as const; // 与 AppSwitcher 的 appIconName 保持一致(codex 复用 openai 图标) const APP_FILTER_ICON: Record = { claude: "claude", codex: "openai", gemini: "gemini", opencode: "opencode", }; // Select 的 "all" 哨兵和用户自定义名称同处一个值域——真有来源/模型叫 "all" // 就会撞名(重复 value、选中即清空筛选)。动态选项统一加前缀编码隔离值域。 const DYNAMIC_OPTION_PREFIX = "v:"; const encodeOptionValue = (name: string) => `${DYNAMIC_OPTION_PREFIX}${name}`; const decodeOptionValue = (value: string) => value === "all" ? undefined : value.slice(DYNAMIC_OPTION_PREFIX.length); export function UsageDashboard() { const { t, i18n } = useTranslation(); const queryClient = useQueryClient(); const [range, setRange] = useState({ preset: "today" }); const [appType, setAppType] = useState("all"); const [providerName, setProviderName] = useState( undefined, ); const [model, setModel] = useState(undefined); const [refreshIntervalMs, setRefreshIntervalMs] = useState(30000); // 切应用时清掉下游筛选,避免留下一个在新范围内查无数据的"幽灵"组合; // 切 Provider 同理清掉模型(模型选项随 Provider 级联)。 const changeAppType = (next: AppTypeFilter) => { setAppType(next); if (next !== appType) { setProviderName(undefined); setModel(undefined); } }; const changeProviderName = (next: string | undefined) => { setProviderName(next); if (next !== providerName) { setModel(undefined); } }; // 后端写入新日志时 emit `usage-log-recorded`,本 hook 立刻 invalidate 所有 // usage 查询,实现实时刷新(仅在 Dashboard 挂载时生效,离开页面自动取消监听) useUsageEventBridge(); const changeRefreshInterval = (next: number) => { setRefreshIntervalMs(next); queryClient.invalidateQueries({ queryKey: usageKeys.all }); }; const language = i18n.resolvedLanguage || i18n.language || "en"; const locale = getLocaleFromLanguage(language); const resolvedRange = useMemo(() => resolveUsageRange(range), [range]); const rangeLabel = useMemo(() => { if (range.preset !== "custom") { return getUsageRangePresetLabel(range.preset, t); } return `${new Date(resolvedRange.startDate * 1000).toLocaleString(locale)} - ${new Date( resolvedRange.endDate * 1000, ).toLocaleString(locale)}`; }, [locale, range, resolvedRange.endDate, resolvedRange.startDate, t]); // 顶栏下拉的选项池:Provider 列表只跟应用/时间范围走(不受自身选中值影响), // 模型列表随所选 Provider 级联。两者都只列当前范围内真实有数据的条目。 // refetchInterval 必须跟随面板的刷新设置——未筛选时这两个查询与统计表共享 // query key,落下的话会以默认 30s 拖着同 key 查询一起轮询,"--" 形同虚设。 const optionsRefetch = { refetchInterval: refreshIntervalMs > 0 ? refreshIntervalMs : (false as const), }; const { data: providerOptionsData } = useProviderStats( range, { appType }, optionsRefetch, ); const { data: modelOptionsData } = useModelStats( range, { appType, providerName }, optionsRefetch, ); const providerOptions = useMemo(() => { const names = new Set(); for (const stat of providerOptionsData ?? []) { names.add(stat.providerName); } // 数据刷新后选中项可能掉出列表(如改了时间范围);补回去保证 Select // 仍能渲染选中文案,用户看得见才能主动清除。 if (providerName) names.add(providerName); return Array.from(names); }, [providerOptionsData, providerName]); const modelOptions = useMemo(() => { const names = new Set(); for (const stat of modelOptionsData ?? []) { names.add(stat.model); } if (model) names.add(model); return Array.from(names); }, [modelOptionsData, model]); return (

{t("usage.title")}

{t("usage.subtitle")}

{APP_FILTER_OPTIONS.map((type) => { const label = t(`usage.appFilter.${type}`); return ( ); })}
setRange(nextRange)} />
{t("usage.requestLogs")} {t("usage.providerStats")} {t("usage.modelStats")}

{t("settings.advanced.pricing.title")}

{t("settings.advanced.pricing.description")}

); }