Spaces:
Running
Running
udpate filters | url handling | searchbar and languages
Browse files- client/src/components/LeaderboardFilters/LeaderboardFilters.jsx +142 -65
- client/src/components/LeaderboardSection.jsx +256 -54
- client/src/components/Logo/Logo.jsx +24 -1
- client/src/context/LeaderboardContext.jsx +230 -73
- client/src/pages/HowToSubmitPage/HowToSubmitPage.jsx +10 -6
- client/src/pages/LeaderboardPage/LeaderboardPage.jsx +35 -14
- server/pyproject.toml +6 -0
client/src/components/LeaderboardFilters/LeaderboardFilters.jsx
CHANGED
@@ -1,4 +1,4 @@
|
|
1 |
-
import React, { useState } from "react";
|
2 |
import {
|
3 |
Box,
|
4 |
Stack,
|
@@ -16,6 +16,25 @@ import { useDebounce } from "../../hooks/useDebounce";
|
|
16 |
import { alpha } from "@mui/material/styles";
|
17 |
import { useMediaQuery } from "@mui/material";
|
18 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
19 |
const LeaderboardFilters = ({ allSections }) => {
|
20 |
const {
|
21 |
setSearchQuery,
|
@@ -24,10 +43,14 @@ const LeaderboardFilters = ({ allSections }) => {
|
|
24 |
totalLeaderboards,
|
25 |
filteredCount,
|
26 |
filterLeaderboards,
|
|
|
27 |
leaderboards,
|
|
|
|
|
|
|
28 |
} = useLeaderboard();
|
29 |
|
30 |
-
const [inputValue, setInputValue] = useState("");
|
31 |
const [totalArenaCount, setTotalArenaCount] = useState(0);
|
32 |
const debouncedSearch = useDebounce(inputValue, 200);
|
33 |
|
@@ -36,19 +59,60 @@ const LeaderboardFilters = ({ allSections }) => {
|
|
36 |
setSearchQuery(debouncedSearch);
|
37 |
}, [debouncedSearch, setSearchQuery]);
|
38 |
|
39 |
-
// Update
|
40 |
React.useEffect(() => {
|
41 |
-
|
42 |
-
|
43 |
-
|
44 |
-
|
45 |
-
|
46 |
-
|
47 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
48 |
|
49 |
// Check if any filter is active
|
50 |
const isFilterActive = debouncedSearch || arenaOnly;
|
51 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
52 |
const isMobile = useMediaQuery((theme) => theme.breakpoints.down("md"));
|
53 |
|
54 |
return (
|
@@ -70,68 +134,81 @@ const LeaderboardFilters = ({ allSections }) => {
|
|
70 |
justifyContent="center"
|
71 |
sx={{ pb: 2 }}
|
72 |
>
|
73 |
-
{allSections.map(({ id, title, data }) => {
|
74 |
-
const filteredCount =
|
|
|
|
|
|
|
|
|
|
|
75 |
return (
|
76 |
-
<
|
77 |
-
|
78 |
-
|
79 |
-
|
80 |
-
|
81 |
-
|
82 |
-
|
83 |
-
|
84 |
-
|
85 |
-
|
86 |
-
|
87 |
-
|
88 |
-
|
89 |
-
|
90 |
-
|
91 |
-
|
92 |
-
|
93 |
-
|
94 |
-
|
95 |
-
|
96 |
-
: "white",
|
97 |
-
"&:hover": {
|
98 |
backgroundColor: (theme) =>
|
99 |
-
|
|
|
|
|
100 |
? "background.paper"
|
101 |
: "white",
|
102 |
-
|
103 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
104 |
transition: "none",
|
105 |
-
},
|
106 |
-
transition: "none",
|
107 |
-
}}
|
108 |
-
>
|
109 |
-
{title}
|
110 |
-
<Box
|
111 |
-
component="span"
|
112 |
-
sx={{
|
113 |
-
display: "inline-flex",
|
114 |
-
alignItems: "center",
|
115 |
-
gap: 0.75,
|
116 |
-
color: "text.secondary",
|
117 |
-
ml: 0.75,
|
118 |
}}
|
119 |
>
|
|
|
120 |
<Box
|
121 |
component="span"
|
122 |
-
sx={
|
123 |
-
|
124 |
-
|
125 |
-
|
126 |
-
|
127 |
-
|
128 |
-
|
129 |
-
|
130 |
-
|
131 |
-
|
132 |
-
|
133 |
-
|
134 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
135 |
);
|
136 |
})}
|
137 |
</Stack>
|
@@ -180,7 +257,7 @@ const LeaderboardFilters = ({ allSections }) => {
|
|
180 |
fontWeight: 500,
|
181 |
}}
|
182 |
>
|
183 |
-
{
|
184 |
</Typography>
|
185 |
<Box
|
186 |
sx={{
|
@@ -204,7 +281,7 @@ const LeaderboardFilters = ({ allSections }) => {
|
|
204 |
fontWeight: 500,
|
205 |
}}
|
206 |
>
|
207 |
-
{arenaOnly ? totalArenaCount :
|
208 |
</Typography>
|
209 |
</Box>
|
210 |
<Typography
|
|
|
1 |
+
import React, { useState, useMemo } from "react";
|
2 |
import {
|
3 |
Box,
|
4 |
Stack,
|
|
|
16 |
import { alpha } from "@mui/material/styles";
|
17 |
import { useMediaQuery } from "@mui/material";
|
18 |
|
19 |
+
// Helper function to get the category group of a section
|
20 |
+
const getSectionGroup = (id) => {
|
21 |
+
const groups = {
|
22 |
+
modalities: ["agentic", "vision", "audio"],
|
23 |
+
capabilities: ["code", "math"],
|
24 |
+
languages: ["language"],
|
25 |
+
domains: ["financial", "medical", "legal"],
|
26 |
+
evaluation: ["safety"],
|
27 |
+
misc: ["uncategorized"],
|
28 |
+
};
|
29 |
+
|
30 |
+
for (const [group, ids] of Object.entries(groups)) {
|
31 |
+
if (ids.includes(id)) {
|
32 |
+
return group;
|
33 |
+
}
|
34 |
+
}
|
35 |
+
return "misc";
|
36 |
+
};
|
37 |
+
|
38 |
const LeaderboardFilters = ({ allSections }) => {
|
39 |
const {
|
40 |
setSearchQuery,
|
|
|
43 |
totalLeaderboards,
|
44 |
filteredCount,
|
45 |
filterLeaderboards,
|
46 |
+
filterLeaderboardsForCount,
|
47 |
leaderboards,
|
48 |
+
selectedCategory,
|
49 |
+
setSelectedCategory,
|
50 |
+
searchQuery,
|
51 |
} = useLeaderboard();
|
52 |
|
53 |
+
const [inputValue, setInputValue] = useState(searchQuery || "");
|
54 |
const [totalArenaCount, setTotalArenaCount] = useState(0);
|
55 |
const debouncedSearch = useDebounce(inputValue, 200);
|
56 |
|
|
|
59 |
setSearchQuery(debouncedSearch);
|
60 |
}, [debouncedSearch, setSearchQuery]);
|
61 |
|
62 |
+
// Update input value when searchQuery changes externally
|
63 |
React.useEffect(() => {
|
64 |
+
setInputValue(searchQuery || "");
|
65 |
+
}, [searchQuery]);
|
66 |
+
|
67 |
+
// Update total arena count
|
68 |
+
React.useEffect(() => {
|
69 |
+
// Si une catégorie est sélectionnée, on compte les arena dans cette catégorie
|
70 |
+
const boardsToCount = selectedCategory
|
71 |
+
? allSections.find((section) => section.id === selectedCategory)?.data ||
|
72 |
+
[]
|
73 |
+
: allSections.reduce((acc, section) => [...acc, ...section.data], []);
|
74 |
+
|
75 |
+
const arenaCount = boardsToCount.filter((board) =>
|
76 |
+
board.tags?.includes("judge:humans")
|
77 |
+
).length;
|
78 |
+
setTotalArenaCount(arenaCount);
|
79 |
+
}, [leaderboards, selectedCategory, allSections]);
|
80 |
|
81 |
// Check if any filter is active
|
82 |
const isFilterActive = debouncedSearch || arenaOnly;
|
83 |
|
84 |
+
// Calculer le nombre total en fonction de la catégorie sélectionnée
|
85 |
+
const totalCount = useMemo(() => {
|
86 |
+
if (selectedCategory) {
|
87 |
+
const categorySection = allSections.find(
|
88 |
+
(section) => section.id === selectedCategory
|
89 |
+
);
|
90 |
+
return categorySection ? categorySection.data.length : 0;
|
91 |
+
}
|
92 |
+
// Quand aucune catégorie n'est sélectionnée, on compte tous les leaderboards de toutes les sections
|
93 |
+
return allSections.reduce(
|
94 |
+
(total, section) => total + section.data.length,
|
95 |
+
0
|
96 |
+
);
|
97 |
+
}, [selectedCategory, allSections]);
|
98 |
+
|
99 |
+
// Calculer le nombre filtré en prenant en compte tous les filtres
|
100 |
+
const currentFilteredCount = useMemo(() => {
|
101 |
+
if (selectedCategory) {
|
102 |
+
const categorySection = allSections.find(
|
103 |
+
(section) => section.id === selectedCategory
|
104 |
+
);
|
105 |
+
if (!categorySection) return 0;
|
106 |
+
return filterLeaderboards(categorySection.data).length;
|
107 |
+
}
|
108 |
+
// Quand aucune catégorie n'est sélectionnée, on compte dans toutes les sections
|
109 |
+
const allBoards = allSections.reduce(
|
110 |
+
(acc, section) => [...acc, ...section.data],
|
111 |
+
[]
|
112 |
+
);
|
113 |
+
return filterLeaderboards(allBoards).length;
|
114 |
+
}, [selectedCategory, allSections, filterLeaderboards]);
|
115 |
+
|
116 |
const isMobile = useMediaQuery((theme) => theme.breakpoints.down("md"));
|
117 |
|
118 |
return (
|
|
|
134 |
justifyContent="center"
|
135 |
sx={{ pb: 2 }}
|
136 |
>
|
137 |
+
{allSections.map(({ id, title, data }, index) => {
|
138 |
+
const filteredCount = filterLeaderboardsForCount(data).length;
|
139 |
+
const currentGroup = getSectionGroup(id);
|
140 |
+
const prevGroup =
|
141 |
+
index > 0 ? getSectionGroup(allSections[index - 1].id) : null;
|
142 |
+
const needsSpacing = currentGroup !== prevGroup;
|
143 |
+
|
144 |
return (
|
145 |
+
<React.Fragment key={id}>
|
146 |
+
{needsSpacing && index > 0 && (
|
147 |
+
<Box sx={{ width: "0.5rem", display: "inline-block" }} />
|
148 |
+
)}
|
149 |
+
<Button
|
150 |
+
onClick={() => {
|
151 |
+
if (selectedCategory === id || filteredCount > 0) {
|
152 |
+
setSelectedCategory(selectedCategory === id ? null : id);
|
153 |
+
}
|
154 |
+
}}
|
155 |
+
variant={selectedCategory === id ? "contained" : "outlined"}
|
156 |
+
size="small"
|
157 |
+
disabled={filteredCount === 0 && selectedCategory !== id}
|
158 |
+
sx={{
|
159 |
+
textTransform: "none",
|
160 |
+
cursor:
|
161 |
+
filteredCount === 0 && selectedCategory !== id
|
162 |
+
? "default"
|
163 |
+
: "pointer",
|
164 |
+
mb: 1,
|
|
|
|
|
165 |
backgroundColor: (theme) =>
|
166 |
+
selectedCategory === id
|
167 |
+
? undefined
|
168 |
+
: theme.palette.mode === "dark"
|
169 |
? "background.paper"
|
170 |
: "white",
|
171 |
+
"&:hover": {
|
172 |
+
backgroundColor: (theme) =>
|
173 |
+
selectedCategory === id
|
174 |
+
? undefined
|
175 |
+
: theme.palette.mode === "dark"
|
176 |
+
? "background.paper"
|
177 |
+
: "white",
|
178 |
+
},
|
179 |
+
"& .MuiTouchRipple-root": {
|
180 |
+
transition: "none",
|
181 |
+
},
|
182 |
transition: "none",
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
183 |
}}
|
184 |
>
|
185 |
+
{title}
|
186 |
<Box
|
187 |
component="span"
|
188 |
+
sx={{
|
189 |
+
display: "inline-flex",
|
190 |
+
alignItems: "center",
|
191 |
+
gap: 0.75,
|
192 |
+
color: "text.secondary",
|
193 |
+
ml: 0.75,
|
194 |
+
}}
|
195 |
+
>
|
196 |
+
<Box
|
197 |
+
component="span"
|
198 |
+
sx={(theme) => ({
|
199 |
+
width: "4px",
|
200 |
+
height: "4px",
|
201 |
+
borderRadius: "100%",
|
202 |
+
backgroundColor: alpha(
|
203 |
+
theme.palette.text.primary,
|
204 |
+
theme.palette.mode === "dark" ? 0.2 : 0.15
|
205 |
+
),
|
206 |
+
})}
|
207 |
+
/>
|
208 |
+
{filteredCount}
|
209 |
+
</Box>
|
210 |
+
</Button>
|
211 |
+
</React.Fragment>
|
212 |
);
|
213 |
})}
|
214 |
</Stack>
|
|
|
257 |
fontWeight: 500,
|
258 |
}}
|
259 |
>
|
260 |
+
{currentFilteredCount}
|
261 |
</Typography>
|
262 |
<Box
|
263 |
sx={{
|
|
|
281 |
fontWeight: 500,
|
282 |
}}
|
283 |
>
|
284 |
+
{arenaOnly ? totalArenaCount : totalCount}
|
285 |
</Typography>
|
286 |
</Box>
|
287 |
<Typography
|
client/src/components/LeaderboardSection.jsx
CHANGED
@@ -1,31 +1,95 @@
|
|
1 |
-
import React, { useState } from "react";
|
2 |
-
import { Typography, Grid, Box, Button, Collapse } from "@mui/material";
|
3 |
import { alpha } from "@mui/material/styles";
|
4 |
import LeaderboardCard from "./LeaderboardCard";
|
5 |
import ExpandMoreIcon from "@mui/icons-material/ExpandMore";
|
6 |
-
import
|
|
|
7 |
|
8 |
const ITEMS_PER_PAGE = 3;
|
9 |
|
10 |
-
const LeaderboardSection = ({
|
11 |
-
|
12 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
13 |
|
14 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
15 |
|
16 |
// On affiche toujours les 3 premiers
|
17 |
-
const displayedLeaderboards =
|
18 |
// Le reste sera dans le Collapse
|
19 |
-
const remainingLeaderboards =
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
20 |
|
21 |
const toggleExpanded = () => {
|
22 |
-
|
23 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
24 |
};
|
25 |
|
26 |
-
// Calculate how many skeletons we need
|
27 |
-
const skeletonsNeeded = Math.max(0, 3 - leaderboards.length);
|
28 |
-
|
29 |
return (
|
30 |
<Box sx={{ mb: 6 }}>
|
31 |
<Box
|
@@ -33,7 +97,7 @@ const LeaderboardSection = ({ title, leaderboards }) => {
|
|
33 |
display: "flex",
|
34 |
alignItems: "center",
|
35 |
justifyContent: "space-between",
|
36 |
-
mb: 4,
|
37 |
}}
|
38 |
>
|
39 |
<Box sx={{ display: "flex", alignItems: "center", gap: 2 }}>
|
@@ -66,71 +130,209 @@ const LeaderboardSection = ({ title, leaderboards }) => {
|
|
66 |
fontSize: { xs: "1.25rem", md: "1.5rem" },
|
67 |
}}
|
68 |
>
|
69 |
-
{
|
70 |
</Typography>
|
71 |
</Box>
|
72 |
-
{
|
73 |
<Button
|
74 |
onClick={toggleExpanded}
|
75 |
size="small"
|
|
|
76 |
sx={{
|
77 |
color: "text.secondary",
|
78 |
fontSize: "0.875rem",
|
79 |
textTransform: "none",
|
|
|
80 |
"&:hover": {
|
81 |
backgroundColor: (theme) =>
|
82 |
-
|
83 |
-
|
84 |
-
|
85 |
-
|
|
|
|
|
86 |
},
|
87 |
}}
|
88 |
endIcon={
|
89 |
<ExpandMoreIcon
|
90 |
sx={{
|
91 |
-
transform:
|
92 |
transition: "transform 300ms",
|
93 |
}}
|
94 |
/>
|
95 |
}
|
96 |
>
|
97 |
-
{
|
98 |
</Button>
|
99 |
)}
|
100 |
</Box>
|
101 |
-
|
102 |
-
|
103 |
-
|
104 |
-
|
105 |
-
|
106 |
-
|
107 |
-
|
108 |
-
|
109 |
-
|
110 |
-
|
111 |
-
|
112 |
-
|
113 |
-
|
114 |
-
|
115 |
-
|
116 |
-
|
117 |
-
|
118 |
-
|
119 |
-
|
120 |
-
|
121 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
122 |
</Grid>
|
123 |
-
|
124 |
-
|
125 |
-
|
126 |
-
|
127 |
-
|
128 |
-
|
129 |
-
|
130 |
</Grid>
|
131 |
-
|
132 |
-
|
133 |
-
|
134 |
</Box>
|
135 |
);
|
136 |
};
|
|
|
1 |
+
import React, { useState, useMemo } from "react";
|
2 |
+
import { Typography, Grid, Box, Button, Collapse, Stack } from "@mui/material";
|
3 |
import { alpha } from "@mui/material/styles";
|
4 |
import LeaderboardCard from "./LeaderboardCard";
|
5 |
import ExpandMoreIcon from "@mui/icons-material/ExpandMore";
|
6 |
+
import { useLeaderboard } from "../context/LeaderboardContext";
|
7 |
+
import SearchOffIcon from "@mui/icons-material/SearchOff";
|
8 |
|
9 |
const ITEMS_PER_PAGE = 3;
|
10 |
|
11 |
+
const LeaderboardSection = ({
|
12 |
+
title,
|
13 |
+
leaderboards,
|
14 |
+
filteredLeaderboards,
|
15 |
+
id,
|
16 |
+
}) => {
|
17 |
+
const {
|
18 |
+
expandedSections,
|
19 |
+
setExpandedSections,
|
20 |
+
selectedLanguage,
|
21 |
+
setSelectedLanguage,
|
22 |
+
searchQuery,
|
23 |
+
selectedCategory,
|
24 |
+
} = useLeaderboard();
|
25 |
+
const isExpanded = expandedSections.has(id);
|
26 |
|
27 |
+
// Extraire la liste des langues si c'est la section Language Specific
|
28 |
+
// Cette liste ne doit JAMAIS changer, peu importe les filtres
|
29 |
+
const languages = useMemo(() => {
|
30 |
+
if (id !== "language") return null;
|
31 |
+
const langSet = new Set();
|
32 |
+
leaderboards.forEach((board) => {
|
33 |
+
board.tags?.forEach((tag) => {
|
34 |
+
if (tag.startsWith("language:")) {
|
35 |
+
const language = tag.split(":")[1];
|
36 |
+
const capitalizedLang =
|
37 |
+
language.charAt(0).toUpperCase() + language.slice(1);
|
38 |
+
langSet.add(capitalizedLang);
|
39 |
+
}
|
40 |
+
});
|
41 |
+
});
|
42 |
+
return Array.from(langSet).sort();
|
43 |
+
}, [id, leaderboards]);
|
44 |
+
|
45 |
+
// Calculer le nombre de leaderboards par langue
|
46 |
+
// On utilise les leaderboards bruts pour avoir toutes les langues
|
47 |
+
const languageStats = useMemo(() => {
|
48 |
+
if (!languages) return null;
|
49 |
+
const stats = new Map();
|
50 |
+
|
51 |
+
// Compter les leaderboards pour chaque langue
|
52 |
+
languages.forEach((lang) => {
|
53 |
+
const count = leaderboards.filter((board) =>
|
54 |
+
board.tags?.some(
|
55 |
+
(tag) => tag.toLowerCase() === `language:${lang.toLowerCase()}`
|
56 |
+
)
|
57 |
+
).length;
|
58 |
+
stats.set(lang, count);
|
59 |
+
});
|
60 |
+
|
61 |
+
return stats;
|
62 |
+
}, [languages, leaderboards]);
|
63 |
+
|
64 |
+
// On ne retourne null que si on n'a pas de leaderboards bruts
|
65 |
+
if (!leaderboards) return null;
|
66 |
|
67 |
// On affiche toujours les 3 premiers
|
68 |
+
const displayedLeaderboards = filteredLeaderboards.slice(0, ITEMS_PER_PAGE);
|
69 |
// Le reste sera dans le Collapse
|
70 |
+
const remainingLeaderboards = filteredLeaderboards.slice(ITEMS_PER_PAGE);
|
71 |
+
|
72 |
+
// Calculate how many skeletons we need
|
73 |
+
const skeletonsNeeded = Math.max(0, 3 - filteredLeaderboards.length);
|
74 |
+
|
75 |
+
// On affiche le bouton seulement si aucune catégorie n'est sélectionnée
|
76 |
+
const showExpandButton = !selectedCategory;
|
77 |
+
|
78 |
+
// Le bouton est actif seulement s'il y a plus de 3 leaderboards
|
79 |
+
const isExpandButtonEnabled = filteredLeaderboards.length > ITEMS_PER_PAGE;
|
80 |
|
81 |
const toggleExpanded = () => {
|
82 |
+
setExpandedSections((prev) => {
|
83 |
+
const newSet = new Set(prev);
|
84 |
+
if (isExpanded) {
|
85 |
+
newSet.delete(id);
|
86 |
+
} else {
|
87 |
+
newSet.add(id);
|
88 |
+
}
|
89 |
+
return newSet;
|
90 |
+
});
|
91 |
};
|
92 |
|
|
|
|
|
|
|
93 |
return (
|
94 |
<Box sx={{ mb: 6 }}>
|
95 |
<Box
|
|
|
97 |
display: "flex",
|
98 |
alignItems: "center",
|
99 |
justifyContent: "space-between",
|
100 |
+
mb: languageStats ? 2 : 4,
|
101 |
}}
|
102 |
>
|
103 |
<Box sx={{ display: "flex", alignItems: "center", gap: 2 }}>
|
|
|
130 |
fontSize: { xs: "1.25rem", md: "1.5rem" },
|
131 |
}}
|
132 |
>
|
133 |
+
{filteredLeaderboards.length}
|
134 |
</Typography>
|
135 |
</Box>
|
136 |
+
{showExpandButton && (
|
137 |
<Button
|
138 |
onClick={toggleExpanded}
|
139 |
size="small"
|
140 |
+
disabled={!isExpandButtonEnabled}
|
141 |
sx={{
|
142 |
color: "text.secondary",
|
143 |
fontSize: "0.875rem",
|
144 |
textTransform: "none",
|
145 |
+
opacity: isExpandButtonEnabled ? 1 : 0.5,
|
146 |
"&:hover": {
|
147 |
backgroundColor: (theme) =>
|
148 |
+
isExpandButtonEnabled
|
149 |
+
? alpha(
|
150 |
+
theme.palette.text.primary,
|
151 |
+
theme.palette.mode === "dark" ? 0.1 : 0.06
|
152 |
+
)
|
153 |
+
: "transparent",
|
154 |
},
|
155 |
}}
|
156 |
endIcon={
|
157 |
<ExpandMoreIcon
|
158 |
sx={{
|
159 |
+
transform: isExpanded ? "rotate(180deg)" : "rotate(0deg)",
|
160 |
transition: "transform 300ms",
|
161 |
}}
|
162 |
/>
|
163 |
}
|
164 |
>
|
165 |
+
{isExpanded ? "Show less" : "Show more"}
|
166 |
</Button>
|
167 |
)}
|
168 |
</Box>
|
169 |
+
|
170 |
+
{languages && selectedCategory === "language" && (
|
171 |
+
<>
|
172 |
+
<Typography
|
173 |
+
variant="body2"
|
174 |
+
color="text.secondary"
|
175 |
+
sx={{ mb: 2, fontWeight: 500 }}
|
176 |
+
>
|
177 |
+
Languages represented in this category
|
178 |
+
</Typography>
|
179 |
+
<Box
|
180 |
+
sx={{
|
181 |
+
display: "flex",
|
182 |
+
flexWrap: "wrap",
|
183 |
+
gap: 1,
|
184 |
+
mb: filteredLeaderboards.length > 0 ? 4 : 2,
|
185 |
+
mx: -0.5,
|
186 |
+
}}
|
187 |
+
>
|
188 |
+
{languages.map((lang) => {
|
189 |
+
const isActive = selectedLanguage === lang;
|
190 |
+
const count = languageStats?.get(lang) || 0;
|
191 |
+
|
192 |
+
return (
|
193 |
+
<Button
|
194 |
+
key={lang}
|
195 |
+
onClick={() => setSelectedLanguage(isActive ? null : lang)}
|
196 |
+
variant={isActive ? "contained" : "outlined"}
|
197 |
+
size="small"
|
198 |
+
sx={{
|
199 |
+
textTransform: "none",
|
200 |
+
m: 0.125,
|
201 |
+
backgroundColor: (theme) =>
|
202 |
+
isActive
|
203 |
+
? undefined
|
204 |
+
: theme.palette.mode === "dark"
|
205 |
+
? "background.paper"
|
206 |
+
: "white",
|
207 |
+
"&:hover": {
|
208 |
+
backgroundColor: (theme) =>
|
209 |
+
isActive
|
210 |
+
? undefined
|
211 |
+
: theme.palette.mode === "dark"
|
212 |
+
? "background.paper"
|
213 |
+
: "white",
|
214 |
+
},
|
215 |
+
"& .MuiTouchRipple-root": {
|
216 |
+
transition: "none",
|
217 |
+
},
|
218 |
+
transition: "none",
|
219 |
+
}}
|
220 |
+
>
|
221 |
+
{lang}
|
222 |
+
<Box
|
223 |
+
component="span"
|
224 |
+
sx={{
|
225 |
+
display: "inline-flex",
|
226 |
+
alignItems: "center",
|
227 |
+
gap: 0.75,
|
228 |
+
color: isActive ? "inherit" : "text.secondary",
|
229 |
+
ml: 0.75,
|
230 |
+
}}
|
231 |
+
>
|
232 |
+
<Box
|
233 |
+
component="span"
|
234 |
+
sx={(theme) => ({
|
235 |
+
width: "4px",
|
236 |
+
height: "4px",
|
237 |
+
borderRadius: "100%",
|
238 |
+
backgroundColor: alpha(
|
239 |
+
theme.palette.text.primary,
|
240 |
+
theme.palette.mode === "dark" ? 0.2 : 0.15
|
241 |
+
),
|
242 |
+
})}
|
243 |
+
/>
|
244 |
+
{count}
|
245 |
+
</Box>
|
246 |
+
</Button>
|
247 |
+
);
|
248 |
+
})}
|
249 |
+
</Box>
|
250 |
+
</>
|
251 |
+
)}
|
252 |
+
|
253 |
+
{filteredLeaderboards.length === 0 ? (
|
254 |
+
<Box
|
255 |
+
sx={{
|
256 |
+
display: "flex",
|
257 |
+
flexDirection: "column",
|
258 |
+
alignItems: "center",
|
259 |
+
gap: 2,
|
260 |
+
py: 7,
|
261 |
+
bgcolor: (theme) =>
|
262 |
+
theme.palette.mode === "dark"
|
263 |
+
? "background.paper"
|
264 |
+
: "background.default",
|
265 |
+
borderRadius: 2,
|
266 |
+
}}
|
267 |
+
>
|
268 |
+
<SearchOffIcon
|
269 |
+
sx={{
|
270 |
+
fontSize: 64,
|
271 |
+
color: "text.secondary",
|
272 |
+
opacity: 0.5,
|
273 |
+
}}
|
274 |
+
/>
|
275 |
+
<Typography variant="h5" color="text.secondary" align="center">
|
276 |
+
{searchQuery ? (
|
277 |
+
<>
|
278 |
+
No {title.toLowerCase()} leaderboard matches{" "}
|
279 |
+
<Box
|
280 |
+
component="span"
|
281 |
+
sx={{
|
282 |
+
bgcolor: "primary.main",
|
283 |
+
color: "primary.contrastText",
|
284 |
+
px: 1,
|
285 |
+
borderRadius: 1,
|
286 |
+
}}
|
287 |
+
>
|
288 |
+
{searchQuery}
|
289 |
+
</Box>
|
290 |
+
</>
|
291 |
+
) : (
|
292 |
+
`No ${title.toLowerCase()} leaderboard matches your criteria`
|
293 |
+
)}
|
294 |
+
</Typography>
|
295 |
+
<Typography variant="body1" color="text.secondary" align="center">
|
296 |
+
Try adjusting your search filters
|
297 |
+
</Typography>
|
298 |
+
</Box>
|
299 |
+
) : (
|
300 |
+
<>
|
301 |
+
<Grid container spacing={3}>
|
302 |
+
{displayedLeaderboards.map((leaderboard, index) => (
|
303 |
+
<Grid item xs={12} sm={6} md={4} key={index}>
|
304 |
+
<LeaderboardCard leaderboard={leaderboard} />
|
305 |
+
</Grid>
|
306 |
+
))}
|
307 |
+
{/* Add skeletons if needed */}
|
308 |
+
{Array.from({ length: skeletonsNeeded }).map((_, index) => (
|
309 |
+
<Grid item xs={12} sm={6} md={4} key={`skeleton-${index}`}>
|
310 |
+
<Box
|
311 |
+
sx={{
|
312 |
+
height: "180px",
|
313 |
+
borderRadius: 2,
|
314 |
+
bgcolor: (theme) => alpha(theme.palette.primary.main, 0.15),
|
315 |
+
opacity: 1,
|
316 |
+
transition: "opacity 0.3s ease-in-out",
|
317 |
+
"&:hover": {
|
318 |
+
opacity: 0.8,
|
319 |
+
},
|
320 |
+
}}
|
321 |
+
/>
|
322 |
+
</Grid>
|
323 |
+
))}
|
324 |
</Grid>
|
325 |
+
<Collapse in={isExpanded} timeout={300} unmountOnExit>
|
326 |
+
<Grid container spacing={3} sx={{ mt: 0 }}>
|
327 |
+
{remainingLeaderboards.map((leaderboard, index) => (
|
328 |
+
<Grid item xs={12} sm={6} md={4} key={index + ITEMS_PER_PAGE}>
|
329 |
+
<LeaderboardCard leaderboard={leaderboard} />
|
330 |
+
</Grid>
|
331 |
+
))}
|
332 |
</Grid>
|
333 |
+
</Collapse>
|
334 |
+
</>
|
335 |
+
)}
|
336 |
</Box>
|
337 |
);
|
338 |
};
|
client/src/components/Logo/Logo.jsx
CHANGED
@@ -3,10 +3,33 @@ import { Link } from "react-router-dom";
|
|
3 |
import { Box } from "@mui/material";
|
4 |
import logoText from "../../assets/logo-text.svg";
|
5 |
import gradient from "../../assets/gradient.svg";
|
|
|
6 |
|
7 |
const Logo = () => {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
8 |
return (
|
9 |
-
<Link
|
|
|
|
|
|
|
|
|
10 |
<Box
|
11 |
component="img"
|
12 |
src={gradient}
|
|
|
3 |
import { Box } from "@mui/material";
|
4 |
import logoText from "../../assets/logo-text.svg";
|
5 |
import gradient from "../../assets/gradient.svg";
|
6 |
+
import { useLeaderboard } from "../../context/LeaderboardContext";
|
7 |
|
8 |
const Logo = () => {
|
9 |
+
const {
|
10 |
+
setSearchQuery,
|
11 |
+
setArenaOnly,
|
12 |
+
setSelectedCategory,
|
13 |
+
setSelectedLanguage,
|
14 |
+
setExpandedSections,
|
15 |
+
} = useLeaderboard();
|
16 |
+
|
17 |
+
const handleReset = (e) => {
|
18 |
+
e.preventDefault();
|
19 |
+
setSearchQuery("");
|
20 |
+
setArenaOnly(false);
|
21 |
+
setSelectedCategory(null);
|
22 |
+
setSelectedLanguage(null);
|
23 |
+
setExpandedSections(new Set());
|
24 |
+
window.history.pushState({}, "", window.location.pathname);
|
25 |
+
};
|
26 |
+
|
27 |
return (
|
28 |
+
<Link
|
29 |
+
to="/"
|
30 |
+
onClick={handleReset}
|
31 |
+
style={{ textDecoration: "none", position: "relative" }}
|
32 |
+
>
|
33 |
<Box
|
34 |
component="img"
|
35 |
src={gradient}
|
client/src/context/LeaderboardContext.jsx
CHANGED
@@ -4,14 +4,218 @@ import React, {
|
|
4 |
useState,
|
5 |
useCallback,
|
6 |
useMemo,
|
|
|
7 |
} from "react";
|
8 |
|
9 |
const LeaderboardContext = createContext();
|
10 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
11 |
export const LeaderboardProvider = ({ children }) => {
|
|
|
|
|
|
|
12 |
const [leaderboards, setLeaderboards] = useState([]);
|
13 |
-
const [searchQuery, setSearchQuery] = useState("");
|
14 |
-
const [arenaOnly, setArenaOnly] = useState(
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
15 |
|
16 |
// Calculate total number of unique leaderboards (excluding duplicates)
|
17 |
const totalLeaderboards = useMemo(() => {
|
@@ -57,60 +261,10 @@ export const LeaderboardProvider = ({ children }) => {
|
|
57 |
);
|
58 |
}, []);
|
59 |
|
60 |
-
//
|
61 |
-
const filterLeaderboards = useCallback(
|
62 |
-
(boards) => {
|
63 |
-
if (!boards) return [];
|
64 |
-
|
65 |
-
// On évite de créer une copie inutile
|
66 |
-
let filtered = boards;
|
67 |
-
|
68 |
-
// Filter by search query
|
69 |
-
if (searchQuery) {
|
70 |
-
const query = searchQuery.toLowerCase();
|
71 |
-
const searchableTagPrefixes = [
|
72 |
-
"domain:",
|
73 |
-
"language:",
|
74 |
-
"judge:",
|
75 |
-
"test:",
|
76 |
-
"modality:",
|
77 |
-
"submission:",
|
78 |
-
"domain:",
|
79 |
-
"eval:",
|
80 |
-
];
|
81 |
-
|
82 |
-
filtered = filtered.filter((board) => {
|
83 |
-
const isTagSearch = searchableTagPrefixes.some((prefix) =>
|
84 |
-
query.startsWith(prefix)
|
85 |
-
);
|
86 |
-
|
87 |
-
if (isTagSearch) {
|
88 |
-
return board.tags?.some((tag) => tag.toLowerCase().includes(query));
|
89 |
-
}
|
90 |
-
|
91 |
-
return board.card_data?.title?.toLowerCase().includes(query);
|
92 |
-
});
|
93 |
-
}
|
94 |
-
|
95 |
-
// Filter arena only
|
96 |
-
if (arenaOnly) {
|
97 |
-
filtered = filtered.filter((board) =>
|
98 |
-
board.tags?.includes("judge:humans")
|
99 |
-
);
|
100 |
-
}
|
101 |
-
|
102 |
-
return filtered;
|
103 |
-
},
|
104 |
-
[searchQuery, arenaOnly]
|
105 |
-
);
|
106 |
-
|
107 |
-
// Define sections
|
108 |
const allSections = useMemo(() => {
|
109 |
if (!leaderboards) return [];
|
110 |
|
111 |
-
// D'abord filtrer les leaderboards selon la recherche
|
112 |
-
const filteredBoards = filterLeaderboards(leaderboards);
|
113 |
-
|
114 |
// Garder une trace des leaderboards déjà catégorisés
|
115 |
const categorizedIds = new Set();
|
116 |
|
@@ -118,7 +272,7 @@ export const LeaderboardProvider = ({ children }) => {
|
|
118 |
{
|
119 |
id: "agentic",
|
120 |
title: "Agentic",
|
121 |
-
data: filterByTag("modality:agent",
|
122 |
categorizedIds.add(board.id);
|
123 |
return board;
|
124 |
}),
|
@@ -126,7 +280,7 @@ export const LeaderboardProvider = ({ children }) => {
|
|
126 |
{
|
127 |
id: "code",
|
128 |
title: "Code",
|
129 |
-
data: filterByTag("eval:code",
|
130 |
categorizedIds.add(board.id);
|
131 |
return board;
|
132 |
}),
|
@@ -134,7 +288,7 @@ export const LeaderboardProvider = ({ children }) => {
|
|
134 |
{
|
135 |
id: "math",
|
136 |
title: "Math",
|
137 |
-
data: filterByTag("eval:math",
|
138 |
categorizedIds.add(board.id);
|
139 |
return board;
|
140 |
}),
|
@@ -142,7 +296,7 @@ export const LeaderboardProvider = ({ children }) => {
|
|
142 |
{
|
143 |
id: "language",
|
144 |
title: "Language Specific",
|
145 |
-
data: filterByLanguage(
|
146 |
categorizedIds.add(board.id);
|
147 |
return board;
|
148 |
}),
|
@@ -150,7 +304,7 @@ export const LeaderboardProvider = ({ children }) => {
|
|
150 |
{
|
151 |
id: "vision",
|
152 |
title: "Vision",
|
153 |
-
data: filterByVision(
|
154 |
categorizedIds.add(board.id);
|
155 |
return board;
|
156 |
}),
|
@@ -158,7 +312,7 @@ export const LeaderboardProvider = ({ children }) => {
|
|
158 |
{
|
159 |
id: "audio",
|
160 |
title: "Audio",
|
161 |
-
data: filterByTag("modality:audio",
|
162 |
categorizedIds.add(board.id);
|
163 |
return board;
|
164 |
}),
|
@@ -166,7 +320,7 @@ export const LeaderboardProvider = ({ children }) => {
|
|
166 |
{
|
167 |
id: "financial",
|
168 |
title: "Financial",
|
169 |
-
data: filterByTag("domain:financial",
|
170 |
categorizedIds.add(board.id);
|
171 |
return board;
|
172 |
}),
|
@@ -174,7 +328,7 @@ export const LeaderboardProvider = ({ children }) => {
|
|
174 |
{
|
175 |
id: "medical",
|
176 |
title: "Medical",
|
177 |
-
data: filterByTag("domain:medical",
|
178 |
categorizedIds.add(board.id);
|
179 |
return board;
|
180 |
}),
|
@@ -182,7 +336,7 @@ export const LeaderboardProvider = ({ children }) => {
|
|
182 |
{
|
183 |
id: "legal",
|
184 |
title: "Legal",
|
185 |
-
data: filterByTag("domain:legal",
|
186 |
categorizedIds.add(board.id);
|
187 |
return board;
|
188 |
}),
|
@@ -190,7 +344,7 @@ export const LeaderboardProvider = ({ children }) => {
|
|
190 |
{
|
191 |
id: "safety",
|
192 |
title: "Safety",
|
193 |
-
data: filterByTag("eval:safety",
|
194 |
categorizedIds.add(board.id);
|
195 |
return board;
|
196 |
}),
|
@@ -199,18 +353,12 @@ export const LeaderboardProvider = ({ children }) => {
|
|
199 |
id: "uncategorized",
|
200 |
title: "Uncategorized",
|
201 |
// Mettre dans uncategorized uniquement les leaderboards qui n'apparaissent nulle part ailleurs
|
202 |
-
data:
|
203 |
},
|
204 |
];
|
205 |
|
206 |
return sections;
|
207 |
-
}, [
|
208 |
-
leaderboards,
|
209 |
-
filterLeaderboards,
|
210 |
-
filterByTag,
|
211 |
-
filterByLanguage,
|
212 |
-
filterByVision,
|
213 |
-
]);
|
214 |
|
215 |
// Get sections with data
|
216 |
const sections = useMemo(() => {
|
@@ -259,8 +407,8 @@ export const LeaderboardProvider = ({ children }) => {
|
|
259 |
|
260 |
// Get filtered count
|
261 |
const filteredCount = useMemo(() => {
|
262 |
-
return
|
263 |
-
}, [
|
264 |
|
265 |
// Function to get highlighted parts of text
|
266 |
const getHighlightedText = useCallback((text, searchTerm) => {
|
@@ -303,9 +451,18 @@ export const LeaderboardProvider = ({ children }) => {
|
|
303 |
totalLeaderboards,
|
304 |
filteredCount,
|
305 |
filterLeaderboards,
|
|
|
306 |
sections,
|
307 |
allSections,
|
308 |
getHighlightedText,
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
309 |
};
|
310 |
|
311 |
return (
|
|
|
4 |
useState,
|
5 |
useCallback,
|
6 |
useMemo,
|
7 |
+
useEffect,
|
8 |
} from "react";
|
9 |
|
10 |
const LeaderboardContext = createContext();
|
11 |
|
12 |
+
// Helper pour mettre à jour l'URL
|
13 |
+
const updateURL = (params) => {
|
14 |
+
const url = new URL(window.location);
|
15 |
+
Object.entries(params).forEach(([key, value]) => {
|
16 |
+
if (value) {
|
17 |
+
url.searchParams.set(key, value);
|
18 |
+
} else {
|
19 |
+
url.searchParams.delete(key);
|
20 |
+
}
|
21 |
+
});
|
22 |
+
window.history.pushState({}, "", url);
|
23 |
+
};
|
24 |
+
|
25 |
+
// Helper pour lire les paramètres de l'URL
|
26 |
+
const getURLParams = () => {
|
27 |
+
const params = new URLSearchParams(window.location.search);
|
28 |
+
return {
|
29 |
+
category: params.get("category"),
|
30 |
+
search: params.get("search"),
|
31 |
+
arena: params.get("arena") === "true",
|
32 |
+
language: params.get("language"),
|
33 |
+
};
|
34 |
+
};
|
35 |
+
|
36 |
export const LeaderboardProvider = ({ children }) => {
|
37 |
+
// Lire les paramètres initiaux depuis l'URL
|
38 |
+
const initialParams = getURLParams();
|
39 |
+
|
40 |
const [leaderboards, setLeaderboards] = useState([]);
|
41 |
+
const [searchQuery, setSearchQuery] = useState(initialParams.search || "");
|
42 |
+
const [arenaOnly, setArenaOnly] = useState(initialParams.arena);
|
43 |
+
const [selectedCategory, setSelectedCategory] = useState(
|
44 |
+
initialParams.category
|
45 |
+
);
|
46 |
+
const [selectedLanguage, setSelectedLanguage] = useState(
|
47 |
+
initialParams.language
|
48 |
+
);
|
49 |
+
const [expandedSections, setExpandedSections] = useState(new Set());
|
50 |
+
|
51 |
+
// Mettre à jour l'URL quand les filtres changent
|
52 |
+
useEffect(() => {
|
53 |
+
updateURL({
|
54 |
+
category: selectedCategory,
|
55 |
+
search: searchQuery,
|
56 |
+
arena: arenaOnly ? "true" : null,
|
57 |
+
language: selectedLanguage,
|
58 |
+
});
|
59 |
+
}, [selectedCategory, searchQuery, arenaOnly, selectedLanguage]);
|
60 |
+
|
61 |
+
// Écouter les changements d'URL (navigation back/forward)
|
62 |
+
useEffect(() => {
|
63 |
+
const handleURLChange = () => {
|
64 |
+
const params = getURLParams();
|
65 |
+
setSearchQuery(params.search || "");
|
66 |
+
setArenaOnly(params.arena);
|
67 |
+
setSelectedCategory(params.category);
|
68 |
+
setSelectedLanguage(params.language);
|
69 |
+
};
|
70 |
+
|
71 |
+
window.addEventListener("popstate", handleURLChange);
|
72 |
+
return () => window.removeEventListener("popstate", handleURLChange);
|
73 |
+
}, []);
|
74 |
+
|
75 |
+
// Wrapper pour setSelectedCategory qui gère aussi l'expansion des sections
|
76 |
+
const handleCategorySelection = useCallback((categoryId) => {
|
77 |
+
// On réinitialise toujours la langue sélectionnée
|
78 |
+
setSelectedLanguage(null);
|
79 |
+
|
80 |
+
setSelectedCategory((prev) => {
|
81 |
+
if (prev === categoryId) {
|
82 |
+
// Si on désélectionne, on replie toutes les sections
|
83 |
+
setExpandedSections(new Set());
|
84 |
+
return null;
|
85 |
+
}
|
86 |
+
// Si on sélectionne une nouvelle catégorie, on la déploie
|
87 |
+
setExpandedSections(new Set([categoryId]));
|
88 |
+
return categoryId;
|
89 |
+
});
|
90 |
+
}, []);
|
91 |
+
|
92 |
+
// Wrapper pour la sélection de langue
|
93 |
+
const handleLanguageSelection = useCallback((language) => {
|
94 |
+
setSelectedLanguage((prev) => (prev === language ? null : language));
|
95 |
+
}, []);
|
96 |
+
|
97 |
+
// Filter leaderboards based on search query and arena toggle, ignoring category selection
|
98 |
+
const filterLeaderboardsForCount = useCallback(
|
99 |
+
(boards) => {
|
100 |
+
if (!boards) return [];
|
101 |
+
|
102 |
+
let filtered = [...boards];
|
103 |
+
|
104 |
+
// Filter by search query (text only)
|
105 |
+
if (searchQuery) {
|
106 |
+
const query = searchQuery.toLowerCase();
|
107 |
+
filtered = filtered.filter((board) =>
|
108 |
+
board.card_data?.title?.toLowerCase().includes(query)
|
109 |
+
);
|
110 |
+
}
|
111 |
+
|
112 |
+
// Filter arena only
|
113 |
+
if (arenaOnly) {
|
114 |
+
filtered = filtered.filter((board) =>
|
115 |
+
board.tags?.includes("judge:humans")
|
116 |
+
);
|
117 |
+
}
|
118 |
+
|
119 |
+
return filtered;
|
120 |
+
},
|
121 |
+
[searchQuery, arenaOnly]
|
122 |
+
);
|
123 |
+
|
124 |
+
// Filter leaderboards based on all criteria including category and language selection
|
125 |
+
const filterLeaderboards = useCallback(
|
126 |
+
(boards) => {
|
127 |
+
if (!boards) return [];
|
128 |
+
|
129 |
+
let filtered = filterLeaderboardsForCount(boards);
|
130 |
+
|
131 |
+
// Filter by selected language if any
|
132 |
+
if (selectedLanguage) {
|
133 |
+
filtered = filtered.filter((board) =>
|
134 |
+
board.tags?.some(
|
135 |
+
(tag) =>
|
136 |
+
tag.toLowerCase() === `language:${selectedLanguage.toLowerCase()}`
|
137 |
+
)
|
138 |
+
);
|
139 |
+
}
|
140 |
+
|
141 |
+
// Filter by selected category if any
|
142 |
+
if (selectedCategory) {
|
143 |
+
filtered = filtered.filter((board) => {
|
144 |
+
const { tags = [] } = board;
|
145 |
+
switch (selectedCategory) {
|
146 |
+
case "agentic":
|
147 |
+
return tags.includes("modality:agent");
|
148 |
+
case "code":
|
149 |
+
return tags.includes("eval:code");
|
150 |
+
case "math":
|
151 |
+
return tags.includes("eval:math");
|
152 |
+
case "language":
|
153 |
+
return tags.some((tag) => tag.startsWith("language:"));
|
154 |
+
case "vision":
|
155 |
+
return tags.some(
|
156 |
+
(tag) => tag === "modality:video" || tag === "modality:image"
|
157 |
+
);
|
158 |
+
case "audio":
|
159 |
+
return tags.includes("modality:audio");
|
160 |
+
case "financial":
|
161 |
+
return tags.includes("domain:financial");
|
162 |
+
case "medical":
|
163 |
+
return tags.includes("domain:medical");
|
164 |
+
case "legal":
|
165 |
+
return tags.includes("domain:legal");
|
166 |
+
case "safety":
|
167 |
+
return tags.includes("eval:safety");
|
168 |
+
case "uncategorized":
|
169 |
+
return !tags.some(
|
170 |
+
(tag) =>
|
171 |
+
[
|
172 |
+
"modality:agent",
|
173 |
+
"eval:code",
|
174 |
+
"eval:math",
|
175 |
+
"modality:video",
|
176 |
+
"modality:image",
|
177 |
+
"modality:audio",
|
178 |
+
"domain:financial",
|
179 |
+
"domain:medical",
|
180 |
+
"domain:legal",
|
181 |
+
"eval:safety",
|
182 |
+
].includes(tag) || tag.startsWith("language:")
|
183 |
+
);
|
184 |
+
default:
|
185 |
+
return true;
|
186 |
+
}
|
187 |
+
});
|
188 |
+
}
|
189 |
+
|
190 |
+
return filtered;
|
191 |
+
},
|
192 |
+
[filterLeaderboardsForCount, selectedCategory, selectedLanguage]
|
193 |
+
);
|
194 |
+
|
195 |
+
// Fonction pour obtenir les leaderboards bruts d'une section
|
196 |
+
const getSectionLeaderboards = useCallback((boards) => {
|
197 |
+
if (!boards) return [];
|
198 |
+
return [...boards];
|
199 |
+
}, []);
|
200 |
+
|
201 |
+
// Fonction pour compter les leaderboards par langue
|
202 |
+
const getLanguageStats = useCallback((boards) => {
|
203 |
+
const stats = new Map();
|
204 |
+
|
205 |
+
boards.forEach((board) => {
|
206 |
+
board.tags?.forEach((tag) => {
|
207 |
+
if (tag.startsWith("language:")) {
|
208 |
+
const language = tag.split(":")[1];
|
209 |
+
const capitalizedLang =
|
210 |
+
language.charAt(0).toUpperCase() + language.slice(1);
|
211 |
+
const count = stats.get(capitalizedLang) || 0;
|
212 |
+
stats.set(capitalizedLang, count + 1);
|
213 |
+
}
|
214 |
+
});
|
215 |
+
});
|
216 |
+
|
217 |
+
return stats;
|
218 |
+
}, []);
|
219 |
|
220 |
// Calculate total number of unique leaderboards (excluding duplicates)
|
221 |
const totalLeaderboards = useMemo(() => {
|
|
|
261 |
);
|
262 |
}, []);
|
263 |
|
264 |
+
// Define sections with raw data
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
265 |
const allSections = useMemo(() => {
|
266 |
if (!leaderboards) return [];
|
267 |
|
|
|
|
|
|
|
268 |
// Garder une trace des leaderboards déjà catégorisés
|
269 |
const categorizedIds = new Set();
|
270 |
|
|
|
272 |
{
|
273 |
id: "agentic",
|
274 |
title: "Agentic",
|
275 |
+
data: filterByTag("modality:agent", leaderboards).map((board) => {
|
276 |
categorizedIds.add(board.id);
|
277 |
return board;
|
278 |
}),
|
|
|
280 |
{
|
281 |
id: "code",
|
282 |
title: "Code",
|
283 |
+
data: filterByTag("eval:code", leaderboards).map((board) => {
|
284 |
categorizedIds.add(board.id);
|
285 |
return board;
|
286 |
}),
|
|
|
288 |
{
|
289 |
id: "math",
|
290 |
title: "Math",
|
291 |
+
data: filterByTag("eval:math", leaderboards).map((board) => {
|
292 |
categorizedIds.add(board.id);
|
293 |
return board;
|
294 |
}),
|
|
|
296 |
{
|
297 |
id: "language",
|
298 |
title: "Language Specific",
|
299 |
+
data: filterByLanguage(leaderboards).map((board) => {
|
300 |
categorizedIds.add(board.id);
|
301 |
return board;
|
302 |
}),
|
|
|
304 |
{
|
305 |
id: "vision",
|
306 |
title: "Vision",
|
307 |
+
data: filterByVision(leaderboards).map((board) => {
|
308 |
categorizedIds.add(board.id);
|
309 |
return board;
|
310 |
}),
|
|
|
312 |
{
|
313 |
id: "audio",
|
314 |
title: "Audio",
|
315 |
+
data: filterByTag("modality:audio", leaderboards).map((board) => {
|
316 |
categorizedIds.add(board.id);
|
317 |
return board;
|
318 |
}),
|
|
|
320 |
{
|
321 |
id: "financial",
|
322 |
title: "Financial",
|
323 |
+
data: filterByTag("domain:financial", leaderboards).map((board) => {
|
324 |
categorizedIds.add(board.id);
|
325 |
return board;
|
326 |
}),
|
|
|
328 |
{
|
329 |
id: "medical",
|
330 |
title: "Medical",
|
331 |
+
data: filterByTag("domain:medical", leaderboards).map((board) => {
|
332 |
categorizedIds.add(board.id);
|
333 |
return board;
|
334 |
}),
|
|
|
336 |
{
|
337 |
id: "legal",
|
338 |
title: "Legal",
|
339 |
+
data: filterByTag("domain:legal", leaderboards).map((board) => {
|
340 |
categorizedIds.add(board.id);
|
341 |
return board;
|
342 |
}),
|
|
|
344 |
{
|
345 |
id: "safety",
|
346 |
title: "Safety",
|
347 |
+
data: filterByTag("eval:safety", leaderboards).map((board) => {
|
348 |
categorizedIds.add(board.id);
|
349 |
return board;
|
350 |
}),
|
|
|
353 |
id: "uncategorized",
|
354 |
title: "Uncategorized",
|
355 |
// Mettre dans uncategorized uniquement les leaderboards qui n'apparaissent nulle part ailleurs
|
356 |
+
data: leaderboards.filter((board) => !categorizedIds.has(board.id)),
|
357 |
},
|
358 |
];
|
359 |
|
360 |
return sections;
|
361 |
+
}, [leaderboards, filterByTag, filterByLanguage, filterByVision]);
|
|
|
|
|
|
|
|
|
|
|
|
|
362 |
|
363 |
// Get sections with data
|
364 |
const sections = useMemo(() => {
|
|
|
407 |
|
408 |
// Get filtered count
|
409 |
const filteredCount = useMemo(() => {
|
410 |
+
return filterLeaderboardsForCount(leaderboards).length;
|
411 |
+
}, [filterLeaderboardsForCount, leaderboards]);
|
412 |
|
413 |
// Function to get highlighted parts of text
|
414 |
const getHighlightedText = useCallback((text, searchTerm) => {
|
|
|
451 |
totalLeaderboards,
|
452 |
filteredCount,
|
453 |
filterLeaderboards,
|
454 |
+
filterLeaderboardsForCount,
|
455 |
sections,
|
456 |
allSections,
|
457 |
getHighlightedText,
|
458 |
+
selectedCategory,
|
459 |
+
setSelectedCategory: handleCategorySelection,
|
460 |
+
selectedLanguage,
|
461 |
+
setSelectedLanguage: handleLanguageSelection,
|
462 |
+
expandedSections,
|
463 |
+
setExpandedSections,
|
464 |
+
getLanguageStats,
|
465 |
+
getSectionLeaderboards,
|
466 |
};
|
467 |
|
468 |
return (
|
client/src/pages/HowToSubmitPage/HowToSubmitPage.jsx
CHANGED
@@ -205,7 +205,7 @@ const getTagEmoji = (tag) => {
|
|
205 |
function: "⚙️",
|
206 |
model: "🧠",
|
207 |
humans: "👥",
|
208 |
-
|
209 |
},
|
210 |
modality: {
|
211 |
text: "📝",
|
@@ -228,7 +228,7 @@ const getTagEmoji = (tag) => {
|
|
228 |
language: {
|
229 |
english: "🇬🇧",
|
230 |
french: "🇫🇷",
|
231 |
-
|
232 |
},
|
233 |
domain: {
|
234 |
financial: "💰",
|
@@ -645,7 +645,7 @@ const HowToSubmitPage = () => {
|
|
645 |
"judge:function",
|
646 |
"judge:model",
|
647 |
"judge:humans",
|
648 |
-
"judge:
|
649 |
]}
|
650 |
explanations={[
|
651 |
"evaluations are run <strong>automatically</strong>, using an evaluation suite such as <strong>lm_eval</strong> or <strong>lighteval</strong>",
|
@@ -685,7 +685,7 @@ const HowToSubmitPage = () => {
|
|
685 |
"eval:code",
|
686 |
"eval:performance",
|
687 |
"eval:safety",
|
688 |
-
"
|
689 |
]}
|
690 |
explanations={[
|
691 |
"the evaluation looks at <strong>generation capabilities</strong> specifically (can be image generation, text generation, ...)",
|
@@ -700,7 +700,11 @@ const HowToSubmitPage = () => {
|
|
700 |
<TagSection
|
701 |
title="Language"
|
702 |
description="You can indicate the languages covered by your benchmark like so: language:mylanguage."
|
703 |
-
tags={[
|
|
|
|
|
|
|
|
|
704 |
explanations={[
|
705 |
"",
|
706 |
"",
|
@@ -737,7 +741,7 @@ const HowToSubmitPage = () => {
|
|
737 |
},
|
738 |
}}
|
739 |
>
|
740 |
-
|
741 |
</Link>{" "}
|
742 |
on Hugging Face.
|
743 |
</Typography>
|
|
|
205 |
function: "⚙️",
|
206 |
model: "🧠",
|
207 |
humans: "👥",
|
208 |
+
vibeCheck: "✨",
|
209 |
},
|
210 |
modality: {
|
211 |
text: "📝",
|
|
|
228 |
language: {
|
229 |
english: "🇬🇧",
|
230 |
french: "🇫🇷",
|
231 |
+
yourOwnLanguage: "🌍",
|
232 |
},
|
233 |
domain: {
|
234 |
financial: "💰",
|
|
|
645 |
"judge:function",
|
646 |
"judge:model",
|
647 |
"judge:humans",
|
648 |
+
"judge:vibe check",
|
649 |
]}
|
650 |
explanations={[
|
651 |
"evaluations are run <strong>automatically</strong>, using an evaluation suite such as <strong>lm_eval</strong> or <strong>lighteval</strong>",
|
|
|
685 |
"eval:code",
|
686 |
"eval:performance",
|
687 |
"eval:safety",
|
688 |
+
"eval:rag",
|
689 |
]}
|
690 |
explanations={[
|
691 |
"the evaluation looks at <strong>generation capabilities</strong> specifically (can be image generation, text generation, ...)",
|
|
|
700 |
<TagSection
|
701 |
title="Language"
|
702 |
description="You can indicate the languages covered by your benchmark like so: language:mylanguage."
|
703 |
+
tags={[
|
704 |
+
"language:english",
|
705 |
+
"language:french",
|
706 |
+
"language:your own language",
|
707 |
+
]}
|
708 |
explanations={[
|
709 |
"",
|
710 |
"",
|
|
|
741 |
},
|
742 |
}}
|
743 |
>
|
744 |
+
Clémentine Fourrier
|
745 |
</Link>{" "}
|
746 |
on Hugging Face.
|
747 |
</Typography>
|
client/src/pages/LeaderboardPage/LeaderboardPage.jsx
CHANGED
@@ -20,6 +20,7 @@ const LeaderboardPageContent = () => {
|
|
20 |
allSections,
|
21 |
searchQuery,
|
22 |
arenaOnly,
|
|
|
23 |
} = useLeaderboard();
|
24 |
|
25 |
useEffect(() => {
|
@@ -96,7 +97,8 @@ const LeaderboardPageContent = () => {
|
|
96 |
>
|
97 |
<LeaderboardFilters allSections={allSections} />
|
98 |
|
99 |
-
{
|
|
|
100 |
<Box
|
101 |
sx={{
|
102 |
display: "flex",
|
@@ -140,19 +142,38 @@ const LeaderboardPageContent = () => {
|
|
140 |
</Box>
|
141 |
)}
|
142 |
|
143 |
-
{
|
144 |
-
|
145 |
-
|
146 |
-
|
147 |
-
|
148 |
-
|
149 |
-
|
150 |
-
|
151 |
-
|
152 |
-
|
153 |
-
|
154 |
-
|
155 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
156 |
</Box>
|
157 |
)}
|
158 |
</Box>
|
|
|
20 |
allSections,
|
21 |
searchQuery,
|
22 |
arenaOnly,
|
23 |
+
selectedCategory,
|
24 |
} = useLeaderboard();
|
25 |
|
26 |
useEffect(() => {
|
|
|
97 |
>
|
98 |
<LeaderboardFilters allSections={allSections} />
|
99 |
|
100 |
+
{/* Message global "No results" seulement si on n'a pas de catégorie sélectionnée */}
|
101 |
+
{!hasLeaderboards && isFiltering && !selectedCategory && (
|
102 |
<Box
|
103 |
sx={{
|
104 |
display: "flex",
|
|
|
142 |
</Box>
|
143 |
)}
|
144 |
|
145 |
+
{/* On affiche toujours la section sélectionnée, sinon on affiche les sections avec des résultats */}
|
146 |
+
{selectedCategory
|
147 |
+
? sections
|
148 |
+
.filter(({ id }) => id === selectedCategory)
|
149 |
+
.map(({ id, title, data }) => {
|
150 |
+
const filteredLeaderboards = filterLeaderboards(data);
|
151 |
+
return (
|
152 |
+
<Box key={id} id={id}>
|
153 |
+
<LeaderboardSection
|
154 |
+
id={id}
|
155 |
+
title={title}
|
156 |
+
leaderboards={data}
|
157 |
+
filteredLeaderboards={filteredLeaderboards}
|
158 |
+
/>
|
159 |
+
</Box>
|
160 |
+
);
|
161 |
+
})
|
162 |
+
: (hasLeaderboards || !isFiltering) &&
|
163 |
+
sections.map(({ id, title, data }) => {
|
164 |
+
const filteredLeaderboards = filterLeaderboards(data);
|
165 |
+
if (filteredLeaderboards.length === 0) return null;
|
166 |
+
return (
|
167 |
+
<Box key={id} id={id}>
|
168 |
+
<LeaderboardSection
|
169 |
+
id={id}
|
170 |
+
title={title}
|
171 |
+
leaderboards={data}
|
172 |
+
filteredLeaderboards={filteredLeaderboards}
|
173 |
+
/>
|
174 |
+
</Box>
|
175 |
+
);
|
176 |
+
})}
|
177 |
</Box>
|
178 |
)}
|
179 |
</Box>
|
server/pyproject.toml
CHANGED
@@ -3,6 +3,9 @@ name = "leaderboard-explorer-server"
|
|
3 |
version = "0.1.0"
|
4 |
description = "Backend server for Leaderboard Explorer"
|
5 |
authors = ["Your Name <[email protected]>"]
|
|
|
|
|
|
|
6 |
|
7 |
[tool.poetry.dependencies]
|
8 |
python = "^3.9"
|
@@ -14,6 +17,9 @@ python-jose = {extras = ["cryptography"], version = "^3.3.0"}
|
|
14 |
apscheduler = "^3.10.4"
|
15 |
huggingface-hub = "^0.21.3"
|
16 |
|
|
|
|
|
|
|
17 |
[build-system]
|
18 |
requires = ["poetry-core>=1.0.0"]
|
19 |
build-backend = "poetry.core.masonry.api"
|
|
|
3 |
version = "0.1.0"
|
4 |
description = "Backend server for Leaderboard Explorer"
|
5 |
authors = ["Your Name <[email protected]>"]
|
6 |
+
packages = [
|
7 |
+
{ include = "server.py" }
|
8 |
+
]
|
9 |
|
10 |
[tool.poetry.dependencies]
|
11 |
python = "^3.9"
|
|
|
17 |
apscheduler = "^3.10.4"
|
18 |
huggingface-hub = "^0.21.3"
|
19 |
|
20 |
+
[tool.poetry.scripts]
|
21 |
+
dev = "uvicorn server:app --reload --host 0.0.0.0 --port 3002"
|
22 |
+
|
23 |
[build-system]
|
24 |
requires = ["poetry-core>=1.0.0"]
|
25 |
build-backend = "poetry.core.masonry.api"
|