Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
56 changes: 54 additions & 2 deletions frontend/src/pages/leaderboard/Leaderboard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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<string | null>(null);

// Sync tab state with query parameter
const [searchParams, setSearchParams] = useSearchParams();

Expand Down Expand Up @@ -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 <Loading />;
if (error) return <ErrorAlert status={errorStatus} message={error} />;

Expand Down Expand Up @@ -267,7 +319,7 @@ export default function Leaderboard() {
<Card sx={{ mt: 2 }}>
<CardContent>
<CardTitle fontWeight="bold">User Performance Trend</CardTitle>
<UserTrendChart leaderboardId={id!!} />
<UserTrendChart leaderboardId={id!!} defaultUser={defaultUser} defaultGpuType={defaultGpuType} />
</CardContent>
</Card>
</TabPanel>
Expand Down
34 changes: 30 additions & 4 deletions frontend/src/pages/leaderboard/components/UserTrendChart.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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<UserTrendResponse | null>(null);
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const [selectedGpuType, setSelectedGpuType] = useState<string>("");
const [selectedGpuType, setSelectedGpuType] = useState<string>(defaultGpuType || "");
const resolvedMode = useThemeStore((state) => state.resolvedMode);
const isDark = resolvedMode === "dark";
const textColor = isDark ? "#e0e0e0" : "#333";
Expand All @@ -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");
Expand All @@ -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 () => {
Expand Down
Loading