From 259d6d8d5b1d390c0c59407e3a9fb596e0e48215 Mon Sep 17 00:00:00 2001 From: Jack-Khuu Date: Fri, 13 Feb 2026 12:36:31 -0800 Subject: [PATCH 1/3] push --- frontend/src/api/api.ts | 18 +- .../src/pages/leaderboard/Leaderboard.tsx | 108 +++--- .../leaderboard/components/AiTrendChart.tsx | 350 ------------------ .../leaderboard/components/UserTrendChart.tsx | 269 +++++++++++--- kernelboard/api/leaderboard.py | 12 +- 5 files changed, 285 insertions(+), 472 deletions(-) delete mode 100644 frontend/src/pages/leaderboard/components/AiTrendChart.tsx diff --git a/frontend/src/api/api.ts b/frontend/src/api/api.ts index 508902c..586d0fa 100644 --- a/frontend/src/api/api.ts +++ b/frontend/src/api/api.ts @@ -259,7 +259,7 @@ export async function fetchEvents(): Promise { return r.data; } -export interface AiTrendDataPoint { +export interface CustomTrendDataPoint { score: string; submission_id: number; submission_time: string; @@ -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 { - const res = await fetch(`/api/leaderboard/${leaderboardId}/ai_trend`); +export async function fetchCustomTrend(leaderboardId: string): Promise { + 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; @@ -294,7 +294,7 @@ export async function fetchAiTrend(leaderboardId: string): Promise ({ fontSize: "1.5rem", fontWeight: "bold", })); -type TabKey = "rankings" | "reference" | "submission" | "ai_trend"; +type TabKey = "rankings" | "reference" | "submission"; // Tab accessibility props function a11yProps(index: number) { @@ -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(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(); @@ -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); @@ -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: any) => 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 ; @@ -213,9 +210,6 @@ export default function Leaderboard() { - {showAiTrend && ( - - )} @@ -223,11 +217,20 @@ export default function Leaderboard() { {Object.entries(data.rankings).length > 0 ? ( - + <> + + + + + Performance Trend + + + + ) : ( @@ -305,25 +308,6 @@ export default function Leaderboard() { )} - {/* AI Trend Tab - only shown when showAiTrend=true */} - {showAiTrend && ( - - - - - AI Model Performance Trend - - - - - - - User Performance Trend - - - - - )} ); diff --git a/frontend/src/pages/leaderboard/components/AiTrendChart.tsx b/frontend/src/pages/leaderboard/components/AiTrendChart.tsx deleted file mode 100644 index 6bfd795..0000000 --- a/frontend/src/pages/leaderboard/components/AiTrendChart.tsx +++ /dev/null @@ -1,350 +0,0 @@ -import { useEffect, useMemo, useState } from "react"; -import ReactECharts from "echarts-for-react"; -import { - Box, - Typography, - CircularProgress, - FormControl, - InputLabel, - Select, - MenuItem, -} from "@mui/material"; -import { fetchAiTrend, type AiTrendResponse } from "../../../api/api"; -import { - formatMicrosecondsNum, - formatMicroseconds, -} from "../../../lib/utils/ranking"; -import { useThemeStore } from "../../../lib/store/themeStore"; - -interface AiTrendChartProps { - leaderboardId: string; - rankings?: Record>; -} - -// Generate a consistent color from a string using hash -function hashStringToColor(str: string): string { - let hash = 0; - for (let i = 0; i < str.length; i++) { - hash = str.charCodeAt(i) + ((hash << 5) - hash); - hash = hash & hash; - } - - const hue = Math.abs(hash) % 360; - const saturation = 65 + (Math.abs(hash >> 8) % 20); - const lightness = 45 + (Math.abs(hash >> 16) % 15); - - return `hsl(${hue}, ${saturation}%, ${lightness}%)`; -} - -export default function AiTrendChart({ leaderboardId, rankings }: AiTrendChartProps) { - const [data, setData] = useState(null); - const [loading, setLoading] = useState(true); - const [error, setError] = useState(null); - const [selectedGpuType, setSelectedGpuType] = useState(""); - const resolvedMode = useThemeStore((state) => state.resolvedMode); - const isDark = resolvedMode === "dark"; - const textColor = isDark ? "#e0e0e0" : "#333"; - - const gpuTypes = useMemo( - () => (data?.time_series ? Object.keys(data.time_series) : []), - [data?.time_series], - ); - - useEffect(() => { - if (gpuTypes.length > 0 && !selectedGpuType && data?.time_series) { - // Find the GPU type with the most unique users (from overall rankings) - // that also has actual AI data entries - let maxUniqueUsers = 0; - let defaultGpuType = ""; - - for (const gpuType of gpuTypes) { - const gpuData = data.time_series[gpuType]; - // Check if this GPU type has actual AI data entries - if (!gpuData || Object.keys(gpuData).length === 0) { - continue; - } - - let hasEntries = false; - for (const model of Object.keys(gpuData)) { - if (gpuData[model].length > 0) { - hasEntries = true; - break; - } - } - - if (!hasEntries) { - continue; - } - - // Count unique users from overall rankings (not just AI submissions) - const rankingsForGpu = rankings?.[gpuType]; - const uniqueUserCount = rankingsForGpu?.length ?? 0; - - if (uniqueUserCount > maxUniqueUsers) { - maxUniqueUsers = uniqueUserCount; - defaultGpuType = gpuType; - } - } - - // Fallback to first GPU type with AI data if no rankings match - if (!defaultGpuType) { - for (const gpuType of gpuTypes) { - const gpuData = data.time_series[gpuType]; - if (gpuData && Object.keys(gpuData).length > 0) { - for (const model of Object.keys(gpuData)) { - if (gpuData[model].length > 0) { - defaultGpuType = gpuType; - break; - } - } - if (defaultGpuType) break; - } - } - } - - // Final fallback to first GPU type - if (!defaultGpuType && gpuTypes.length > 0) { - defaultGpuType = gpuTypes[0]; - } - - setSelectedGpuType(defaultGpuType); - } - }, [gpuTypes, selectedGpuType, data?.time_series, rankings]); - - useEffect(() => { - const loadData = async () => { - setLoading(true); - setError(null); - try { - const result = await fetchAiTrend(leaderboardId); - setData(result); - } catch (err: unknown) { - setError(err instanceof Error ? err.message : "Failed to load data"); - } finally { - setLoading(false); - } - }; - loadData(); - }, [leaderboardId]); - - if (loading) { - return ( - - - - ); - } - - if (error) { - return ( - - {error} - - ); - } - - if ( - !data || - !data.time_series || - Object.keys(data.time_series).length === 0 - ) { - return ( - - No AI data available - - ); - } - - const selectedData = selectedGpuType ? data.time_series[selectedGpuType] : null; - if (!selectedData || Object.keys(selectedData).length === 0) { - return ( - - - - GPU Type - - - - - - No {selectedGpuType || "AI"} data available - - - - ); - } - - // Build series for ECharts - const series: Array> = []; - - Object.entries(selectedData).forEach(([model, dataPoints]) => { - const color = hashStringToColor(model); - - const sortedData = [...dataPoints].sort( - (a, b) => - new Date(a.submission_time).getTime() - - new Date(b.submission_time).getTime() - ); - - series.push({ - name: model, - type: "line", - data: sortedData.map((point) => ({ - value: [ - new Date(point.submission_time).getTime(), - parseFloat(point.score), - ], - gpu_type: point.gpu_type, - })), - smooth: true, - symbol: "circle", - symbolSize: 8, - lineStyle: { - width: 2, - color, - }, - itemStyle: { - color, - }, - }); - }); - - const option = { - title: { - text: `AI Model Performance Trend (${selectedGpuType})`, - left: "center", - textStyle: { - fontSize: 16, - fontWeight: "bold", - color: textColor, - }, - }, - tooltip: { - trigger: "item", - formatter: (params: { value: [number, number]; data: { gpu_type?: string }; seriesName: string }) => { - const date = new Date(params.value[0]); - const score = formatMicroseconds(params.value[1]); - const gpuType = params.data.gpu_type || "Unknown"; - return ` - ${params.seriesName}
- GPU Type: ${gpuType}
- Time: ${date.toLocaleString()}
- Score: ${score} - `; - }, - }, - legend: { - data: Object.keys(selectedData), - bottom: 0, - textStyle: { - color: textColor, - }, - }, - grid: { - left: "3%", - right: "4%", - bottom: "15%", - top: "15%", - containLabel: true, - }, - xAxis: { - type: "time", - name: "Submission Time", - nameLocation: "middle", - nameGap: 30, - nameTextStyle: { - color: textColor, - }, - axisLabel: { - color: textColor, - formatter: (value: number) => { - const date = new Date(value); - return `${date.getMonth() + 1}/${date.getDate()}`; - }, - }, - axisLine: { - lineStyle: { - color: textColor, - }, - }, - }, - yAxis: { - type: "value", - name: "Score (lower is better)", - nameLocation: "middle", - nameGap: 70, - nameTextStyle: { - color: textColor, - }, - axisLabel: { - color: textColor, - formatter: (value: number) => `${formatMicrosecondsNum(value)}μs`, - }, - axisLine: { - lineStyle: { - color: textColor, - }, - }, - splitLine: { - lineStyle: { - color: isDark ? "#444" : "#ccc", - }, - }, - }, - series, - }; - - return ( - - - - GPU Type - - - - - - ); -} diff --git a/frontend/src/pages/leaderboard/components/UserTrendChart.tsx b/frontend/src/pages/leaderboard/components/UserTrendChart.tsx index ac7cd44..9240beb 100644 --- a/frontend/src/pages/leaderboard/components/UserTrendChart.tsx +++ b/frontend/src/pages/leaderboard/components/UserTrendChart.tsx @@ -11,23 +11,43 @@ import { InputLabel, Select, MenuItem, + Button, } from "@mui/material"; import { fetchUserTrend, + fetchCustomTrend, searchUsers, type UserTrendResponse, + type CustomTrendResponse, type UserSearchResult, } from "../../../api/api"; + +// Display name prefix for custom (KernelAgent) entries +const CUSTOM_ENTRY_PREFIX = "KernelAgent"; + +// Simple option type - custom entries are identified by id starting with "custom_" to avoid collisions +interface TrendOption { + id: string; + label: string; +} import { formatMicrosecondsNum, formatMicroseconds, } from "../../../lib/utils/ranking"; import { useThemeStore } from "../../../lib/store/themeStore"; +interface RankingEntry { + user_name: string; + score: number; + file_name?: string; + submission_id?: number; +} + interface UserTrendChartProps { leaderboardId: string; - defaultUser?: { userId: string; username: string } | null; + defaultUsers?: Array<{ userId: string; username: string }>; defaultGpuType?: string | null; + rankings?: Record; } function hashStringToColor(str: string): string { @@ -44,20 +64,59 @@ function hashStringToColor(str: string): string { return `hsl(${hue}, ${saturation}%, ${lightness}%)`; } -export default function UserTrendChart({ leaderboardId, defaultUser, defaultGpuType }: UserTrendChartProps) { +export default function UserTrendChart({ leaderboardId, defaultUsers, defaultGpuType, rankings }: UserTrendChartProps) { const [data, setData] = useState(null); + const [customData, setCustomData] = useState(null); const [loading, setLoading] = useState(false); const [error, setError] = useState(null); const [selectedGpuType, setSelectedGpuType] = useState(defaultGpuType || ""); + const [resetting, setResetting] = useState(false); const resolvedMode = useThemeStore((state) => state.resolvedMode); const isDark = resolvedMode === "dark"; const textColor = isDark ? "#e0e0e0" : "#333"; - const [selectedUsers, setSelectedUsers] = useState([]); + const [selectedOptions, setSelectedOptions] = useState(() => { + if (defaultUsers && defaultUsers.length > 0) { + return defaultUsers.map((user) => ({ + id: user.userId, + label: user.username, + })); + } + return []; + }); const [userOptions, setUserOptions] = useState([]); const [searchLoading, setSearchLoading] = useState(false); const [inputValue, setInputValue] = useState(""); + // Fetch custom trend data on mount + useEffect(() => { + const loadCustomData = async () => { + try { + const result = await fetchCustomTrend(leaderboardId); + setCustomData(result); + } catch (err) { + console.error("Failed to load custom trend data:", err); + } + }; + loadCustomData(); + }, [leaderboardId]); + + // Build combined options: users + custom entries + // Custom entries are identified by id starting with "custom_" + // Sort all options alphabetically by label + const combinedOptions: TrendOption[] = [ + ...userOptions.map((u) => ({ + id: u.user_id, + label: u.username, + })), + ...(customData?.time_series?.[selectedGpuType] + ? Object.keys(customData.time_series[selectedGpuType]).map((model) => ({ + id: `custom_${model}`, + label: `${CUSTOM_ENTRY_PREFIX} - ${model}`, + })) + : []), + ].sort((a, b) => a.label.localeCompare(b.label)); + const loadData = useCallback( async (userIds: string[]) => { if (userIds.length === 0) { @@ -111,18 +170,30 @@ export default function UserTrendChart({ leaderboardId, defaultUser, defaultGpuT loadInitialUsers(); }, [leaderboardId]); - // Pre-select the default user if provided + // Load data for default users when they arrive (only when defaultUsers changes) useEffect(() => { - if (!defaultUser?.userId) return; - - const defaultUserAsSearchResult: UserSearchResult = { - user_id: defaultUser.userId, - username: defaultUser.username, - }; + if (defaultUsers && defaultUsers.length > 0) { + // Update selected options when defaults arrive + setSelectedOptions(defaultUsers.map((user) => ({ + id: user.userId, + label: user.username, + }))); + // Fetch data for the default users + const userIds = defaultUsers.map((u) => u.userId); + fetchUserTrend(leaderboardId, userIds).then((result) => { + setData(result); + }).catch((err) => { + console.error("Failed to load default users data:", err); + }); + } + }, [defaultUsers, leaderboardId]); - setSelectedUsers([defaultUserAsSearchResult]); - loadData([defaultUser.userId]); - }, [defaultUser, loadData]); + // Update GPU type when defaultGpuType changes + useEffect(() => { + if (defaultGpuType) { + setSelectedGpuType(defaultGpuType); + } + }, [defaultGpuType]); // Search users when input changes useEffect(() => { @@ -141,37 +212,88 @@ export default function UserTrendChart({ leaderboardId, defaultUser, defaultGpuT return () => clearTimeout(searchTimeout); }, [inputValue, leaderboardId]); - const handleUserSelectionChange = ( + // Helper to check if option is a custom entry (id starts with "custom_") + const isCustomEntry = (opt: TrendOption) => opt.id.startsWith("custom_"); + + const handleOptionSelectionChange = ( _event: React.SyntheticEvent, - newValue: UserSearchResult[] + newValue: TrendOption[] ) => { - setSelectedUsers(newValue); - const userIds = newValue.map((u) => u.user_id); + setSelectedOptions(newValue); + const userIds = newValue + .filter((opt) => !isCustomEntry(opt)) + .map((opt) => opt.id); loadData(userIds); }; - const gpuTypes = data?.time_series ? Object.keys(data.time_series) : []; + // Reset to top 5 users for current GPU type + const handleReset = async () => { + if (!rankings || !selectedGpuType) return; + + const gpuRankings = rankings[selectedGpuType]; + if (!gpuRankings || gpuRankings.length === 0) return; + + setResetting(true); + try { + // Get top 5 users for this GPU type + const topUserNames = gpuRankings + .slice(0, 5) + .map((r) => r.user_name) + .filter(Boolean); + + if (topUserNames.length === 0) return; + + // Search for each user by username to get their user_id + const userPromises = topUserNames.map((userName: string) => + searchUsers(leaderboardId, userName, 1) + ); + const results = await Promise.all(userPromises); + + const foundUsers = results + .filter((result) => result.users && result.users.length > 0) + .map((result) => ({ + id: result.users[0].user_id, + label: result.users[0].username, + })); + + setSelectedOptions(foundUsers); + const userIds = foundUsers.map((u) => u.id); + loadData(userIds); + } catch (err) { + console.error("Failed to reset to top users:", err); + } finally { + setResetting(false); + } + }; + + // Get selected users and custom entries separately + const selectedUsers = selectedOptions.filter((opt) => !isCustomEntry(opt)); + const selectedCustomEntries = selectedOptions.filter((opt) => isCustomEntry(opt)); + + // GPU types from user data or custom data + const gpuTypesFromUsers = data?.time_series ? Object.keys(data.time_series) : []; + const gpuTypesFromCustom = customData?.time_series ? Object.keys(customData.time_series) : []; + const gpuTypes = [...new Set([...gpuTypesFromUsers, ...gpuTypesFromCustom])]; const renderSearchInput = () => ( setInputValue(newInputValue)} - getOptionLabel={(option) => option.username} - isOptionEqualToValue={(option, value) => - option.user_id === value.user_id - } + getOptionLabel={(option) => option.label} + isOptionEqualToValue={(option, value) => option.id === value.id} loading={searchLoading} renderInput={(params) => ( @@ -205,11 +327,11 @@ export default function UserTrendChart({ leaderboardId, defaultUser, defaultGpuT const { key, ...restProps } = props; return (
  • - {option.username} + {option.label}
  • ); }} - noOptionsText="No users found" + noOptionsText="No contestants found" slotProps={{ listbox: { style: { maxHeight: 300 } }, }} @@ -231,10 +353,21 @@ export default function UserTrendChart({ leaderboardId, defaultUser, defaultGpuT )} + {rankings && selectedGpuType && ( + + )}
    ); - if (selectedUsers.length === 0) { + if (selectedUsers.length === 0 && selectedCustomEntries.length === 0) { return ( {renderSearchInput()} @@ -284,11 +417,11 @@ export default function UserTrendChart({ leaderboardId, defaultUser, defaultGpuT ); } - if ( - !data || - !data.time_series || - Object.keys(data.time_series).length === 0 - ) { + // When only custom entries are selected, we don't need user data + const hasUserData = data?.time_series && Object.keys(data.time_series).length > 0; + const hasCustomSelection = selectedCustomEntries.length > 0; + + if (!hasUserData && !hasCustomSelection) { return ( {renderSearchInput()} @@ -306,8 +439,11 @@ export default function UserTrendChart({ leaderboardId, defaultUser, defaultGpuT ); } - const gpuData = data.time_series[selectedGpuType]; - if (!gpuData || Object.keys(gpuData).length === 0) { + // Use user data GPU type or fall back to first available AI GPU type + const effectiveGpuType = selectedGpuType || gpuTypes[0] || ""; + const gpuData = data?.time_series?.[effectiveGpuType] || {}; + + if (Object.keys(gpuData).length === 0 && !hasCustomSelection) { return ( {renderSearchInput()} @@ -318,7 +454,7 @@ export default function UserTrendChart({ leaderboardId, defaultUser, defaultGpuT minHeight={400} > - No {selectedGpuType} data available for selected users + No {effectiveGpuType} data available for selected users @@ -361,7 +497,50 @@ export default function UserTrendChart({ leaderboardId, defaultUser, defaultGpuT }); }); - const chartTitle = `User Performance Trend (${selectedGpuType})`; + // Add custom entry series if selected + if (selectedCustomEntries.length > 0 && customData?.time_series?.[effectiveGpuType]) { + const customGpuData = customData.time_series[effectiveGpuType]; + + selectedCustomEntries.forEach((opt) => { + const model = opt.id.replace("custom_", ""); + const customDataPoints = customGpuData[model]; + if (!customDataPoints || customDataPoints.length === 0) return; + + const sortedCustomData = [...customDataPoints].sort( + (a, b) => + new Date(a.submission_time).getTime() - + new Date(b.submission_time).getTime() + ); + + const displayName = `${CUSTOM_ENTRY_PREFIX} - ${model}`; + const color = hashStringToColor(`custom_${model}`); + + series.push({ + name: displayName, + type: "line", + data: sortedCustomData.map((point) => ({ + value: [ + new Date(point.submission_time).getTime(), + parseFloat(point.score), + ], + gpu_type: point.gpu_type, + user_name: displayName, + })), + smooth: true, + symbol: "circle", + symbolSize: 8, + lineStyle: { + width: 2, + color, + }, + itemStyle: { + color, + }, + }); + }); + } + + const chartTitle = `Performance Trend (${selectedGpuType})`; const option = { title: { @@ -396,7 +575,7 @@ export default function UserTrendChart({ leaderboardId, defaultUser, defaultGpuT }, }, grid: { - left: "3%", + left: "5%", right: "4%", bottom: "15%", top: "15%", @@ -427,7 +606,7 @@ export default function UserTrendChart({ leaderboardId, defaultUser, defaultGpuT type: "value", name: "Score (lower is better)", nameLocation: "middle", - nameGap: 70, + nameGap: 90, nameTextStyle: { color: textColor, }, @@ -452,7 +631,7 @@ export default function UserTrendChart({ leaderboardId, defaultUser, defaultGpuT return ( {renderSearchInput()} - + ); } diff --git a/kernelboard/api/leaderboard.py b/kernelboard/api/leaderboard.py index 8ff0b0d..9aa34e1 100644 --- a/kernelboard/api/leaderboard.py +++ b/kernelboard/api/leaderboard.py @@ -198,12 +198,12 @@ def is_result_invalid(result): HARDCODED_USER_ID = "205851652572315658" -@leaderboard_bp.route("//ai_trend", methods=["GET"]) -def get_ai_trend(leaderboard_id: int): +@leaderboard_bp.route("//custom_trend", methods=["GET"]) +def get_custom_trend(leaderboard_id: int): """ - GET /leaderboard//ai_trend + GET /leaderboard//custom_trend - Returns time series data for ai_trend matching file name patterns like: + Returns time series data for custom submissions matching file name patterns like: - H100_claude-opus-4.5_ka_submission - H100_gpt-5-2_ka_submission - H100_gpt-5_ka_submission @@ -270,7 +270,7 @@ def get_ai_trend(leaderboard_id: int): total_time = (time.perf_counter() - total_start) * 1000 logger.info( - "[Perf] timeseries leaderboard_id=%s | query=%.2fms | total=%.2fms", + "[Perf] custom_trend leaderboard_id=%s | query=%.2fms | total=%.2fms", leaderboard_id, query_time, total_time, ) @@ -467,7 +467,7 @@ def group_multi_user_submissions( ) -> dict: """ Group submissions from multiple users by gpu_type with username as series. - Same format as ai_trend: { "H100": { "user1": [...], "user2": [...] } } + Same format as custom_trend: { "H100": { "user1": [...], "user2": [...] } } """ series = {} for user_id, items in items_by_user.items(): From 821036f1ff0e09b37f028c19ac6ba9ba92627a67 Mon Sep 17 00:00:00 2001 From: Jack-Khuu Date: Fri, 13 Feb 2026 12:43:20 -0800 Subject: [PATCH 2/3] lint --- .../pages/leaderboard/components/UserTrendChart.tsx | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/frontend/src/pages/leaderboard/components/UserTrendChart.tsx b/frontend/src/pages/leaderboard/components/UserTrendChart.tsx index 9240beb..ff03e07 100644 --- a/frontend/src/pages/leaderboard/components/UserTrendChart.tsx +++ b/frontend/src/pages/leaderboard/components/UserTrendChart.tsx @@ -39,15 +39,15 @@ import { useThemeStore } from "../../../lib/store/themeStore"; interface RankingEntry { user_name: string; score: number; - file_name?: string; - submission_id?: number; + file_name?: string | null; + submission_id?: number | null; } interface UserTrendChartProps { leaderboardId: string; - defaultUsers?: Array<{ userId: string; username: string }>; + defaultUsers?: Array<{ userId: string; username: string }> | null; defaultGpuType?: string | null; - rankings?: Record; + rankings?: Record | null; } function hashStringToColor(str: string): string { @@ -554,7 +554,7 @@ export default function UserTrendChart({ leaderboardId, defaultUsers, defaultGpu }, tooltip: { trigger: "item", - formatter: (params: { value: [number, number]; data: { gpu_type?: string; user_name?: string }; seriesName: string }) => { + formatter: (params: { value: [number, number]; data: { gpu_type?: string | null; user_name?: string | null}; seriesName: string }) => { const date = new Date(params.value[0]); const score = formatMicroseconds(params.value[1]); const gpuType = params.data.gpu_type || "Unknown"; From d8d282b312078ffb8545462306b7a55746e7eac5 Mon Sep 17 00:00:00 2001 From: Jack-Khuu Date: Fri, 13 Feb 2026 12:54:04 -0800 Subject: [PATCH 3/3] lint again --- frontend/src/pages/leaderboard/Leaderboard.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/frontend/src/pages/leaderboard/Leaderboard.tsx b/frontend/src/pages/leaderboard/Leaderboard.tsx index 8ac793d..4828115 100644 --- a/frontend/src/pages/leaderboard/Leaderboard.tsx +++ b/frontend/src/pages/leaderboard/Leaderboard.tsx @@ -130,7 +130,7 @@ export default function Leaderboard() { // Get top 5 users (sorted by score ascending) const topUserNames = mostActiveGpuRankings .slice(0, 5) - .map((r: any) => r.user_name) + .map((r) => r.user_name) .filter(Boolean); if (topUserNames.length === 0) return; @@ -227,7 +227,7 @@ export default function Leaderboard() { Performance Trend - +