From 74bbd14425123d76867527afb632a0ec2dd3c90e Mon Sep 17 00:00:00 2001 From: Yang Wang Date: Mon, 9 Feb 2026 15:31:56 -0800 Subject: [PATCH 1/3] hidemetadata --- kernelboard/api/leaderboard.py | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/kernelboard/api/leaderboard.py b/kernelboard/api/leaderboard.py index eb475b7..a739940 100644 --- a/kernelboard/api/leaderboard.py +++ b/kernelboard/api/leaderboard.py @@ -281,21 +281,22 @@ def get_ai_trend(leaderboard_id: int): def parse_model_from_filename(file_name: str) -> str: """ - Extract model name from file names ending with ka_submission.py: - - trimul_H100_claude-opus-4.5_ka_submission.py -> claude-opus-4.5 + Extract model name - the segment right before _ka_submission.py + Examples: + - matmul_py_H100_claude-opus-4.5_ka_submission.py -> claude-opus-4.5 - trimul_H100_gpt-52_ka_submission.py -> gpt-52 - - trimul_H100_gpt-5_ka_submission.py -> gpt-5 Returns None if file doesn't match pattern. """ - if not file_name or not file_name.endswith("_ka_submission.py"): + suffix = "_ka_submission.py" + if not file_name or not file_name.endswith(suffix): return None - # Extract model name: everything between last GPU type and _ka_submission - # Pattern: {anything}_{gpu}_{model}_ka_submission.py - pattern = r"^.+_[A-Za-z0-9]+_(.+?)_ka_submission\.py$" - match = re.match(pattern, file_name) - if match: - return match.group(1) + # Remove the suffix and get everything before it + base = file_name[:-len(suffix)] + # Split by underscore and get the last segment + parts = base.rsplit("_", 1) + if len(parts) == 2: + return parts[1] return None From 2a6b50c542e00e5fabc986c3d3b905f4697bfafe Mon Sep 17 00:00:00 2001 From: Yang Wang Date: Mon, 9 Feb 2026 16:14:18 -0800 Subject: [PATCH 2/3] hidemetadata --- frontend/package-lock.json | 78 ++++--- frontend/package.json | 2 + frontend/src/api/api.ts | 4 +- frontend/src/components/app-layout/NavBar.tsx | 10 +- .../common/LoadingCircleProgress.tsx | 2 +- frontend/src/components/common/loading.tsx | 2 +- .../src/components/common/styles/theme.tsx | 6 +- .../markdown-renderer/MarkdownRenderer.tsx | 5 +- frontend/src/lib/store/themeStore.ts | 3 +- frontend/src/lib/utils/ranking.ts | 4 + .../src/pages/leaderboard/Leaderboard.tsx | 62 +++-- .../leaderboard/components/AiTrendChart.tsx | 212 ++++++++++++++++++ .../leaderboard/components/CodeDialog.tsx | 9 +- .../leaderboard/components/RankingLists.tsx | 6 +- frontend/src/pages/lectures/Lectures.tsx | 53 +++-- .../pages/working-groups/WorkingGroups.tsx | 6 +- kernelboard/api/leaderboard.py | 1 + 17 files changed, 393 insertions(+), 72 deletions(-) create mode 100644 frontend/src/pages/leaderboard/components/AiTrendChart.tsx diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 2ac72a3..9db4006 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -14,6 +14,8 @@ "@mui/material": "^7.2.0", "@types/react-syntax-highlighter": "^15.5.13", "dayjs": "^1.11.13", + "echarts": "^6.0.0", + "echarts-for-react": "^3.0.6", "imurmurhash": "^0.1.4", "katex": "^0.16.28", "react": "^19.1.0", @@ -115,7 +117,6 @@ "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.0.tgz", "integrity": "sha512-UlLAnTPrFdNGoFtbSXwcGFQBtQZJCNjaN6hQNP3UPvuNXT1i82N26KL3dZeIpNalWywr9IuQuncaAfUaS1g6sQ==", "dev": true, - "peer": true, "dependencies": { "@ampproject/remapping": "^2.2.0", "@babel/code-frame": "^7.27.1", @@ -440,7 +441,6 @@ "url": "https://opencollective.com/csstools" } ], - "peer": true, "engines": { "node": ">=18" }, @@ -463,7 +463,6 @@ "url": "https://opencollective.com/csstools" } ], - "peer": true, "engines": { "node": ">=18" } @@ -520,7 +519,6 @@ "version": "11.14.0", "resolved": "https://registry.npmjs.org/@emotion/react/-/react-11.14.0.tgz", "integrity": "sha512-O000MLDBDdk/EohJPFUqvnp4qnHeYkVP5B0xEG0D/L7cOKP9kefu2DXn8dj74cQfsEzUqh+sr1RzFqiL1o+PpA==", - "peer": true, "dependencies": { "@babel/runtime": "^7.18.3", "@emotion/babel-plugin": "^11.13.5", @@ -561,7 +559,6 @@ "version": "11.14.1", "resolved": "https://registry.npmjs.org/@emotion/styled/-/styled-11.14.1.tgz", "integrity": "sha512-qEEJt42DuToa3gurlH4Qqc1kVpNq8wO8cJtDzU46TjlzWjDlsVyevtYCRijVq3SrHsROS+gVQ8Fnea108GnKzw==", - "peer": true, "dependencies": { "@babel/runtime": "^7.18.3", "@emotion/babel-plugin": "^11.13.5", @@ -1292,7 +1289,6 @@ "version": "7.2.0", "resolved": "https://registry.npmjs.org/@mui/material/-/material-7.2.0.tgz", "integrity": "sha512-NTuyFNen5Z2QY+I242MDZzXnFIVIR6ERxo7vntFi9K1wCgSwvIl0HcAO2OOydKqqKApE6omRiYhpny1ZhGuH7Q==", - "peer": true, "dependencies": { "@babel/runtime": "^7.27.6", "@mui/core-downloads-tracker": "^7.2.0", @@ -1893,7 +1889,8 @@ "version": "5.0.4", "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz", "integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==", - "dev": true + "dev": true, + "peer": true }, "node_modules/@types/babel__core": { "version": "7.20.5", @@ -2017,7 +2014,6 @@ "resolved": "https://registry.npmjs.org/@types/node/-/node-24.0.13.tgz", "integrity": "sha512-Qm9OYVOFHFYg3wJoTSrz80hoec5Lia/dPp84do3X7dZvLikQvM1YpmvTBEdIr/e+U8HTkFjLHLnl78K/qjf+jQ==", "dev": true, - "peer": true, "dependencies": { "undici-types": "~7.8.0" } @@ -2036,7 +2032,6 @@ "version": "19.1.8", "resolved": "https://registry.npmjs.org/@types/react/-/react-19.1.8.tgz", "integrity": "sha512-AwAfQ2Wa5bCx9WP8nZL2uMZWod7J7/JSplxbTmBQ5ms6QpqNYm672H0Vu9ZVKVngQ+ii4R/byguVEUZQyeg44g==", - "peer": true, "dependencies": { "csstype": "^3.0.2" } @@ -2046,7 +2041,6 @@ "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.1.6.tgz", "integrity": "sha512-4hOiT/dwO8Ko0gV1m/TJZYk3y0KBnY9vzDh7W+DH17b2HFSOGgdj33dhihPeuy3l0q23+4e+hoXHV6hCC4dCXw==", "dev": true, - "peer": true, "peerDependencies": { "@types/react": "^19.0.0" } @@ -2116,7 +2110,6 @@ "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.36.0.tgz", "integrity": "sha512-FuYgkHwZLuPbZjQHzJXrtXreJdFMKl16BFYyRrLxDhWr6Qr7Kbcu2s1Yhu8tsiMXw1S0W1pjfFfYEt+R604s+Q==", "dev": true, - "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.36.0", "@typescript-eslint/types": "8.36.0", @@ -2468,7 +2461,6 @@ "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "dev": true, - "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -2515,6 +2507,7 @@ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", "dev": true, + "peer": true, "engines": { "node": ">=8" } @@ -2628,7 +2621,6 @@ "url": "https://github.com/sponsors/ai" } ], - "peer": true, "dependencies": { "caniuse-lite": "^1.0.30001726", "electron-to-chromium": "^1.5.173", @@ -2980,7 +2972,8 @@ "version": "0.5.16", "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz", "integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==", - "dev": true + "dev": true, + "peer": true }, "node_modules/dom-helpers": { "version": "5.2.1", @@ -2991,6 +2984,28 @@ "csstype": "^3.0.2" } }, + "node_modules/echarts": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/echarts/-/echarts-6.0.0.tgz", + "integrity": "sha512-Tte/grDQRiETQP4xz3iZWSvoHrkCQtwqd6hs+mifXcjrCuo2iKWbajFObuLJVBlDIJlOzgQPd1hsaKt/3+OMkQ==", + "dependencies": { + "tslib": "2.3.0", + "zrender": "6.0.0" + } + }, + "node_modules/echarts-for-react": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/echarts-for-react/-/echarts-for-react-3.0.6.tgz", + "integrity": "sha512-4zqLgTGWS3JvkQDXjzkR1k1CHRdpd6by0988TWMJgnvDytegWLbeP/VNZmMa+0VJx2eD7Y632bi2JquXDgiGJg==", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "size-sensor": "^1.0.1" + }, + "peerDependencies": { + "echarts": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0", + "react": "^15.0.0 || >=16.0.0" + } + }, "node_modules/electron-to-chromium": { "version": "1.5.182", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.182.tgz", @@ -3088,7 +3103,6 @@ "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.31.0.tgz", "integrity": "sha512-QldCVh/ztyKJJZLr4jXNUByx3gR+TDYZCRXEktiZoUR3PGy4qCmSbkxcIle8GEwGpb5JBZazlaJ/CxLidXdEbQ==", "dev": true, - "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.12.1", @@ -3287,8 +3301,7 @@ "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", - "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", - "dev": true + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" }, "node_modules/fast-glob": { "version": "3.3.3", @@ -3995,7 +4008,6 @@ "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-26.1.0.tgz", "integrity": "sha512-Cvc9WUhxSMEo4McES3P7oK3QaXldCfNWp7pl2NNeiIFlCoLr3kfq9kb1fxftiwk1FLV7CvpvDfonxtzUDeSOPg==", "dev": true, - "peer": true, "dependencies": { "cssstyle": "^4.2.1", "data-urls": "^5.0.0", @@ -4200,6 +4212,7 @@ "resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz", "integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==", "dev": true, + "peer": true, "bin": { "lz-string": "bin/bin.js" } @@ -5399,6 +5412,7 @@ "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.5.1.tgz", "integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==", "dev": true, + "peer": true, "dependencies": { "ansi-regex": "^5.0.1", "ansi-styles": "^5.0.0", @@ -5413,6 +5427,7 @@ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", "dev": true, + "peer": true, "engines": { "node": ">=10" }, @@ -5424,7 +5439,8 @@ "version": "17.0.2", "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", - "dev": true + "dev": true, + "peer": true }, "node_modules/prismjs": { "version": "1.30.0", @@ -5492,7 +5508,6 @@ "version": "19.1.0", "resolved": "https://registry.npmjs.org/react/-/react-19.1.0.tgz", "integrity": "sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg==", - "peer": true, "engines": { "node": ">=0.10.0" } @@ -5501,7 +5516,6 @@ "version": "19.1.0", "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.1.0.tgz", "integrity": "sha512-Xs1hdnE+DyKgeHJeJznQmYMIBG3TKIHJJT95Q58nHLSrElKlGQqDTR2HQ9fx5CN/Gk6Vh/kupBTDLU11/nDk/g==", - "peer": true, "dependencies": { "scheduler": "^0.26.0" }, @@ -6111,6 +6125,11 @@ "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", "dev": true }, + "node_modules/size-sensor": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/size-sensor/-/size-sensor-1.0.3.tgz", + "integrity": "sha512-+k9mJ2/rQMiRmQUcjn+qznch260leIXY8r4FyYKKyRBO/s5UoeMAHGkCJyE1R/4wrIhTJONfyloY55SkE7ve3A==" + }, "node_modules/source-map": { "version": "0.5.7", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", @@ -6301,7 +6320,6 @@ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz", "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==", "dev": true, - "peer": true, "engines": { "node": ">=12" }, @@ -6420,6 +6438,11 @@ "typescript": ">=4.8.4" } }, + "node_modules/tslib": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.0.tgz", + "integrity": "sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg==" + }, "node_modules/type-check": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", @@ -6437,7 +6460,6 @@ "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz", "integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==", "dev": true, - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -6666,7 +6688,6 @@ "resolved": "https://registry.npmjs.org/vite/-/vite-6.3.5.tgz", "integrity": "sha512-cZn6NDFE7wdTpINgs++ZJ4N49W2vRp8LCKrn3Ob1kYNtOo21vfDoaV5GzBfLU4MovSAB8uNRm4jgzVQZ+mBzPQ==", "dev": true, - "peer": true, "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.4.4", @@ -6777,7 +6798,6 @@ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz", "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==", "dev": true, - "peer": true, "engines": { "node": ">=12" }, @@ -7036,6 +7056,14 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/zrender": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/zrender/-/zrender-6.0.0.tgz", + "integrity": "sha512-41dFXEEXuJpNecuUQq6JlbybmnHaqqpGlbH1yxnA5V9MMP4SbohSVZsJIwz+zdjQXSSlR1Vc34EgH1zxyTDvhg==", + "dependencies": { + "tslib": "2.3.0" + } + }, "node_modules/zustand": { "version": "5.0.7", "resolved": "https://registry.npmjs.org/zustand/-/zustand-5.0.7.tgz", diff --git a/frontend/package.json b/frontend/package.json index f550189..5c1cf48 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -18,6 +18,8 @@ "@mui/material": "^7.2.0", "@types/react-syntax-highlighter": "^15.5.13", "dayjs": "^1.11.13", + "echarts": "^6.0.0", + "echarts-for-react": "^3.0.6", "imurmurhash": "^0.1.4", "katex": "^0.16.28", "react": "^19.1.0", diff --git a/frontend/src/api/api.ts b/frontend/src/api/api.ts index 493be66..718cd16 100644 --- a/frontend/src/api/api.ts +++ b/frontend/src/api/api.ts @@ -73,7 +73,9 @@ export async function fetchAllNews(): Promise { return r.data; } -export async function fetchLeaderboardSummaries(useV1: boolean = false): Promise { +export async function fetchLeaderboardSummaries( + useV1: boolean = false, +): Promise { const start = performance.now(); const url = useV1 ? "/api/leaderboard-summaries?v1_query" diff --git a/frontend/src/components/app-layout/NavBar.tsx b/frontend/src/components/app-layout/NavBar.tsx index 3579144..bd67618 100644 --- a/frontend/src/components/app-layout/NavBar.tsx +++ b/frontend/src/components/app-layout/NavBar.tsx @@ -111,7 +111,15 @@ export default function NavBar() { ))} - + {modeIcon} diff --git a/frontend/src/components/common/LoadingCircleProgress.tsx b/frontend/src/components/common/LoadingCircleProgress.tsx index 069f1db..150734d 100644 --- a/frontend/src/components/common/LoadingCircleProgress.tsx +++ b/frontend/src/components/common/LoadingCircleProgress.tsx @@ -9,7 +9,7 @@ export default function LoadingCircleProgress({ }) { const displayMessage = useMemo( () => message ?? getRandomGpuJoke(), - [message] + [message], ); return ( diff --git a/frontend/src/components/common/loading.tsx b/frontend/src/components/common/loading.tsx index b58023b..1aad11d 100644 --- a/frontend/src/components/common/loading.tsx +++ b/frontend/src/components/common/loading.tsx @@ -30,7 +30,7 @@ type LoadingProps = { export default function Loading({ message }: LoadingProps) { const displayMessage = useMemo( () => message ?? getRandomGpuJoke(), - [message] + [message], ); return ( diff --git a/frontend/src/components/common/styles/theme.tsx b/frontend/src/components/common/styles/theme.tsx index 48f1cf8..6d864fa 100644 --- a/frontend/src/components/common/styles/theme.tsx +++ b/frontend/src/components/common/styles/theme.tsx @@ -1,4 +1,8 @@ -import { createTheme, type Components, type PaletteMode } from "@mui/material/styles"; +import { + createTheme, + type Components, + type PaletteMode, +} from "@mui/material/styles"; const colorPalette = { primary: "#5865F2", // Blurple, Discord's brand color diff --git a/frontend/src/components/markdown-renderer/MarkdownRenderer.tsx b/frontend/src/components/markdown-renderer/MarkdownRenderer.tsx index f84a40c..057be1b 100644 --- a/frontend/src/components/markdown-renderer/MarkdownRenderer.tsx +++ b/frontend/src/components/markdown-renderer/MarkdownRenderer.tsx @@ -56,7 +56,10 @@ const MarkdownRenderer: React.FC = ({ components={{ a: ({ node, ...props }) => ( ), diff --git a/frontend/src/lib/store/themeStore.ts b/frontend/src/lib/store/themeStore.ts index af66bab..f1b3d9c 100644 --- a/frontend/src/lib/store/themeStore.ts +++ b/frontend/src/lib/store/themeStore.ts @@ -22,7 +22,8 @@ function resolveMode(mode: ThemeMode): "light" | "dark" { function loadSavedMode(): ThemeMode { try { const saved = localStorage.getItem("theme-mode"); - if (saved === "light" || saved === "dark" || saved === "system") return saved; + if (saved === "light" || saved === "dark" || saved === "system") + return saved; } catch { // localStorage unavailable } diff --git a/frontend/src/lib/utils/ranking.ts b/frontend/src/lib/utils/ranking.ts index f10bb32..461ec5d 100644 --- a/frontend/src/lib/utils/ranking.ts +++ b/frontend/src/lib/utils/ranking.ts @@ -5,3 +5,7 @@ export const formatMicroseconds = (score: number): string => { return `${(score * 1_000_000).toFixed(3)}μs`; }; + +export const formatMicrosecondsNum = (score: number): number => { + return parseFloat((score * 1_000_000).toFixed(3)); +}; diff --git a/frontend/src/pages/leaderboard/Leaderboard.tsx b/frontend/src/pages/leaderboard/Leaderboard.tsx index b660393..0a972cc 100644 --- a/frontend/src/pages/leaderboard/Leaderboard.tsx +++ b/frontend/src/pages/leaderboard/Leaderboard.tsx @@ -9,8 +9,8 @@ import { Typography, } from "@mui/material"; import Grid from "@mui/material/Grid"; -import { useEffect, useState } from "react"; -import { fetchCodes, fetchLeaderBoard } from "../../api/api"; +import { useEffect, useState, useMemo } from "react"; +import { fetchLeaderBoard } from "../../api/api"; import { fetcherApiCallback } from "../../lib/hooks/useApi"; import { isExpired, toDateUtc } from "../../lib/date/utils"; import RankingsList from "./components/RankingLists"; @@ -24,13 +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"; export const CardTitle = styled(Typography)(() => ({ fontSize: "1.5rem", fontWeight: "bold", })); -const TAB_KEYS = ["rankings", "reference", "submission"] as const; -type TabKey = (typeof TAB_KEYS)[number]; +type TabKey = "rankings" | "reference" | "submission" | "ai_trend"; // Tab accessibility props function a11yProps(index: number) { @@ -44,18 +44,18 @@ function a11yProps(index: number) { function TabPanel(props: { children?: React.ReactNode; value: string; - index: number; + tabKey: string; }) { - const { children, value, index, ...other } = props; + const { children, value, tabKey, ...other } = props; return ( ); } @@ -71,6 +71,19 @@ export default function Leaderboard() { // 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 initialTabFromUrl = ((): TabKey => { const t = (searchParams.get("tab") || "").toLowerCase(); return (TAB_KEYS as readonly string[]).includes(t) @@ -147,14 +160,21 @@ export default function Leaderboard() { + {showAiTrend && ( + + )} {/* Ranking Tab */} - + {Object.entries(data.rankings).length > 0 ? ( - + ) : ( @@ -169,7 +189,7 @@ export default function Leaderboard() { {/* Reference Implementation Tab */} - + Reference Implementation @@ -181,7 +201,7 @@ export default function Leaderboard() { {/* Submission Tab */} - + {!isAuthed ? (
please login to submit
) : ( @@ -231,6 +251,20 @@ export default function Leaderboard() { )}
+ + {/* AI Trend Tab - only shown when showAiTrend=true */} + {showAiTrend && ( + + + + + AI Model Performance Trend + + + + + + )}
); diff --git a/frontend/src/pages/leaderboard/components/AiTrendChart.tsx b/frontend/src/pages/leaderboard/components/AiTrendChart.tsx new file mode 100644 index 0000000..a11c10d --- /dev/null +++ b/frontend/src/pages/leaderboard/components/AiTrendChart.tsx @@ -0,0 +1,212 @@ +import { useEffect, useState } from "react"; +import ReactECharts from "echarts-for-react"; +import { Box, Typography, CircularProgress } from "@mui/material"; +import { fetchAiTrend, type AiTrendResponse } from "../../../api/api"; +import { + formatMicrosecondsNum, + formatMicroseconds, +} from "../../../lib/utils/ranking"; + +interface AiTrendChartProps { + leaderboardId: string; +} + +// 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; // Convert to 32bit integer + } + + // Generate HSL color with good saturation and lightness for visibility + const hue = Math.abs(hash) % 360; + const saturation = 65 + (Math.abs(hash >> 8) % 20); // 65-85% + const lightness = 45 + (Math.abs(hash >> 16) % 15); // 45-60% + + return `hsl(${hue}, ${saturation}%, ${lightness}%)`; +} + +export default function AiTrendChart({ leaderboardId }: AiTrendChartProps) { + const [data, setData] = useState(null); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + useEffect(() => { + const loadData = async () => { + try { + setLoading(true); + setError(null); + const result = await fetchAiTrend(leaderboardId); + setData(result); + } catch (err) { + setError( + err instanceof Error ? err.message : "Failed to load AI trend 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 + + ); + } + + // For now, only render H100 data + const h100Data = data.time_series["H100"]; + if (!h100Data || Object.keys(h100Data).length === 0) { + return ( + + + No H100 AI data available + + + ); + } + + // Build series for ECharts + const series: any[] = []; + + Object.entries(h100Data).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 (H100)", + left: "center", + textStyle: { + fontSize: 16, + fontWeight: "bold", + }, + }, + tooltip: { + trigger: "item", + formatter: (params: any) => { + 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(h100Data), + bottom: 0, + }, + grid: { + left: "3%", + right: "4%", + bottom: "15%", + top: "15%", + containLabel: true, + }, + xAxis: { + type: "time", + name: "Submission Time", + nameLocation: "middle", + nameGap: 30, + axisLabel: { + formatter: (value: number) => { + const date = new Date(value); + return `${date.getMonth() + 1}/${date.getDate()}`; + }, + }, + }, + yAxis: { + type: "value", + name: "Score (lower is better)", + nameLocation: "middle", + nameGap: 70, + axisLabel: { + formatter: (value: number) => `${formatMicrosecondsNum(value)}μs`, + }, + }, + series, + }; + + return ( + + + + ); +} diff --git a/frontend/src/pages/leaderboard/components/CodeDialog.tsx b/frontend/src/pages/leaderboard/components/CodeDialog.tsx index 3039084..df04e71 100644 --- a/frontend/src/pages/leaderboard/components/CodeDialog.tsx +++ b/frontend/src/pages/leaderboard/components/CodeDialog.tsx @@ -1,4 +1,11 @@ -import { Box, Button, Dialog, DialogContent, DialogTitle, Typography } from "@mui/material"; +import { + Box, + Button, + Dialog, + DialogContent, + DialogTitle, + Typography, +} from "@mui/material"; import { useState } from "react"; import { Link } from "react-router-dom"; import CodeBlock from "../../../components/codeblock/CodeBlock"; diff --git a/frontend/src/pages/leaderboard/components/RankingLists.tsx b/frontend/src/pages/leaderboard/components/RankingLists.tsx index 117b274..27f42de 100644 --- a/frontend/src/pages/leaderboard/components/RankingLists.tsx +++ b/frontend/src/pages/leaderboard/components/RankingLists.tsx @@ -88,7 +88,7 @@ export default function RankingsList({ }: RankingsListProps) { const showLoc = !!deadline && isExpired(deadline); const me = useAuthStore((s) => s.me); - const isAdmin = !!(me?.user?.is_admin); + const isAdmin = !!me?.user?.is_admin; const [expanded, setExpanded] = useState>({}); const [colorHash, _] = useState( Math.random().toString(36).slice(2, 8), @@ -181,12 +181,12 @@ export default function RankingsList({ {item.user_name} {getMedalIcon(item.rank)} - + {formatMicroseconds(item.score)} - + {item.prev_score > 0 && `+${formatMicroseconds(item.prev_score)}`} diff --git a/frontend/src/pages/lectures/Lectures.tsx b/frontend/src/pages/lectures/Lectures.tsx index c4ccca8..e6a243d 100644 --- a/frontend/src/pages/lectures/Lectures.tsx +++ b/frontend/src/pages/lectures/Lectures.tsx @@ -114,16 +114,20 @@ function formatDate(dateString: string): string { function formatEventDateTime(isoString: string): string { const date = new Date(isoString); - return date.toLocaleDateString("en-US", { - weekday: "long", - month: "long", - day: "numeric", - year: "numeric", - }) + " at " + date.toLocaleTimeString("en-US", { - hour: "numeric", - minute: "2-digit", - timeZoneName: "short", - }); + return ( + date.toLocaleDateString("en-US", { + weekday: "long", + month: "long", + day: "numeric", + year: "numeric", + }) + + " at " + + date.toLocaleTimeString("en-US", { + hour: "numeric", + minute: "2-digit", + timeZoneName: "short", + }) + ); } function getDurationDays(startDate: string, endDate: string): number { @@ -160,16 +164,17 @@ function HackathonCard({ hackathon }: { hackathon: Hackathon }) { size="small" color={hackathon.type === "in-person" ? "primary" : "secondary"} /> - {ongoing && ( - - )} + {ongoing && } {hackathon.title} - {formatDate(hackathon.startDate)} - {formatDate(hackathon.endDate)} ({duration} days) · {hackathon.location} + {formatDate(hackathon.startDate)} - {formatDate(hackathon.endDate)} ( + {duration} days) · {hackathon.location} {hackathon.description && ( - {hackathon.description} + + {hackathon.description} + )} - isOngoing(h.startDate, h.endDate) + isOngoing(h.startDate, h.endDate), ); return ( @@ -257,9 +262,15 @@ export default function Lectures() { {/* Upcoming Lectures Section */} Upcoming Lectures - - Lectures are pulled live from our Discord server. For the most up-to-date schedule, - join the{" "} + + Lectures are pulled live from our Discord server. For the most + up-to-date schedule, join the{" "} {error} ) : events.length === 0 ? ( - No upcoming lectures scheduled. Check back soon or join our Discord to - stay updated! + No upcoming lectures scheduled. Check back soon or join our Discord + to stay updated! ) : ( events.map((event) => ) diff --git a/frontend/src/pages/working-groups/WorkingGroups.tsx b/frontend/src/pages/working-groups/WorkingGroups.tsx index fcf20af..098a20a 100644 --- a/frontend/src/pages/working-groups/WorkingGroups.tsx +++ b/frontend/src/pages/working-groups/WorkingGroups.tsx @@ -222,7 +222,11 @@ function ProjectCard({ project }: { project: Project }) { )} {project.note && ( {project.note} diff --git a/kernelboard/api/leaderboard.py b/kernelboard/api/leaderboard.py index d662cc0..d9a4864 100644 --- a/kernelboard/api/leaderboard.py +++ b/kernelboard/api/leaderboard.py @@ -327,6 +327,7 @@ def group_by_model(items: List[dict]) -> dict: series[gpu_type][model].append({ "submission_time": item["submission_time"], "score": item["score"], + "gpu_type": gpu_type, "submission_id": item["submission_id"], }) return series From 6b3f044162fd364f897c1aa673504451971badb3 Mon Sep 17 00:00:00 2001 From: Yang Wang Date: Mon, 9 Feb 2026 16:16:01 -0800 Subject: [PATCH 3/3] hidemetadata --- frontend/src/api/api.ts | 33 ++++++++++++++++++++++++++++++--- 1 file changed, 30 insertions(+), 3 deletions(-) diff --git a/frontend/src/api/api.ts b/frontend/src/api/api.ts index 718cd16..d3dad05 100644 --- a/frontend/src/api/api.ts +++ b/frontend/src/api/api.ts @@ -73,9 +73,7 @@ export async function fetchAllNews(): Promise { return r.data; } -export async function fetchLeaderboardSummaries( - useV1: boolean = false, -): Promise { +export async function fetchLeaderboardSummaries(useV1: boolean = false): Promise { const start = performance.now(); const url = useV1 ? "/api/leaderboard-summaries?v1_query" @@ -191,3 +189,32 @@ export async function fetchEvents(): Promise { const r = await res.json(); return r.data; } + +export interface AiTrendDataPoint { + score: string; + submission_id: number; + submission_time: string; + gpu_type: string; +} + +export interface AiTrendTimeSeries { + [gpuType: string]: { + [model: string]: AiTrendDataPoint[]; + }; +} + +export interface AiTrendResponse { + leaderboard_id: number; + time_series: AiTrendTimeSeries; +} + +export async function fetchAiTrend(leaderboardId: string): Promise { + const res = await fetch(`/api/leaderboard/${leaderboardId}/ai_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); + } + const r = await res.json(); + return r.data; +}