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
18 changes: 9 additions & 9 deletions frontend/src/api/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -259,7 +259,7 @@ export async function fetchEvents(): Promise<DiscordEvent[]> {
return r.data;
}

export interface AiTrendDataPoint {
export interface CustomTrendDataPoint {
score: string;
submission_id: number;
submission_time: string;
Expand All @@ -269,23 +269,23 @@ export interface AiTrendDataPoint {
model?: string;
}

export interface AiTrendTimeSeries {
export interface CustomTrendTimeSeries {
[gpuType: string]: {
[model: string]: AiTrendDataPoint[];
[model: string]: CustomTrendDataPoint[];
};
}

export interface AiTrendResponse {
export interface CustomTrendResponse {
leaderboard_id: number;
time_series: AiTrendTimeSeries;
time_series: CustomTrendTimeSeries;
}

export async function fetchAiTrend(leaderboardId: string): Promise<AiTrendResponse> {
const res = await fetch(`/api/leaderboard/${leaderboardId}/ai_trend`);
export async function fetchCustomTrend(leaderboardId: string): Promise<CustomTrendResponse> {
const res = await fetch(`/api/leaderboard/${leaderboardId}/custom_trend`);
if (!res.ok) {
const json = await res.json();
const message = json?.message || "Unknown error";
throw new APIError(`Failed to fetch AI trend: ${message}`, res.status);
throw new APIError(`Failed to fetch custom trend: ${message}`, res.status);
}
const r = await res.json();
return r.data;
Expand All @@ -294,7 +294,7 @@ export async function fetchAiTrend(leaderboardId: string): Promise<AiTrendRespon
export interface UserTrendResponse {
leaderboard_id: number;
user_ids: string[];
time_series: AiTrendTimeSeries;
time_series: CustomTrendTimeSeries;
}

export async function fetchUserTrend(
Expand Down
108 changes: 46 additions & 62 deletions frontend/src/pages/leaderboard/Leaderboard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import {
Typography,
} from "@mui/material";
import Grid from "@mui/material/Grid";
import { useEffect, useState, useMemo } from "react";
import { useEffect, useState } from "react";
import { fetchLeaderBoard, searchUsers } from "../../api/api";
import { fetcherApiCallback } from "../../lib/hooks/useApi";
import { isExpired, toDateUtc } from "../../lib/date/utils";
Expand All @@ -24,14 +24,13 @@ import { SubmissionMode } from "../../lib/types/mode";
import { useAuthStore } from "../../lib/store/authStore";
import SubmissionHistorySection from "./components/submission-history/SubmissionHistorySection";
import LeaderboardSubmit from "./components/LeaderboardSubmit";
import AiTrendChart from "./components/AiTrendChart";
import UserTrendChart from "./components/UserTrendChart";
export const CardTitle = styled(Typography)(() => ({
fontSize: "1.5rem",
fontWeight: "bold",
}));

type TabKey = "rankings" | "reference" | "submission" | "ai_trend";
type TabKey = "rankings" | "reference" | "submission";

// Tab accessibility props
function a11yProps(index: number) {
Expand Down Expand Up @@ -70,27 +69,16 @@ 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);
// State for top users (strongest submissions) and default GPU type
const [defaultUsers, setDefaultUsers] = useState<
Array<{ userId: string; username: string }>
>([]);
const [defaultGpuType, setDefaultGpuType] = useState<string | null>(null);

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

// Check if AI Trend should be shown
const showAiTrend = searchParams.get("showAiTrend") === "true";

// Build tab keys dynamically based on showAiTrend
const TAB_KEYS: TabKey[] = useMemo(() => {
const keys: TabKey[] = ["rankings", "reference", "submission"];
if (showAiTrend) {
keys.push("ai_trend");
}
return keys;
}, [showAiTrend]);
const TAB_KEYS: TabKey[] = ["rankings", "reference", "submission"];

const initialTabFromUrl = ((): TabKey => {
const t = (searchParams.get("tab") || "").toLowerCase();
Expand All @@ -116,10 +104,10 @@ export default function Leaderboard() {
if (id) call(id);
}, [id, call]);

// Fetch top user (strongest submission) when rankings are available
// Fetch top users (strongest submissions) when rankings are available
// Select from the GPU with the most unique users
useEffect(() => {
const findTopUser = async () => {
const findTopUsers = async () => {
if (!id || !data?.rankings) return;

const gpuTypes = Object.keys(data.rankings);
Expand All @@ -139,26 +127,35 @@ export default function Leaderboard() {
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;
// Get top 5 users (sorted by score ascending)
const topUserNames = mostActiveGpuRankings
.slice(0, 5)
.map((r) => r.user_name)
.filter(Boolean);

if (topUserNames.length === 0) 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,
});
}
// Search for each user by username to get their user_id
const userPromises = topUserNames.map((userName: string) =>
searchUsers(id, userName, 1)
);
const results = await Promise.all(userPromises);

const foundUsers = results
.filter((result) => result.users && result.users.length > 0)
.map((result) => ({
userId: result.users[0].user_id,
username: result.users[0].username,
}));

setDefaultUsers(foundUsers);
} catch (err) {
console.error("Failed to fetch top user:", err);
console.error("Failed to fetch top users:", err);
}
};

findTopUser();
findTopUsers();
}, [id, data?.rankings]);

if (loading) return <Loading />;
Expand Down Expand Up @@ -213,21 +210,27 @@ export default function Leaderboard() {
<Tab label="Rankings" value="rankings" {...a11yProps(0)} />
<Tab label="Reference" value="reference" {...a11yProps(1)} />
<Tab label="Submission" value="submission" {...a11yProps(2)} />
{showAiTrend && (
<Tab label="AI Trend" value="ai_trend" {...a11yProps(3)} />
)}
</Tabs>
</Box>

{/* Ranking Tab */}
<TabPanel value={tab} tabKey="rankings">
<Box>
{Object.entries(data.rankings).length > 0 ? (
<RankingsList
rankings={data.rankings}
leaderboardId={id}
deadline={data.deadline}
/>
<>
<RankingsList
rankings={data.rankings}
leaderboardId={id}
deadline={data.deadline}
/>
<Box sx={{ my: 4, borderTop: 1, borderColor: "divider" }} />
<Card>
<CardContent>
<CardTitle fontWeight="bold">Performance Trend</CardTitle>
<UserTrendChart leaderboardId={id!} defaultUsers={defaultUsers} defaultGpuType={defaultGpuType} rankings={data.rankings} />
</CardContent>
</Card>
</>
) : (
<Box display="flex" flexDirection="column" alignItems="center">
<Typography variant="h6" fontWeight="bold">
Expand Down Expand Up @@ -305,25 +308,6 @@ export default function Leaderboard() {
)}
</TabPanel>

{/* AI Trend Tab - only shown when showAiTrend=true */}
{showAiTrend && (
<TabPanel value={tab} tabKey="ai_trend">
<Card>
<CardContent>
<CardTitle fontWeight="bold">
AI Model Performance Trend
</CardTitle>
<AiTrendChart leaderboardId={id!} rankings={data.rankings} />
</CardContent>
</Card>
<Card sx={{ mt: 2 }}>
<CardContent>
<CardTitle fontWeight="bold">User Performance Trend</CardTitle>
<UserTrendChart leaderboardId={id!} defaultUser={defaultUser} defaultGpuType={defaultGpuType} />
</CardContent>
</Card>
</TabPanel>
)}
</Box>
</ConstrainedContainer>
);
Expand Down
Loading
Loading