feat: add sortable columns to rankings table (calls/input/output/total token)
This commit is contained in:
@@ -3,12 +3,13 @@
|
|||||||
import { useEffect, useState, useCallback } from "react";
|
import { useEffect, useState, useCallback } from "react";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { motion } from "motion/react";
|
import { motion } from "motion/react";
|
||||||
import { Trophy, Users, Cpu, Radio } from "lucide-react";
|
import { Trophy, Users, Cpu, Radio, ArrowUpDown, ArrowDown, ArrowUp } from "lucide-react";
|
||||||
import { TimeRangeSelector } from "@/components/TimeRangeSelector";
|
import { TimeRangeSelector } from "@/components/TimeRangeSelector";
|
||||||
import { type TimeRange, getTimeRange, buildQuery, formatNumber, formatTokens } from "@/lib/utils";
|
import { type TimeRange, getTimeRange, buildQuery, formatNumber, formatTokens } from "@/lib/utils";
|
||||||
import { useI18n } from "@/lib/i18n";
|
import { useI18n } from "@/lib/i18n";
|
||||||
|
|
||||||
type Tab = "user" | "model" | "channel";
|
type Tab = "user" | "model" | "channel";
|
||||||
|
type SortKey = "calls" | "prompt_tokens" | "completion_tokens" | "total_tokens";
|
||||||
|
|
||||||
interface RankItem {
|
interface RankItem {
|
||||||
rank: number; name: string; id?: number; calls: number;
|
rank: number; name: string; id?: number; calls: number;
|
||||||
@@ -21,6 +22,8 @@ export default function RankingsPage() {
|
|||||||
const [tab, setTab] = useState<Tab>("user");
|
const [tab, setTab] = useState<Tab>("user");
|
||||||
const [data, setData] = useState<RankItem[]>([]);
|
const [data, setData] = useState<RankItem[]>([]);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [sortKey, setSortKey] = useState<SortKey>("total_tokens");
|
||||||
|
const [sortAsc, setSortAsc] = useState(false);
|
||||||
|
|
||||||
const tabConfig: Record<Tab, { label: string; icon: typeof Users }> = {
|
const tabConfig: Record<Tab, { label: string; icon: typeof Users }> = {
|
||||||
user: { label: t("rank.user"), icon: Users },
|
user: { label: t("rank.user"), icon: Users },
|
||||||
@@ -44,7 +47,31 @@ export default function RankingsPage() {
|
|||||||
return `/detail/user/${encodeURIComponent(item.name)}`;
|
return `/detail/user/${encodeURIComponent(item.name)}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
const headers = [t("th.rank"), t("th.name"), t("th.calls"), t("th.input"), t("th.output"), t("th.totalToken")];
|
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;
|
||||||
|
});
|
||||||
|
|
||||||
|
function SortIcon({ col }: { col: SortKey }) {
|
||||||
|
if (sortKey !== col) return <ArrowUpDown className="h-3 w-3" style={{ color: "var(--text-muted)", opacity: 0.4 }} />;
|
||||||
|
return sortAsc
|
||||||
|
? <ArrowUp className="h-3 w-3" style={{ color: "var(--accent)" }} />
|
||||||
|
: <ArrowDown className="h-3 w-3" style={{ color: "var(--accent)" }} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
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: "completion_tokens", label: t("th.output"), align: "right" },
|
||||||
|
{ key: "total_tokens", label: t("th.totalToken"), align: "right" },
|
||||||
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
@@ -80,18 +107,28 @@ export default function RankingsPage() {
|
|||||||
<table className="w-full text-sm">
|
<table className="w-full text-sm">
|
||||||
<thead>
|
<thead>
|
||||||
<tr style={{ borderBottom: "1px solid var(--surface-border)" }}>
|
<tr style={{ borderBottom: "1px solid var(--surface-border)" }}>
|
||||||
{headers.map((h, i) => (
|
{columns.map((col) => (
|
||||||
<th key={h} className={`px-4 py-3 text-xs font-medium uppercase tracking-wider ${i >= 2 ? "text-right" : "text-left"}`} style={{ color: "var(--text-muted)" }}>{h}</th>
|
<th key={col.label}
|
||||||
|
className={`px-4 py-3 text-xs font-medium uppercase tracking-wider ${col.align === "right" ? "text-right" : "text-left"} ${col.key ? "cursor-pointer select-none transition-colors hover:opacity-80" : ""}`}
|
||||||
|
style={{ color: col.key && sortKey === col.key ? "var(--text-accent)" : "var(--text-muted)" }}
|
||||||
|
onClick={col.key ? () => handleSort(col.key!) : undefined}
|
||||||
|
>
|
||||||
|
{col.key ? (
|
||||||
|
<span className={`inline-flex items-center gap-1 ${col.align === "right" ? "justify-end" : ""}`}>
|
||||||
|
{col.label} <SortIcon col={col.key} />
|
||||||
|
</span>
|
||||||
|
) : col.label}
|
||||||
|
</th>
|
||||||
))}
|
))}
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{loading ? (
|
{loading ? (
|
||||||
<tr><td colSpan={6} className="px-4 py-16 text-center"><div className="inline-block h-5 w-5 animate-spin rounded-full spinner" /></td></tr>
|
<tr><td colSpan={6} className="px-4 py-16 text-center"><div className="inline-block h-5 w-5 animate-spin rounded-full spinner" /></td></tr>
|
||||||
) : data.map((item, i) => (
|
) : sorted.map((item, i) => (
|
||||||
<motion.tr key={item.rank} initial={{ opacity: 0 }} animate={{ opacity: 1 }} transition={{ delay: i * 0.02 }}
|
<motion.tr key={item.name} initial={{ opacity: 0 }} animate={{ opacity: 1 }} transition={{ delay: i * 0.02 }}
|
||||||
className="row-glow transition-colors" style={{ borderBottom: "1px solid var(--surface-border)", borderBottomWidth: "1px", opacity: 0.01 }}>
|
className="row-glow transition-colors" style={{ borderBottom: "1px solid var(--surface-border)" }}>
|
||||||
<td className="px-4 py-3 font-[family-name:var(--font-geist-mono)] text-xs" style={{ color: "var(--text-muted)" }}>{item.rank}</td>
|
<td className="px-4 py-3 font-[family-name:var(--font-geist-mono)] text-xs" style={{ color: "var(--text-muted)" }}>{i + 1}</td>
|
||||||
<td className="px-4 py-3">
|
<td className="px-4 py-3">
|
||||||
<Link href={detailHref(item)} className="transition-colors" style={{ color: "var(--text-accent)" }}>{item.name}</Link>
|
<Link href={detailHref(item)} className="transition-colors" style={{ color: "var(--text-accent)" }}>{item.name}</Link>
|
||||||
</td>
|
</td>
|
||||||
|
|||||||
Reference in New Issue
Block a user