"use client"; import { useEffect, useState, useCallback, startTransition } from "react"; import Link from "next/link"; import { motion } from "motion/react"; import { Trophy, Users, Cpu, Radio, ArrowUpDown, ArrowDown, ArrowUp } from "lucide-react"; import { TimeRangeSelector } from "@/components/TimeRangeSelector"; import { buildQuery, formatNumber, formatTokens } from "@/lib/utils"; import { useTimeRange } from "@/lib/time-range-context"; import { useI18n } from "@/lib/i18n"; type Tab = "user" | "model" | "channel"; type SortKey = "calls" | "prompt_tokens" | "completion_tokens" | "cache_creation_tokens" | "cache_read_tokens" | "total_tokens"; interface RankItem { rank: number; name: string; username?: string; id?: number; calls: number; prompt_tokens: number; completion_tokens: number; cache_creation_tokens: number; cache_read_tokens: number; total_tokens: number; } export default function RankingsPage() { const { t } = useI18n(); const { getEffectiveRange } = useTimeRange(); const [tab, setTab] = useState("user"); const [data, setData] = useState([]); const [loading, setLoading] = useState(true); const [sortKey, setSortKey] = useState("total_tokens"); const [sortAsc, setSortAsc] = useState(false); const tabConfig: Record = { user: { label: t("rank.user"), icon: Users }, model: { label: t("rank.model"), icon: Cpu }, channel: { label: t("rank.channel"), icon: Radio }, }; const fetchData = useCallback(async () => { startTransition(() => setLoading(true)); const { start, end } = getEffectiveRange(); const res = await fetch(buildQuery("/api/rankings", { start, end, type: tab, limit: 100 })); const json = await res.json(); startTransition(() => { setData(json); setLoading(false); }); }, [tab, getEffectiveRange]); useEffect(() => { fetchData(); }, [fetchData]); function detailHref(item: RankItem): string { if (tab === "channel") return `/detail/channel/${item.id}`; if (tab === "model") return `/detail/model/${encodeURIComponent(item.name)}`; return `/detail/user/${encodeURIComponent(item.username || item.name)}`; } function handleSort(key: SortKey) { if (sortKey === key) setSortAsc(!sortAsc); else { setSortKey(key); setSortAsc(false); } } const sorted = [...data].sort((a, b) => { const diff = (a[sortKey] as number) - (b[sortKey] as number); return sortAsc ? diff : -diff; }); const renderSortIcon = (col: SortKey) => { if (sortKey !== col) return ; return sortAsc ? : ; }; const columns: { key: SortKey | null; label: string; align: "left" | "right" }[] = [ { key: null, label: t("th.rank"), align: "left" }, { key: null, label: t("th.name"), align: "left" }, { key: "calls", label: t("th.calls"), align: "right" }, { key: "prompt_tokens", label: t("th.input"), align: "right" }, { key: "cache_creation_tokens", label: t("th.cacheCreation"), align: "right" }, { key: "cache_read_tokens", label: t("th.cacheRead"), align: "right" }, { key: "completion_tokens", label: t("th.output"), align: "right" }, { key: "total_tokens", label: t("th.totalToken"), align: "right" }, ]; return (

{t("rank.title")}

{(Object.keys(tabConfig) as Tab[]).map((key) => { const Icon = tabConfig[key].icon; return ( ); })}
{columns.map((col) => ( ))} {loading ? ( ) : sorted.map((item, i) => ( ))}
handleSort(col.key!) : undefined} > {col.key ? ( {col.label} {renderSortIcon(col.key)} ) : col.label}
{i + 1} {item.name} {formatNumber(item.calls)} {formatTokens(item.prompt_tokens)} {formatTokens(item.cache_creation_tokens)} {formatTokens(item.cache_read_tokens)} {formatTokens(item.completion_tokens)} {formatTokens(item.total_tokens)}
); }