This commit is contained in:
董宏凯 committed 2026-06-05 21:17:31 +08:00
commit cbbde887d4
5 files changed
+375 -31

No files matched your search

+3
View File
@@ -104,6 +104,9 @@ export interface AdminLeaderboardResponse {
range_days: number;
since: string;
until: string;
status?: "ready" | "running" | "refreshing";
generated_at?: string;
message?: string;
overview: AdminLeaderboardOverview;
users_by_activity: AdminLeaderboardUser[];
users_by_questions: AdminLeaderboardUser[];
+4
View File
@@ -33,6 +33,10 @@ export function useUserLeaderboard(
limit,
],
queryFn: () => getUserLeaderboard(range, limit),
refetchInterval: (query) => {
const status = query.state.data?.status;
return status === "running" || status === "refreshing" ? 3000 : false;
},
});
}
@@ -39,6 +39,7 @@ import {
DialogTrigger,
} from "@/components/ui/dialog";
import { Input } from "@/components/ui/input";
import { Pagination } from "@/components/ui/pagination";
import { ScrollArea } from "@/components/ui/scroll-area";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
@@ -63,6 +64,7 @@ type UserRankTab = "activity" | "questions" | "tokens" | "skills" | "efficiency"
type RangeState = { since: string; until: string; days?: number };
type LeaderboardAccent = "crimson" | "rose" | "amber" | "teal";
const DEFAULT_REPORT_MODEL = "__default__";
const LEADERBOARD_PAGE_SIZE = 20;
const accentClasses: Record<LeaderboardAccent, { soft: string; text: string; bar: string; border: string }> = {
crimson: {
@@ -722,11 +724,13 @@ function UserTable({
users,
tab,
selectedUserId,
rankOffset = 0,
onSelect,
}: {
users: AdminLeaderboardUser[];
tab: UserRankTab;
selectedUserId: string | null;
rankOffset?: number;
onSelect: (user: AdminLeaderboardUser) => void;
}) {
const max = Math.max(1, ...users.map((user) => userScore(user, tab)));
@@ -749,7 +753,7 @@ function UserTable({
)}
>
<div className="flex size-8 items-center justify-center rounded-md bg-[#FFF1F1] text-sm font-semibold text-[#C1121F] dark:bg-[#3A1215] dark:text-[#F2B8BB]">
{index + 1}
{rankOffset + index + 1}
</div>
<div className="min-w-0">
<div className="flex items-center gap-2">
@@ -1041,6 +1045,7 @@ export default function AdminUserLeaderboardPage() {
const [groupRules, setGroupRules] = useState("zero,two");
const [showGroupStats, setShowGroupStats] = useState(false);
const [selectedUserId, setSelectedUserId] = useState<string | null>(null);
const [userPage, setUserPage] = useState(1);
const apiRange = useMemo(
() => rangeForApi(range),
[range],
@@ -1082,7 +1087,23 @@ export default function AdminUserLeaderboardPage() {
}, [data, query, tab]);
const overview = data?.overview;
const leaderboardStatus = data?.status;
const leaderboardPending = leaderboardStatus === "running" || leaderboardStatus === "refreshing";
const leaderboardHasRows = rankedUsers.length > 0 || (overview?.total_runs ?? 0) > 0 || (overview?.total_users ?? 0) > 0;
const topUsers = rankedUsers.slice(0, 3);
const userPageCount = Math.max(1, Math.ceil(rankedUsers.length / LEADERBOARD_PAGE_SIZE));
const visibleUsers = useMemo(() => {
const start = (userPage - 1) * LEADERBOARD_PAGE_SIZE;
return rankedUsers.slice(start, start + LEADERBOARD_PAGE_SIZE);
}, [rankedUsers, userPage]);
useEffect(() => {
setUserPage(1);
}, [apiRange.since, apiRange.until, query, tab]);
useEffect(() => {
setUserPage((page) => Math.min(page, userPageCount));
}, [userPageCount]);
useEffect(() => {
if (rankedUsers.length === 0) {
@@ -1411,9 +1432,14 @@ export default function AdminUserLeaderboardPage() {
<ScrollArea className="min-h-0 flex-1">
<div className="space-y-5 p-6">
{isLoading && (
<div className="flex items-center justify-center gap-2 py-20 text-sm text-[#5F6368]">
<Loader2 className="size-4 animate-spin" />
加载统计数据
<div className="flex flex-col items-center justify-center gap-3 py-20 text-center text-sm text-[#5F6368]">
<div className="flex items-center gap-2">
<Loader2 className="size-4 animate-spin" />
<span>正在统计排行榜数据</span>
</div>
<div className="max-w-md text-xs leading-5 text-[#7A5A5A] dark:text-[#D8B8B8]">
线上用户较多或时间范围较大时,统计时间可能较长,请耐心等待。为避免影响其他接口,系统会限制排行榜统计的并发数量。
</div>
</div>
)}
@@ -1423,8 +1449,29 @@ export default function AdminUserLeaderboardPage() {
</div>
)}
{!isLoading && data && overview && (
{!isLoading && !error && leaderboardPending && !leaderboardHasRows && (
<div className="flex flex-col items-center justify-center gap-3 rounded-lg border border-[#F1D6D6] bg-white px-6 py-16 text-center text-sm text-[#5F6368] shadow-sm dark:border-[#462123] dark:bg-[#230D0F]">
<div className="flex items-center gap-2 font-medium text-[#202124] dark:text-white">
<Loader2 className="size-4 animate-spin text-[#C1121F]" />
<span>排行榜正在后台统计</span>
</div>
<div className="max-w-lg text-xs leading-5 text-[#7A5A5A] dark:text-[#D8B8B8]">
{data?.message || "统计任务已放到后台执行,页面会自动刷新结果。这样排行榜慢慢算,不会长时间占住当前接口请求。"}
</div>
</div>
)}
{!isLoading && data && overview && (!leaderboardPending || leaderboardHasRows) && (
<>
{leaderboardPending && leaderboardHasRows && (
<div className="flex flex-wrap items-center justify-between gap-3 rounded-lg border border-[#FDE68A] bg-[#FFFBEB] px-4 py-3 text-sm text-[#92400E] dark:border-amber-200/25 dark:bg-amber-300/10 dark:text-amber-100">
<div className="flex items-center gap-2">
<Loader2 className="size-4 animate-spin" />
<span>{data.message || "正在后台刷新排行榜,当前展示缓存结果。"}</span>
</div>
{data.generated_at && <span className="text-xs">缓存生成于 {fmtTime(data.generated_at)}</span>}
</div>
)}
<div className="grid gap-3 md:grid-cols-2 xl:grid-cols-4">
<StatCard icon={Users} accent="crimson" label="总用户" value={fmtNumber(overview.total_users)} note={`${fmtNumber(overview.active_users)} 位在当前时间段活跃`} />
<StatCard icon={MousePointerClick} accent="teal" label="使用次数" value={fmtNumber(overview.total_runs)} note={`${fmtNumber(overview.total_threads)} 个会话被创建`} />
@@ -1492,11 +1539,23 @@ export default function AdminUserLeaderboardPage() {
</div>
</div>
<UserTable
users={rankedUsers}
users={visibleUsers}
tab={tab}
selectedUserId={selectedUserId}
rankOffset={(userPage - 1) * LEADERBOARD_PAGE_SIZE}
onSelect={(user) => setSelectedUserId(user.user_id)}
/>
<div className="flex flex-wrap items-center justify-between gap-3 text-xs text-[#6B7280] dark:text-slate-400">
<span>
共 {fmtNumber(rankedUsers.length)} 位用户,当前第 {userPage} / {userPageCount} 页
</span>
<Pagination
page={userPage}
pageSize={LEADERBOARD_PAGE_SIZE}
total={rankedUsers.length}
onPageChange={setUserPage}
/>
</div>
</section>
<section className="overflow-hidden rounded-lg border border-[#F1D6D6] bg-white shadow-sm dark:border-[#462123] dark:bg-[#230D0F]">
+2 -2
View File
@@ -131,7 +131,7 @@ export default function AgentChatPage() {
const pendingMessageHandled = useRef(false);
useEffect(() => {
if (pendingMessageHandled.current) return;
if (!agent_id || !isNewThread || hasStartedConversation) return;
if (!agent_id || !isNewThread) return;
let pending: { agentId?: string; text?: string } | null = null;
try {
const raw = sessionStorage.getItem("pendingAgentMessage");
@@ -143,7 +143,7 @@ export default function AgentChatPage() {
pendingMessageHandled.current = true;
sessionStorage.removeItem("pendingAgentMessage");
handleSubmit({ text: pending.text, files: [] });
}, [agent_id, isNewThread, hasStartedConversation, handleSubmit]);
}, [agent_id, isNewThread, handleSubmit]);
const handleClarificationSubmit = useCallback(
(answer: string) => {