diff --git a/frontend/src/pages/leaderboard/Leaderboard.tsx b/frontend/src/pages/leaderboard/Leaderboard.tsx index 7e55744..2733713 100644 --- a/frontend/src/pages/leaderboard/Leaderboard.tsx +++ b/frontend/src/pages/leaderboard/Leaderboard.tsx @@ -10,7 +10,7 @@ import { } from "@mui/material"; import Grid from "@mui/material/Grid"; import { useEffect, useState, useMemo } from "react"; -import { fetchLeaderBoard } from "../../api/api"; +import { fetchLeaderBoard, searchUsers } from "../../api/api"; import { fetcherApiCallback } from "../../lib/hooks/useApi"; import { isExpired, toDateUtc } from "../../lib/date/utils"; import RankingsList from "./components/RankingLists"; @@ -70,6 +70,13 @@ export default function Leaderboard() { const isAuthed = !!(me && me.authenticated); const userId = me?.user?.identity ?? null; + // State for top user (strongest submission) and default GPU type + const [defaultUser, setDefaultUser] = useState<{ + userId: string; + username: string; + } | null>(null); + const [defaultGpuType, setDefaultGpuType] = useState(null); + // Sync tab state with query parameter const [searchParams, setSearchParams] = useSearchParams(); @@ -109,6 +116,51 @@ export default function Leaderboard() { if (id) call(id); }, [id]); + // Fetch top user (strongest submission) when rankings are available + // Select from the GPU with the most unique users + useEffect(() => { + const findTopUser = async () => { + if (!id || !data?.rankings) return; + + const gpuTypes = Object.keys(data.rankings); + if (gpuTypes.length === 0) return; + + // Find the GPU type with the most unique users + const mostActiveGpu = gpuTypes.reduce((currentMax, gpuType) => { + const rankings = data.rankings[gpuType]; + const userCount = rankings ? rankings.length : 0; + const maxCount = data.rankings[currentMax]?.length ?? 0; + return userCount > maxCount ? gpuType : currentMax; + }, gpuTypes[0]); + + // Set the default GPU type to the one with most users + setDefaultGpuType(mostActiveGpu); + + const mostActiveGpuRankings = data.rankings[mostActiveGpu]; + if (!mostActiveGpuRankings || mostActiveGpuRankings.length === 0) return; + + // The first item is the top user (sorted by score ascending) + const topUserName = mostActiveGpuRankings[0].user_name; + if (!topUserName) return; + + try { + // Search for the user by username to get their user_id + const result = await searchUsers(id, topUserName, 1); + if (result.users && result.users.length > 0) { + const foundUser = result.users[0]; + setDefaultUser({ + userId: foundUser.user_id, + username: foundUser.username, + }); + } + } catch (err) { + console.error("Failed to fetch top user:", err); + } + }; + + findTopUser(); + }, [id, data?.rankings]); + if (loading) return ; if (error) return ; @@ -267,7 +319,7 @@ export default function Leaderboard() { User Performance Trend - + diff --git a/frontend/src/pages/leaderboard/components/UserTrendChart.tsx b/frontend/src/pages/leaderboard/components/UserTrendChart.tsx index 94c9efd..e552feb 100644 --- a/frontend/src/pages/leaderboard/components/UserTrendChart.tsx +++ b/frontend/src/pages/leaderboard/components/UserTrendChart.tsx @@ -26,6 +26,8 @@ import { useThemeStore } from "../../../lib/store/themeStore"; interface UserTrendChartProps { leaderboardId: string; + defaultUser?: { userId: string; username: string } | null; + defaultGpuType?: string | null; } function hashStringToColor(str: string): string { @@ -42,11 +44,11 @@ function hashStringToColor(str: string): string { return `hsl(${hue}, ${saturation}%, ${lightness}%)`; } -export default function UserTrendChart({ leaderboardId }: UserTrendChartProps) { +export default function UserTrendChart({ leaderboardId, defaultUser, defaultGpuType }: UserTrendChartProps) { const [data, setData] = useState(null); const [loading, setLoading] = useState(false); const [error, setError] = useState(null); - const [selectedGpuType, setSelectedGpuType] = useState(""); + const [selectedGpuType, setSelectedGpuType] = useState(defaultGpuType || ""); const resolvedMode = useThemeStore((state) => state.resolvedMode); const isDark = resolvedMode === "dark"; const textColor = isDark ? "#e0e0e0" : "#333"; @@ -68,10 +70,21 @@ export default function UserTrendChart({ leaderboardId }: UserTrendChartProps) { try { const result = await fetchUserTrend(leaderboardId, userIds); setData(result); - // Set default GPU type to the first available + // Set default GPU type to the one with the most unique users const gpuTypes = Object.keys(result.time_series || {}); if (gpuTypes.length > 0 && !gpuTypes.includes(selectedGpuType)) { - setSelectedGpuType(gpuTypes[0]); + // Count unique users per GPU type + let maxUsers = 0; + let defaultGpu = gpuTypes[0]; + for (const gpuType of gpuTypes) { + const gpuData = result.time_series[gpuType]; + const userCount = gpuData ? Object.keys(gpuData).length : 0; + if (userCount > maxUsers) { + maxUsers = userCount; + defaultGpu = gpuType; + } + } + setSelectedGpuType(defaultGpu); } } catch (err: any) { setError(err.message || "Failed to load data"); @@ -98,6 +111,19 @@ export default function UserTrendChart({ leaderboardId }: UserTrendChartProps) { loadInitialUsers(); }, [leaderboardId]); + // Pre-select the default user if provided + useEffect(() => { + if (!defaultUser?.userId) return; + + const defaultUserAsSearchResult: UserSearchResult = { + user_id: defaultUser.userId, + username: defaultUser.username, + }; + + setSelectedUsers([defaultUserAsSearchResult]); + loadData([defaultUser.userId]); + }, [defaultUser, loadData]); + // Search users when input changes useEffect(() => { const searchTimeout = setTimeout(async () => {