tfrere commited on
Commit
5621ad9
·
1 Parent(s): 606f214

add multi selection and update language specific list

Browse files
client/src/App.jsx CHANGED
@@ -1,11 +1,5 @@
1
- import React, { useEffect } from "react";
2
- import {
3
- HashRouter as Router,
4
- Routes,
5
- Route,
6
- useSearchParams,
7
- useLocation,
8
- } from "react-router-dom";
9
  import { ThemeProvider } from "@mui/material/styles";
10
  import { Box, CssBaseline } from "@mui/material";
11
  import Navigation from "./components/Navigation/Navigation";
@@ -15,38 +9,6 @@ import Footer from "./components/Footer/Footer";
15
  import getTheme from "./config/theme";
16
  import { useThemeMode } from "./hooks/useThemeMode";
17
 
18
- function UrlHandler() {
19
- const location = useLocation();
20
- const [searchParams] = useSearchParams();
21
-
22
- useEffect(() => {
23
- const isHFSpace = window.location !== window.parent.location;
24
- if (!isHFSpace) return;
25
-
26
- const queryString = window.location.search;
27
- const hash = window.location.hash;
28
-
29
- window.parent.postMessage(
30
- {
31
- queryString,
32
- hash,
33
- },
34
- "https://huggingface.co"
35
- );
36
- }, [location, searchParams]);
37
-
38
- useEffect(() => {
39
- const handleHashChange = (event) => {
40
- console.log("hash change event", event);
41
- };
42
-
43
- window.addEventListener("hashchange", handleHashChange);
44
- return () => window.removeEventListener("hashchange", handleHashChange);
45
- }, []);
46
-
47
- return null;
48
- }
49
-
50
  function App() {
51
  const { mode, toggleTheme } = useThemeMode();
52
  const theme = getTheme(mode);
@@ -64,7 +26,6 @@ function App() {
64
  <ThemeProvider theme={theme}>
65
  <CssBaseline />
66
  <Router>
67
- <UrlHandler />
68
  <Box
69
  sx={{
70
  minHeight: "100vh",
 
1
+ import React from "react";
2
+ import { HashRouter as Router, Routes, Route } from "react-router-dom";
 
 
 
 
 
 
3
  import { ThemeProvider } from "@mui/material/styles";
4
  import { Box, CssBaseline } from "@mui/material";
5
  import Navigation from "./components/Navigation/Navigation";
 
9
  import getTheme from "./config/theme";
10
  import { useThemeMode } from "./hooks/useThemeMode";
11
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
12
  function App() {
13
  const { mode, toggleTheme } = useThemeMode();
14
  const theme = getTheme(mode);
 
26
  <ThemeProvider theme={theme}>
27
  <CssBaseline />
28
  <Router>
 
29
  <Box
30
  sx={{
31
  minHeight: "100vh",
client/src/components/LeaderboardFilters/LeaderboardFilters.jsx CHANGED
@@ -5,6 +5,32 @@ import { useDebounce } from "../../hooks/useDebounce";
5
  import { alpha, lighten, darken } from "@mui/material/styles";
6
  import SearchBar from "./SearchBar";
7
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
8
  // Helper function to get the category group of a section
9
  const getSectionGroup = (id) => {
10
  const groups = {
@@ -65,8 +91,8 @@ const LeaderboardFilters = ({ allSections = [] }) => {
65
  arenaOnly,
66
  setArenaOnly,
67
  filterLeaderboards,
68
- selectedCategory,
69
- setSelectedCategory,
70
  searchQuery,
71
  } = useLeaderboard();
72
 
@@ -86,70 +112,208 @@ const LeaderboardFilters = ({ allSections = [] }) => {
86
 
87
  // Update total arena count
88
  React.useEffect(() => {
89
- const boardsToCount = selectedCategory
90
- ? allSections.find((section) => section.id === selectedCategory)?.data ||
91
- []
92
- : allSections.reduce(
93
- (acc, section) => [...acc, ...(section.data || [])],
94
- []
95
- );
96
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
97
  const arenaCount = boardsToCount.filter((board) =>
98
  board.tags?.includes("judge:humans")
99
  ).length;
100
  setTotalArenaCount(arenaCount);
101
- }, [selectedCategory, allSections]);
102
 
103
- // Calculer le nombre total en fonction de la catégorie sélectionnée
104
  const totalCount = useMemo(() => {
105
  if (!allSections) return 0;
106
 
107
- if (selectedCategory) {
108
- const categorySection = allSections.find(
109
- (section) => section.id === selectedCategory
110
- );
111
- return categorySection?.data?.length || 0;
112
  }
113
 
114
- const uniqueIds = new Set();
115
- allSections.forEach((section) => {
116
- (section.data || []).forEach((board) => {
117
- if (board.approval_status === "approved") {
118
- uniqueIds.add(board.id);
119
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
120
  });
121
- });
122
- return uniqueIds.size;
123
- }, [selectedCategory, allSections]);
 
 
 
 
 
 
 
 
 
 
124
 
125
  // Calculer le nombre filtré en prenant en compte tous les filtres
126
  const currentFilteredCount = useMemo(() => {
127
  if (!allSections) return 0;
128
 
129
- if (arenaOnly && !searchQuery && !selectedCategory) {
130
  return totalArenaCount;
131
  }
132
 
133
- if (selectedCategory) {
134
- const categorySection = allSections.find(
135
- (section) => section.id === selectedCategory
136
- );
137
- if (!categorySection?.data) return 0;
138
- return filterLeaderboards(categorySection.data).length;
139
- }
 
140
 
141
- const uniqueFilteredIds = new Set();
142
- allSections.forEach((section) => {
143
- const filteredBoards = filterLeaderboards(section.data || []);
144
- filteredBoards.forEach((board) => {
145
- if (board.approval_status === "approved") {
146
- uniqueFilteredIds.add(board.id);
147
- }
148
- });
149
- });
150
- return uniqueFilteredIds.size;
151
  }, [
152
- selectedCategory,
153
  allSections,
154
  filterLeaderboards,
155
  arenaOnly,
@@ -184,6 +348,7 @@ const LeaderboardFilters = ({ allSections = [] }) => {
184
  const prevGroup =
185
  index > 0 ? getSectionGroup(allSections[index - 1].id) : null;
186
  const needsSpacing = currentGroup !== prevGroup;
 
187
 
188
  return (
189
  <React.Fragment key={id}>
@@ -192,22 +357,19 @@ const LeaderboardFilters = ({ allSections = [] }) => {
192
  )}
193
  <Button
194
  onClick={() => {
195
- if (selectedCategory === id || count > 0) {
196
- setSelectedCategory(selectedCategory === id ? null : id);
197
  }
198
  }}
199
- variant={selectedCategory === id ? "contained" : "outlined"}
200
  size="small"
201
- disabled={count === 0 && selectedCategory !== id}
202
  sx={{
203
  textTransform: "none",
204
- cursor:
205
- count === 0 && selectedCategory !== id
206
- ? "default"
207
- : "pointer",
208
  mb: 1,
209
  backgroundColor: (theme) => {
210
- if (selectedCategory === id) {
211
  return groupColors[currentGroup]?.main;
212
  }
213
  return theme.palette.mode === "dark"
@@ -215,7 +377,7 @@ const LeaderboardFilters = ({ allSections = [] }) => {
215
  : groupColors[currentGroup]?.light;
216
  },
217
  color: (theme) => {
218
- if (selectedCategory === id) {
219
  return "white";
220
  }
221
  return groupColors[currentGroup]?.main;
@@ -225,7 +387,7 @@ const LeaderboardFilters = ({ allSections = [] }) => {
225
  },
226
  "&:hover": {
227
  backgroundColor: (theme) => {
228
- if (selectedCategory === id) {
229
  return groupColors[currentGroup]?.main;
230
  }
231
  return groupColors[currentGroup]?.light;
@@ -245,7 +407,7 @@ const LeaderboardFilters = ({ allSections = [] }) => {
245
  alignItems: "center",
246
  gap: 0.75,
247
  color: (theme) => {
248
- if (selectedCategory === id) {
249
  return "white";
250
  }
251
  // Assombrir la couleur principale du groupe
@@ -262,13 +424,12 @@ const LeaderboardFilters = ({ allSections = [] }) => {
262
  width: "4px",
263
  height: "4px",
264
  borderRadius: "100%",
265
- backgroundColor:
266
- selectedCategory === id
267
- ? "rgba(255, 255, 255, 0.5)"
268
- : alpha(
269
- groupColors[currentGroup]?.main,
270
- theme.palette.mode === "dark" ? 0.4 : 0.3
271
- ),
272
  })}
273
  />
274
  {count}
 
5
  import { alpha, lighten, darken } from "@mui/material/styles";
6
  import SearchBar from "./SearchBar";
7
 
8
+ // Constantes pour les tags de catégorisation
9
+ const CATEGORIZATION_TAGS = [
10
+ "modality:agent",
11
+ "modality:artefacts",
12
+ "modality:text",
13
+ "eval:code",
14
+ "eval:math",
15
+ "eval:reasoning",
16
+ "eval:hallucination",
17
+ "modality:video",
18
+ "modality:image",
19
+ "modality:3d",
20
+ "modality:audio",
21
+ "domain:financial",
22
+ "domain:medical",
23
+ "domain:legal",
24
+ "domain:biology",
25
+ "domain:translation",
26
+ "domain:chemistry",
27
+ "domain:physics",
28
+ "domain:commercial",
29
+ "eval:safety",
30
+ "eval:performance",
31
+ "eval:rag",
32
+ ];
33
+
34
  // Helper function to get the category group of a section
35
  const getSectionGroup = (id) => {
36
  const groups = {
 
91
  arenaOnly,
92
  setArenaOnly,
93
  filterLeaderboards,
94
+ selectedCategories,
95
+ setSelectedCategories,
96
  searchQuery,
97
  } = useLeaderboard();
98
 
 
112
 
113
  // Update total arena count
114
  React.useEffect(() => {
115
+ const allLeaderboards = Array.from(
116
+ new Set(
117
+ allSections.reduce((acc, section) => {
118
+ return [...acc, ...(section.data || [])];
119
+ }, [])
120
+ )
121
+ );
122
 
123
+ // Filtrer d'abord par catégories si nécessaire
124
+ let boardsToCount = allLeaderboards;
125
+ if (selectedCategories.size > 0) {
126
+ boardsToCount = boardsToCount.filter((board) => {
127
+ if (board.approval_status !== "approved") return false;
128
+ const tags = board.tags || [];
129
+ return Array.from(selectedCategories).every((category) => {
130
+ switch (category) {
131
+ case "agentic":
132
+ return tags.includes("modality:agent");
133
+ case "text":
134
+ return tags.includes("modality:text");
135
+ case "image":
136
+ return tags.includes("modality:image");
137
+ case "video":
138
+ return tags.includes("modality:video");
139
+ case "code":
140
+ return tags.includes("eval:code");
141
+ case "math":
142
+ return tags.includes("eval:math");
143
+ case "reasoning":
144
+ return tags.includes("eval:reasoning");
145
+ case "hallucination":
146
+ return tags.includes("eval:hallucination");
147
+ case "rag":
148
+ return tags.includes("eval:rag");
149
+ case "language":
150
+ return tags.some((tag) => tag.startsWith("language:"));
151
+ case "vision":
152
+ return tags.some(
153
+ (tag) => tag === "modality:video" || tag === "modality:image"
154
+ );
155
+ case "threeD":
156
+ return tags.includes("modality:3d");
157
+ case "audio":
158
+ return tags.includes("modality:audio");
159
+ case "financial":
160
+ return tags.includes("domain:financial");
161
+ case "medical":
162
+ return tags.includes("domain:medical");
163
+ case "legal":
164
+ return tags.includes("domain:legal");
165
+ case "biology":
166
+ return tags.includes("domain:biology");
167
+ case "commercial":
168
+ return tags.includes("domain:commercial");
169
+ case "translation":
170
+ return tags.includes("domain:translation");
171
+ case "chemistry":
172
+ return tags.includes("domain:chemistry");
173
+ case "safety":
174
+ return tags.includes("eval:safety");
175
+ case "performance":
176
+ return tags.includes("eval:performance");
177
+ case "uncategorized":
178
+ return !tags.some(
179
+ (tag) =>
180
+ CATEGORIZATION_TAGS.includes(tag) ||
181
+ tag.startsWith("language:")
182
+ );
183
+ default:
184
+ return false;
185
+ }
186
+ });
187
+ });
188
+ }
189
+
190
+ // Ensuite compter les leaderboards arena
191
  const arenaCount = boardsToCount.filter((board) =>
192
  board.tags?.includes("judge:humans")
193
  ).length;
194
  setTotalArenaCount(arenaCount);
195
+ }, [selectedCategories, allSections]);
196
 
197
+ // Calculer le nombre total en fonction des catégories sélectionnées
198
  const totalCount = useMemo(() => {
199
  if (!allSections) return 0;
200
 
201
+ if (arenaOnly && selectedCategories.size === 0) {
202
+ return totalArenaCount;
 
 
 
203
  }
204
 
205
+ // Récupérer tous les leaderboards uniques de toutes les sections
206
+ const allLeaderboards = Array.from(
207
+ new Set(
208
+ allSections.reduce((acc, section) => {
209
+ return [...acc, ...(section.data || [])];
210
+ }, [])
211
+ )
212
+ );
213
+
214
+ // Si des catégories sont sélectionnées, filtrer les leaderboards qui correspondent à TOUTES les catégories
215
+ let filteredBoards = allLeaderboards;
216
+ if (selectedCategories.size > 0) {
217
+ filteredBoards = filteredBoards.filter((board) => {
218
+ if (board.approval_status !== "approved") return false;
219
+ const tags = board.tags || [];
220
+ return Array.from(selectedCategories).every((category) => {
221
+ switch (category) {
222
+ case "agentic":
223
+ return tags.includes("modality:agent");
224
+ case "text":
225
+ return tags.includes("modality:text");
226
+ case "image":
227
+ return tags.includes("modality:image");
228
+ case "video":
229
+ return tags.includes("modality:video");
230
+ case "code":
231
+ return tags.includes("eval:code");
232
+ case "math":
233
+ return tags.includes("eval:math");
234
+ case "reasoning":
235
+ return tags.includes("eval:reasoning");
236
+ case "hallucination":
237
+ return tags.includes("eval:hallucination");
238
+ case "rag":
239
+ return tags.includes("eval:rag");
240
+ case "language":
241
+ return tags.some((tag) => tag.startsWith("language:"));
242
+ case "vision":
243
+ return tags.some(
244
+ (tag) => tag === "modality:video" || tag === "modality:image"
245
+ );
246
+ case "threeD":
247
+ return tags.includes("modality:3d");
248
+ case "audio":
249
+ return tags.includes("modality:audio");
250
+ case "financial":
251
+ return tags.includes("domain:financial");
252
+ case "medical":
253
+ return tags.includes("domain:medical");
254
+ case "legal":
255
+ return tags.includes("domain:legal");
256
+ case "biology":
257
+ return tags.includes("domain:biology");
258
+ case "commercial":
259
+ return tags.includes("domain:commercial");
260
+ case "translation":
261
+ return tags.includes("domain:translation");
262
+ case "chemistry":
263
+ return tags.includes("domain:chemistry");
264
+ case "safety":
265
+ return tags.includes("eval:safety");
266
+ case "performance":
267
+ return tags.includes("eval:performance");
268
+ case "uncategorized":
269
+ return !tags.some(
270
+ (tag) =>
271
+ CATEGORIZATION_TAGS.includes(tag) ||
272
+ tag.startsWith("language:")
273
+ );
274
+ default:
275
+ return false;
276
+ }
277
+ });
278
  });
279
+ }
280
+
281
+ // Appliquer le filtre Arena Only si nécessaire
282
+ if (arenaOnly) {
283
+ filteredBoards = filteredBoards.filter((board) =>
284
+ board.tags?.includes("judge:humans")
285
+ );
286
+ }
287
+
288
+ return filteredBoards.filter(
289
+ (board) => board.approval_status === "approved"
290
+ ).length;
291
+ }, [selectedCategories, allSections, arenaOnly, totalArenaCount]);
292
 
293
  // Calculer le nombre filtré en prenant en compte tous les filtres
294
  const currentFilteredCount = useMemo(() => {
295
  if (!allSections) return 0;
296
 
297
+ if (arenaOnly && !searchQuery && selectedCategories.size === 0) {
298
  return totalArenaCount;
299
  }
300
 
301
+ // Récupérer tous les leaderboards uniques de toutes les sections
302
+ const allLeaderboards = Array.from(
303
+ new Set(
304
+ allSections.reduce((acc, section) => {
305
+ return [...acc, ...(section.data || [])];
306
+ }, [])
307
+ )
308
+ );
309
 
310
+ // Appliquer les filtres sur l'ensemble des leaderboards
311
+ const filteredBoards = filterLeaderboards(allLeaderboards);
312
+ return filteredBoards.filter(
313
+ (board) => board.approval_status === "approved"
314
+ ).length;
 
 
 
 
 
315
  }, [
316
+ selectedCategories,
317
  allSections,
318
  filterLeaderboards,
319
  arenaOnly,
 
348
  const prevGroup =
349
  index > 0 ? getSectionGroup(allSections[index - 1].id) : null;
350
  const needsSpacing = currentGroup !== prevGroup;
351
+ const isSelected = selectedCategories.has(id);
352
 
353
  return (
354
  <React.Fragment key={id}>
 
357
  )}
358
  <Button
359
  onClick={() => {
360
+ if (isSelected || count > 0) {
361
+ setSelectedCategories(id);
362
  }
363
  }}
364
+ variant={isSelected ? "contained" : "outlined"}
365
  size="small"
366
+ disabled={count === 0 && !isSelected}
367
  sx={{
368
  textTransform: "none",
369
+ cursor: count === 0 && !isSelected ? "default" : "pointer",
 
 
 
370
  mb: 1,
371
  backgroundColor: (theme) => {
372
+ if (isSelected) {
373
  return groupColors[currentGroup]?.main;
374
  }
375
  return theme.palette.mode === "dark"
 
377
  : groupColors[currentGroup]?.light;
378
  },
379
  color: (theme) => {
380
+ if (isSelected) {
381
  return "white";
382
  }
383
  return groupColors[currentGroup]?.main;
 
387
  },
388
  "&:hover": {
389
  backgroundColor: (theme) => {
390
+ if (isSelected) {
391
  return groupColors[currentGroup]?.main;
392
  }
393
  return groupColors[currentGroup]?.light;
 
407
  alignItems: "center",
408
  gap: 0.75,
409
  color: (theme) => {
410
+ if (isSelected) {
411
  return "white";
412
  }
413
  // Assombrir la couleur principale du groupe
 
424
  width: "4px",
425
  height: "4px",
426
  borderRadius: "100%",
427
+ backgroundColor: isSelected
428
+ ? "rgba(255, 255, 255, 0.5)"
429
+ : alpha(
430
+ groupColors[currentGroup]?.main,
431
+ theme.palette.mode === "dark" ? 0.4 : 0.3
432
+ ),
 
433
  })}
434
  />
435
  {count}
client/src/components/LeaderboardSection/components/LanguageAccordion.jsx CHANGED
@@ -1,16 +1,13 @@
1
  import React from "react";
2
- import {
3
- Accordion,
4
- AccordionSummary,
5
- AccordionDetails,
6
- Typography,
7
- Box,
8
- Button,
9
- } from "@mui/material";
10
  import { alpha } from "@mui/material/styles";
11
- import ExpandMoreIcon from "@mui/icons-material/ExpandMore";
12
 
13
- const LanguageAccordion = ({
 
 
 
 
 
14
  languages,
15
  languageStats,
16
  selectedLanguage,
@@ -18,150 +15,124 @@ const LanguageAccordion = ({
18
  LANGUAGE_FAMILIES,
19
  findLanguageFamily,
20
  }) => {
 
 
 
 
 
 
 
 
 
 
 
 
21
  return (
22
- <Accordion
23
- defaultExpanded={false}
24
- elevation={0}
25
- disableGutters
26
- sx={{
27
- mb: 4,
28
- "&:before": {
29
- display: "none",
30
- },
31
- backgroundColor: "transparent",
32
- }}
33
- >
34
- <AccordionSummary
35
- expandIcon={
36
- <ExpandMoreIcon sx={{ fontSize: 20, color: "text.secondary" }} />
37
- }
38
  sx={{
39
- p: 0,
40
- minHeight: 32,
41
- height: 32,
42
- "&.MuiAccordionSummary-root": {
43
- "&.Mui-expanded": {
44
- minHeight: 32,
45
- height: 32,
46
- },
47
- },
48
- "& .MuiAccordionSummary-content": {
49
- m: 0,
50
- "&.Mui-expanded": {
51
- m: 0,
52
- },
53
- },
54
  }}
55
  >
56
- <Typography
57
- variant="body2"
58
- color="text.secondary"
59
- sx={{ fontWeight: 500 }}
60
- >
61
- Filter by language
62
- </Typography>
63
- </AccordionSummary>
64
- <AccordionDetails sx={{ padding: 0 }}>
65
- {Object.entries(LANGUAGE_FAMILIES).map(([family, subFamilies]) => {
66
- const familyLanguages = languages.filter((lang) => {
67
- const { family: langFamily } = findLanguageFamily(lang);
68
- return langFamily === family;
69
- });
70
-
71
- if (familyLanguages.length === 0 && family !== "Other Languages")
72
- return null;
73
-
74
- const familyTotal = familyLanguages.reduce(
75
- (sum, lang) => sum + (languageStats?.get(lang) || 0),
76
- 0
77
- );
78
 
79
- if (familyTotal === 0) return null;
80
-
81
- return (
82
- <Box key={family} sx={{ mb: 3 }}>
83
- <Typography
84
- variant="body2"
85
- color="text.secondary"
86
- sx={{ mb: 1, opacity: 0.7 }}
87
- >
88
- {family}
89
- </Typography>
90
- <Box sx={{ display: "flex", flexWrap: "wrap", gap: 0.5 }}>
91
- {familyLanguages.map((lang) => {
92
- const count = languageStats?.get(lang) || 0;
93
- if (count === 0) return null;
94
-
95
- const isActive = selectedLanguage === lang;
96
- const isDisabled = count === 0;
97
-
98
- return (
99
- <Button
100
- key={lang}
101
- onClick={() => onLanguageSelect(isActive ? null : lang)}
102
- variant={isActive ? "contained" : "outlined"}
103
- size="small"
104
- disabled={isDisabled}
105
- sx={{
106
- textTransform: "none",
107
- m: 0.125,
108
- opacity: isDisabled ? 0.5 : 1,
109
- backgroundColor: (theme) =>
110
- isActive
111
- ? undefined
112
- : theme.palette.mode === "dark"
113
- ? "background.paper"
114
- : "white",
115
- "&:hover": {
116
- backgroundColor: (theme) =>
117
- isActive
118
- ? undefined
119
- : theme.palette.mode === "dark"
120
- ? "background.paper"
121
- : "white",
122
- opacity: isDisabled ? 0.5 : 0.8,
123
- },
124
- "& .MuiTouchRipple-root": {
125
- transition: "none",
126
  },
 
 
 
127
  transition: "none",
 
 
 
 
 
 
 
 
 
 
 
 
 
128
  }}
129
  >
130
- {lang}
131
  <Box
132
  component="span"
133
- sx={{
134
- display: "inline-flex",
135
- alignItems: "center",
136
- gap: 0.75,
137
- color: isActive ? "inherit" : "text.secondary",
138
- ml: 0.75,
139
- }}
140
- >
141
- <Box
142
- component="span"
143
- sx={(theme) => ({
144
- width: "4px",
145
- height: "4px",
146
- borderRadius: "100%",
147
- backgroundColor: alpha(
148
- theme.palette.text.primary,
149
- theme.palette.mode === "dark" ? 0.2 : 0.15
150
- ),
151
- })}
152
- />
153
- {count}
154
- </Box>
155
- </Button>
156
- );
157
- })}
158
- </Box>
159
  </Box>
160
- );
161
- })}
162
- </AccordionDetails>
163
- </Accordion>
164
  );
165
  };
166
 
167
- export default LanguageAccordion;
 
1
  import React from "react";
2
+ import { Typography, Box, Button } from "@mui/material";
 
 
 
 
 
 
 
3
  import { alpha } from "@mui/material/styles";
 
4
 
5
+ const TAG_COLOR = {
6
+ main: "#F44336",
7
+ light: "#FFEBEE",
8
+ };
9
+
10
+ const LanguageList = ({
11
  languages,
12
  languageStats,
13
  selectedLanguage,
 
15
  LANGUAGE_FAMILIES,
16
  findLanguageFamily,
17
  }) => {
18
+ // Group languages by family
19
+ const groupedLanguages = languages.reduce((acc, lang) => {
20
+ const { family } = findLanguageFamily(lang);
21
+ if (!acc[family]) {
22
+ acc[family] = [];
23
+ }
24
+ acc[family].push(lang);
25
+ return acc;
26
+ }, {});
27
+
28
+ const capitalize = (str) => str.charAt(0).toUpperCase() + str.slice(1);
29
+
30
  return (
31
+ <Box sx={{ mb: 4 }}>
32
+ <Typography
33
+ variant="body2"
34
+ color="text.secondary"
35
+ sx={{ fontWeight: 500, mb: 2 }}
36
+ >
37
+ Filter by language
38
+ </Typography>
39
+ <Box
 
 
 
 
 
 
 
40
  sx={{
41
+ display: "flex",
42
+ flexWrap: "wrap",
43
+ gap: 3,
 
 
 
 
 
 
 
 
 
 
 
 
44
  }}
45
  >
46
+ {Object.entries(groupedLanguages).map(
47
+ ([family, familyLanguages], familyIndex) => (
48
+ <Box
49
+ key={family}
50
+ sx={{
51
+ display: "flex",
52
+ flexWrap: "wrap",
53
+ gap: 1,
54
+ }}
55
+ >
56
+ {familyLanguages.map((lang) => {
57
+ const isActive = selectedLanguage === lang;
58
+ const count = languageStats?.get(lang) || 0;
 
 
 
 
 
 
 
 
 
59
 
60
+ return (
61
+ <Button
62
+ key={lang}
63
+ onClick={() => onLanguageSelect(isActive ? null : lang)}
64
+ variant={isActive ? "contained" : "outlined"}
65
+ size="small"
66
+ sx={{
67
+ textTransform: "none",
68
+ cursor: "pointer",
69
+ mb: 0.75,
70
+ backgroundColor: (theme) => {
71
+ if (isActive) {
72
+ return TAG_COLOR.main;
73
+ }
74
+ return theme.palette.mode === "dark"
75
+ ? "background.paper"
76
+ : TAG_COLOR.light;
77
+ },
78
+ color: (theme) => {
79
+ if (isActive) {
80
+ return "white";
81
+ }
82
+ return TAG_COLOR.main;
83
+ },
84
+ borderColor: TAG_COLOR.main,
85
+ "&:hover": {
86
+ backgroundColor: (theme) => {
87
+ if (isActive) {
88
+ return TAG_COLOR.main;
89
+ }
90
+ return TAG_COLOR.light;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
91
  },
92
+ opacity: 0.8,
93
+ },
94
+ "& .MuiTouchRipple-root": {
95
  transition: "none",
96
+ },
97
+ transition: "none",
98
+ }}
99
+ >
100
+ {capitalize(lang)}
101
+ <Box
102
+ component="span"
103
+ sx={{
104
+ display: "inline-flex",
105
+ alignItems: "center",
106
+ gap: 0.75,
107
+ color: isActive ? "white" : "inherit",
108
+ ml: 0.75,
109
  }}
110
  >
 
111
  <Box
112
  component="span"
113
+ sx={(theme) => ({
114
+ width: "4px",
115
+ height: "4px",
116
+ borderRadius: "100%",
117
+ backgroundColor: isActive
118
+ ? "rgba(255, 255, 255, 0.5)"
119
+ : alpha(
120
+ TAG_COLOR.main,
121
+ theme.palette.mode === "dark" ? 0.4 : 0.3
122
+ ),
123
+ })}
124
+ />
125
+ {count}
126
+ </Box>
127
+ </Button>
128
+ );
129
+ })}
 
 
 
 
 
 
 
 
 
130
  </Box>
131
+ )
132
+ )}
133
+ </Box>
134
+ </Box>
135
  );
136
  };
137
 
138
+ export default LanguageList;
client/src/components/LeaderboardSection/components/LanguageList.jsx ADDED
@@ -0,0 +1,188 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React, { useMemo } from "react";
2
+ import {
3
+ Typography,
4
+ Box,
5
+ Button,
6
+ Accordion,
7
+ AccordionSummary,
8
+ AccordionDetails,
9
+ } from "@mui/material";
10
+ import { alpha } from "@mui/material/styles";
11
+ import ExpandMoreIcon from "@mui/icons-material/ExpandMore";
12
+ import { useLeaderboard } from "../../../context/LeaderboardContext";
13
+
14
+ const TAG_COLOR = {
15
+ main: "#F44336",
16
+ light: "#FFEBEE",
17
+ };
18
+
19
+ const LanguageList = ({
20
+ languages,
21
+ languageStats,
22
+ selectedLanguage,
23
+ onLanguageSelect,
24
+ LANGUAGE_FAMILIES,
25
+ findLanguageFamily,
26
+ }) => {
27
+ const { isLanguageExpanded, setIsLanguageExpanded } = useLeaderboard();
28
+
29
+ // Store initial language stats in a memo to keep them constant
30
+ const initialLanguageStats = useMemo(() => new Map(languageStats), []);
31
+
32
+ // Group languages by family
33
+ const groupedLanguages = languages.reduce((acc, lang) => {
34
+ const { family } = findLanguageFamily(lang);
35
+ if (!acc[family]) {
36
+ acc[family] = [];
37
+ }
38
+ acc[family].push(lang);
39
+ return acc;
40
+ }, {});
41
+
42
+ const capitalize = (str) => str.charAt(0).toUpperCase() + str.slice(1);
43
+
44
+ const handleAccordionChange = (event, expanded) => {
45
+ setIsLanguageExpanded(expanded);
46
+ };
47
+
48
+ return (
49
+ <Box sx={{ mb: 4 }}>
50
+ <Accordion
51
+ expanded={isLanguageExpanded}
52
+ onChange={handleAccordionChange}
53
+ disableGutters
54
+ elevation={0}
55
+ sx={{
56
+ "&:before": {
57
+ display: "none",
58
+ },
59
+ bgcolor: "transparent",
60
+ }}
61
+ >
62
+ <AccordionSummary
63
+ expandIcon={<ExpandMoreIcon />}
64
+ aria-controls="language-filter-content"
65
+ id="language-filter-header"
66
+ sx={{
67
+ p: 0,
68
+ minHeight: "unset",
69
+ "& .MuiAccordionSummary-content": {
70
+ m: 0,
71
+ alignItems: "center",
72
+ },
73
+ "& .MuiAccordionSummary-expandIconWrapper": {
74
+ ml: 1,
75
+ },
76
+ }}
77
+ >
78
+ <Typography
79
+ variant="body2"
80
+ color="text.secondary"
81
+ sx={{ fontWeight: 500 }}
82
+ >
83
+ Filter by language
84
+ </Typography>
85
+ </AccordionSummary>
86
+ <AccordionDetails sx={{ p: 0, pt: 2 }}>
87
+ <Box
88
+ sx={{
89
+ display: "flex",
90
+ flexWrap: "wrap",
91
+ gap: 3,
92
+ }}
93
+ >
94
+ {Object.entries(groupedLanguages).map(
95
+ ([family, familyLanguages], familyIndex) => (
96
+ <Box
97
+ key={family}
98
+ sx={{
99
+ display: "flex",
100
+ flexWrap: "wrap",
101
+ gap: 1,
102
+ }}
103
+ >
104
+ {familyLanguages.map((lang) => {
105
+ const isActive = selectedLanguage === lang;
106
+ const count = initialLanguageStats.get(lang) || 0;
107
+
108
+ return (
109
+ <Button
110
+ key={lang}
111
+ onClick={() => onLanguageSelect(isActive ? null : lang)}
112
+ variant={isActive ? "contained" : "outlined"}
113
+ size="small"
114
+ sx={{
115
+ textTransform: "none",
116
+ cursor: "pointer",
117
+ mb: 0.75,
118
+ backgroundColor: (theme) => {
119
+ if (isActive) {
120
+ return TAG_COLOR.main;
121
+ }
122
+ return theme.palette.mode === "dark"
123
+ ? "background.paper"
124
+ : TAG_COLOR.light;
125
+ },
126
+ color: (theme) => {
127
+ if (isActive) {
128
+ return "white";
129
+ }
130
+ return TAG_COLOR.main;
131
+ },
132
+ borderColor: TAG_COLOR.main,
133
+ "&:hover": {
134
+ backgroundColor: (theme) => {
135
+ if (isActive) {
136
+ return TAG_COLOR.main;
137
+ }
138
+ return TAG_COLOR.light;
139
+ },
140
+ opacity: 0.8,
141
+ },
142
+ "& .MuiTouchRipple-root": {
143
+ transition: "none",
144
+ },
145
+ transition: "none",
146
+ }}
147
+ >
148
+ {capitalize(lang)}
149
+ <Box
150
+ component="span"
151
+ sx={{
152
+ display: "inline-flex",
153
+ alignItems: "center",
154
+ gap: 0.75,
155
+ color: isActive ? "white" : "inherit",
156
+ ml: 0.75,
157
+ }}
158
+ >
159
+ <Box
160
+ component="span"
161
+ sx={(theme) => ({
162
+ width: "4px",
163
+ height: "4px",
164
+ borderRadius: "100%",
165
+ backgroundColor: isActive
166
+ ? "rgba(255, 255, 255, 0.5)"
167
+ : alpha(
168
+ TAG_COLOR.main,
169
+ theme.palette.mode === "dark" ? 0.4 : 0.3
170
+ ),
171
+ })}
172
+ />
173
+ {count}
174
+ </Box>
175
+ </Button>
176
+ );
177
+ })}
178
+ </Box>
179
+ )
180
+ )}
181
+ </Box>
182
+ </AccordionDetails>
183
+ </Accordion>
184
+ </Box>
185
+ );
186
+ };
187
+
188
+ export default LanguageList;
client/src/components/LeaderboardSection/components/LeaderboardGrid.jsx CHANGED
@@ -1,5 +1,5 @@
1
  import React from "react";
2
- import { Grid, Box, Collapse } from "@mui/material";
3
  import { alpha } from "@mui/material/styles";
4
  import LeaderboardCard from "./LeaderboardCard";
5
 
@@ -25,31 +25,25 @@ const LeaderboardGrid = ({
25
  remainingLeaderboards,
26
  isExpanded,
27
  skeletonsNeeded,
 
28
  }) => {
 
 
 
29
  return (
30
- <>
31
- <Grid container spacing={3}>
32
- {displayedLeaderboards.map((leaderboard, index) => (
33
- <Grid item xs={12} sm={6} md={3} key={index}>
34
- <LeaderboardCard leaderboard={leaderboard} />
35
- </Grid>
36
- ))}
37
- {Array.from({ length: skeletonsNeeded }).map((_, index) => (
38
  <Grid item xs={12} sm={6} md={3} key={`skeleton-${index}`}>
39
  <LeaderboardSkeleton />
40
  </Grid>
41
  ))}
42
- </Grid>
43
- <Collapse in={isExpanded} timeout={300} unmountOnExit>
44
- <Grid container spacing={3} sx={{ mt: 0 }}>
45
- {remainingLeaderboards.map((leaderboard, index) => (
46
- <Grid item xs={12} sm={6} md={3} key={index + ITEMS_PER_PAGE}>
47
- <LeaderboardCard leaderboard={leaderboard} />
48
- </Grid>
49
- ))}
50
- </Grid>
51
- </Collapse>
52
- </>
53
  );
54
  };
55
 
 
1
  import React from "react";
2
+ import { Grid, Box } from "@mui/material";
3
  import { alpha } from "@mui/material/styles";
4
  import LeaderboardCard from "./LeaderboardCard";
5
 
 
25
  remainingLeaderboards,
26
  isExpanded,
27
  skeletonsNeeded,
28
+ id,
29
  }) => {
30
+ // On n'affiche que les leaderboards qui doivent être affichés selon l'état d'expansion
31
+ const leaderboardsToShow = displayedLeaderboards;
32
+
33
  return (
34
+ <Grid container spacing={3}>
35
+ {leaderboardsToShow.map((leaderboard, index) => (
36
+ <Grid item xs={12} sm={6} md={3} key={index}>
37
+ <LeaderboardCard leaderboard={leaderboard} />
38
+ </Grid>
39
+ ))}
40
+ {skeletonsNeeded > 0 &&
41
+ Array.from({ length: skeletonsNeeded }).map((_, index) => (
42
  <Grid item xs={12} sm={6} md={3} key={`skeleton-${index}`}>
43
  <LeaderboardSkeleton />
44
  </Grid>
45
  ))}
46
+ </Grid>
 
 
 
 
 
 
 
 
 
 
47
  );
48
  };
49
 
client/src/components/LeaderboardSection/hooks/useLanguageStats.js CHANGED
@@ -1,28 +1,94 @@
1
- import { useMemo } from "react";
2
 
3
  const LANGUAGE_FAMILIES = {
4
- "Romance Languages": [
5
- "french",
6
- "spanish",
7
- "italian",
8
- "portuguese",
9
- "romanian",
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
10
  ],
11
- "Germanic Languages": [
12
- "english",
13
- "german",
14
- "dutch",
15
- "swedish",
16
- "danish",
17
- "norwegian",
 
 
 
 
 
 
 
 
 
 
 
18
  ],
19
- "Asian Languages": ["chinese", "japanese", "korean", "vietnamese", "thai"],
20
  "Other Languages": [], // Will catch any language not in other families
21
  };
22
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
23
  const findLanguageFamily = (language) => {
24
- for (const [family, languages] of Object.entries(LANGUAGE_FAMILIES)) {
25
- if (languages.includes(language.toLowerCase())) {
 
 
 
 
 
 
 
26
  return { family, subFamily: language };
27
  }
28
  }
@@ -30,9 +96,10 @@ const findLanguageFamily = (language) => {
30
  };
31
 
32
  export const useLanguageStats = (leaderboards, filteredLeaderboards) => {
33
- const languages = useMemo(() => {
34
- if (!leaderboards) return null;
35
 
 
36
  const langMap = new Map();
37
  const langFamilyMap = new Map();
38
 
@@ -52,33 +119,38 @@ export const useLanguageStats = (leaderboards, filteredLeaderboards) => {
52
  return [lang, count, family];
53
  });
54
 
55
- return langArray
56
  .sort((a, b) => {
57
- if (a[2] !== b[2]) {
58
- if (a[2] === "Other Languages") return 1;
59
- if (b[2] === "Other Languages") return -1;
60
- return a[2].localeCompare(b[2]);
 
 
 
 
61
  }
 
62
  return b[1] - a[1];
63
  })
64
  .map(([lang]) => lang);
65
- }, [leaderboards]);
66
-
67
- const languageStats = useMemo(() => {
68
- if (!languages) return null;
69
- const stats = new Map();
70
 
71
- languages.forEach((lang) => {
72
- const count = filteredLeaderboards.filter((board) =>
 
 
73
  board.tags?.some(
74
  (tag) => tag.toLowerCase() === `language:${lang.toLowerCase()}`
75
  )
76
  ).length;
77
- stats.set(lang, count);
78
  });
 
79
 
80
- return stats;
81
- }, [languages, filteredLeaderboards]);
82
-
83
- return { languages, languageStats, LANGUAGE_FAMILIES, findLanguageFamily };
 
 
84
  };
 
1
+ import { useMemo, useRef } from "react";
2
 
3
  const LANGUAGE_FAMILIES = {
4
+ "Indo-European": {
5
+ Germanic: ["english", "german", "dutch", "danish", "swedish", "icelandic"],
6
+ Romance: [
7
+ "french",
8
+ "spanish",
9
+ "italian",
10
+ "portuguese",
11
+ "romanian",
12
+ "catalan",
13
+ "galician",
14
+ ],
15
+ Slavic: [
16
+ "russian",
17
+ "polish",
18
+ "czech",
19
+ "slovak",
20
+ "ukrainian",
21
+ "bulgarian",
22
+ "slovenian",
23
+ "serbian",
24
+ "croatian",
25
+ ],
26
+ Baltic: ["lithuanian", "latvian"],
27
+ "Indo-Iranian": [
28
+ "persian",
29
+ "hindi",
30
+ "bengali",
31
+ "gujarati",
32
+ "nepali",
33
+ "marathi",
34
+ ],
35
+ Greek: ["greek"],
36
+ Armenian: ["armenian"],
37
+ },
38
+ "Sino-Tibetan": ["chinese", "mandarin", "taiwanese"],
39
+ Afroasiatic: [
40
+ "arabic",
41
+ "hebrew",
42
+ "darija", // Variante de l'arabe
43
  ],
44
+ Austronesian: [
45
+ "indonesian",
46
+ "malay",
47
+ "filipino",
48
+ "singlish", // Créole basé sur l'anglais mais avec influence malaise
49
+ ],
50
+ "Niger-Congo": ["swahili", "yoruba"],
51
+ Dravidian: ["tamil", "telugu", "kannada", "malayalam"],
52
+ Austroasiatic: ["vietnamese"],
53
+ "Kra-Dai": ["thai"],
54
+ Japonic: [
55
+ "japanese",
56
+ "日本語", // Japonais en caractères japonais
57
+ ],
58
+ Koreanic: ["korean"],
59
+ Uralic: ["hungarian", "finnish", "estonian"],
60
+ "Language Isolate": [
61
+ "basque", // Langue isolée, pas de famille connue
62
  ],
 
63
  "Other Languages": [], // Will catch any language not in other families
64
  };
65
 
66
+ const FAMILY_ORDER = [
67
+ "Indo-European", // Plus grande famille en Europe et Asie du Sud
68
+ "Sino-Tibetan", // Deuxième plus grande famille en Asie
69
+ "Afroasiatic", // Principale famille en Afrique du Nord et Moyen-Orient
70
+ "Austronesian", // Principale famille en Asie du Sud-Est insulaire
71
+ "Niger-Congo", // Plus grande famille en Afrique subsaharienne
72
+ "Dravidian", // Principale famille en Inde du Sud
73
+ "Austroasiatic", // Importante en Asie du Sud-Est continentale
74
+ "Kra-Dai", // Famille du thaï
75
+ "Japonic", // Japonais
76
+ "Koreanic", // Coréen
77
+ "Uralic", // Famille finno-ougrienne
78
+ "Language Isolate", // Langues isolées
79
+ "Other Languages", // Autres langues non classifiées
80
+ ];
81
+
82
  const findLanguageFamily = (language) => {
83
+ for (const [family, content] of Object.entries(LANGUAGE_FAMILIES)) {
84
+ if (family === "Indo-European") {
85
+ // Cas spécial pour l'Indo-Européen qui a des sous-familles
86
+ for (const [subFamily, languages] of Object.entries(content)) {
87
+ if (languages.includes(language.toLowerCase())) {
88
+ return { family, subFamily };
89
+ }
90
+ }
91
+ } else if (content.includes(language.toLowerCase())) {
92
  return { family, subFamily: language };
93
  }
94
  }
 
96
  };
97
 
98
  export const useLanguageStats = (leaderboards, filteredLeaderboards) => {
99
+ const statsRef = useRef(null);
100
+ const languagesRef = useRef(null);
101
 
102
+ if (statsRef.current === null && leaderboards) {
103
  const langMap = new Map();
104
  const langFamilyMap = new Map();
105
 
 
119
  return [lang, count, family];
120
  });
121
 
122
+ languagesRef.current = langArray
123
  .sort((a, b) => {
124
+ const familyA = a[2];
125
+ const familyB = b[2];
126
+
127
+ const orderA = FAMILY_ORDER.indexOf(familyA);
128
+ const orderB = FAMILY_ORDER.indexOf(familyB);
129
+
130
+ if (orderA !== orderB) {
131
+ return orderA - orderB;
132
  }
133
+
134
  return b[1] - a[1];
135
  })
136
  .map(([lang]) => lang);
 
 
 
 
 
137
 
138
+ // Calculer les stats une seule fois
139
+ statsRef.current = new Map();
140
+ languagesRef.current.forEach((lang) => {
141
+ const count = leaderboards.filter((board) =>
142
  board.tags?.some(
143
  (tag) => tag.toLowerCase() === `language:${lang.toLowerCase()}`
144
  )
145
  ).length;
146
+ statsRef.current.set(lang, count);
147
  });
148
+ }
149
 
150
+ return {
151
+ languages: languagesRef.current,
152
+ languageStats: statsRef.current,
153
+ LANGUAGE_FAMILIES,
154
+ findLanguageFamily,
155
+ };
156
  };
client/src/components/LeaderboardSection/index.jsx CHANGED
@@ -2,7 +2,7 @@ import React from "react";
2
  import { Box } from "@mui/material";
3
  import { useLeaderboard } from "../../context/LeaderboardContext";
4
  import SectionHeader from "./components/SectionHeader";
5
- import LanguageAccordion from "./components/LanguageAccordion";
6
  import LeaderboardGrid from "./components/LeaderboardGrid";
7
  import EmptyState from "./components/EmptyState";
8
  import { useLanguageStats } from "./hooks/useLanguageStats";
@@ -21,7 +21,7 @@ const LeaderboardSection = ({
21
  selectedLanguage,
22
  setSelectedLanguage,
23
  searchQuery,
24
- selectedCategory,
25
  } = useLeaderboard();
26
 
27
  const isExpanded = expandedSections.has(id);
@@ -37,16 +37,28 @@ const LeaderboardSection = ({
37
  // On ne retourne null que si on n'a pas de leaderboards bruts
38
  if (!leaderboards) return null;
39
 
40
- // On affiche toujours les 3 premiers
41
- const displayedLeaderboards = approvedLeaderboards.slice(0, ITEMS_PER_PAGE);
42
- // Le reste sera dans le Collapse
43
- const remainingLeaderboards = approvedLeaderboards.slice(ITEMS_PER_PAGE);
 
44
 
45
- // Calculate how many skeletons we need
46
- const skeletonsNeeded = Math.max(0, 4 - approvedLeaderboards.length);
 
 
47
 
48
- // On affiche le bouton seulement si aucune catégorie n'est sélectionnée
49
- const showExpandButton = !selectedCategory;
 
 
 
 
 
 
 
 
 
50
 
51
  // Le bouton est actif seulement s'il y a plus de 4 leaderboards
52
  const isExpandButtonEnabled = approvedLeaderboards.length > ITEMS_PER_PAGE;
@@ -63,6 +75,13 @@ const LeaderboardSection = ({
63
  });
64
  };
65
 
 
 
 
 
 
 
 
66
  return (
67
  <Box sx={{ mb: 6 }}>
68
  <SectionHeader
@@ -74,8 +93,8 @@ const LeaderboardSection = ({
74
  isExpandButtonEnabled={isExpandButtonEnabled}
75
  />
76
 
77
- {languages && selectedCategory === "language" && (
78
- <LanguageAccordion
79
  languages={languages}
80
  languageStats={languageStats}
81
  selectedLanguage={selectedLanguage}
@@ -93,6 +112,7 @@ const LeaderboardSection = ({
93
  remainingLeaderboards={remainingLeaderboards}
94
  isExpanded={isExpanded}
95
  skeletonsNeeded={skeletonsNeeded}
 
96
  />
97
  )}
98
  </Box>
 
2
  import { Box } from "@mui/material";
3
  import { useLeaderboard } from "../../context/LeaderboardContext";
4
  import SectionHeader from "./components/SectionHeader";
5
+ import LanguageList from "./components/LanguageList";
6
  import LeaderboardGrid from "./components/LeaderboardGrid";
7
  import EmptyState from "./components/EmptyState";
8
  import { useLanguageStats } from "./hooks/useLanguageStats";
 
21
  selectedLanguage,
22
  setSelectedLanguage,
23
  searchQuery,
24
+ selectedCategories,
25
  } = useLeaderboard();
26
 
27
  const isExpanded = expandedSections.has(id);
 
37
  // On ne retourne null que si on n'a pas de leaderboards bruts
38
  if (!leaderboards) return null;
39
 
40
+ // Déterminer si on doit paginer ou montrer tous les leaderboards
41
+ const shouldShowAll =
42
+ (selectedCategories.size === 1 && selectedCategories.has(id)) || // Une seule catégorie sélectionnée ET c'est celle-ci
43
+ (selectedCategories.size > 1 && id === "combined") || // Plusieurs catégories ET c'est la section combinée
44
+ isExpanded; // Section dépliée (quelle que soit la sélection)
45
 
46
+ // Si on doit tout montrer, on ne divise pas la liste
47
+ const displayedLeaderboards = shouldShowAll
48
+ ? approvedLeaderboards
49
+ : approvedLeaderboards.slice(0, ITEMS_PER_PAGE);
50
 
51
+ const remainingLeaderboards = shouldShowAll
52
+ ? []
53
+ : approvedLeaderboards.slice(ITEMS_PER_PAGE);
54
+
55
+ // Calculate how many skeletons we need (seulement si on ne montre pas tout)
56
+ const skeletonsNeeded = shouldShowAll
57
+ ? 0
58
+ : Math.max(0, 4 - approvedLeaderboards.length);
59
+
60
+ // On affiche le bouton expand seulement quand on n'a pas de sélection
61
+ const showExpandButton = selectedCategories.size === 0;
62
 
63
  // Le bouton est actif seulement s'il y a plus de 4 leaderboards
64
  const isExpandButtonEnabled = approvedLeaderboards.length > ITEMS_PER_PAGE;
 
75
  });
76
  };
77
 
78
+ // Déterminer si on doit afficher la liste des langues
79
+ const showLanguageList =
80
+ languages &&
81
+ selectedCategories.size > 0 &&
82
+ (id === "language" ||
83
+ (selectedCategories.size > 1 && selectedCategories.has("language")));
84
+
85
  return (
86
  <Box sx={{ mb: 6 }}>
87
  <SectionHeader
 
93
  isExpandButtonEnabled={isExpandButtonEnabled}
94
  />
95
 
96
+ {showLanguageList && (
97
+ <LanguageList
98
  languages={languages}
99
  languageStats={languageStats}
100
  selectedLanguage={selectedLanguage}
 
112
  remainingLeaderboards={remainingLeaderboards}
113
  isExpanded={isExpanded}
114
  skeletonsNeeded={skeletonsNeeded}
115
+ id={id}
116
  />
117
  )}
118
  </Box>
client/src/components/Logo/Logo.jsx CHANGED
@@ -1,34 +1,32 @@
1
  import React from "react";
2
- 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
  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"
@@ -59,7 +57,7 @@ const Logo = () => {
59
  opacity: (theme) => (theme.palette.mode === "dark" ? 0.8 : 1),
60
  }}
61
  />
62
- </Link>
63
  );
64
  };
65
 
 
1
  import React from "react";
2
+ import { useNavigate } 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
+ import { useUrlState } from "../../hooks/useUrlState";
7
  import { useLeaderboard } from "../../context/LeaderboardContext";
8
 
9
  const Logo = () => {
10
+ const { resetParams } = useUrlState();
11
+ const { resetState } = useLeaderboard();
12
+ const navigate = useNavigate();
 
 
 
 
13
 
14
+ const handleClick = (e) => {
15
  e.preventDefault();
16
+ resetState();
17
+ resetParams();
18
+ navigate("/");
 
 
 
19
  };
20
 
21
  return (
22
+ <Box
23
+ component="a"
24
+ onClick={handleClick}
25
+ sx={{
26
+ textDecoration: "none",
27
+ position: "relative",
28
+ cursor: "pointer",
29
+ }}
30
  >
31
  <Box
32
  component="img"
 
57
  opacity: (theme) => (theme.palette.mode === "dark" ? 0.8 : 1),
58
  }}
59
  />
60
+ </Box>
61
  );
62
  };
63
 
client/src/components/common/FilterTag.jsx ADDED
@@ -0,0 +1,83 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React from "react";
2
+ import { Button, Box } from "@mui/material";
3
+ import { alpha } from "@mui/material/styles";
4
+
5
+ const TAG_COLOR = {
6
+ main: "#F44336",
7
+ light: "#FFEBEE",
8
+ };
9
+
10
+ const FilterTag = ({ label, count, isActive, onClick }) => {
11
+ return (
12
+ <Button
13
+ onClick={onClick}
14
+ variant={isActive ? "contained" : "outlined"}
15
+ size="small"
16
+ sx={{
17
+ textTransform: "none",
18
+ cursor: "pointer",
19
+ mb: 0.75,
20
+ backgroundColor: (theme) => {
21
+ if (isActive) {
22
+ return TAG_COLOR.main;
23
+ }
24
+ return theme.palette.mode === "dark"
25
+ ? "background.paper"
26
+ : TAG_COLOR.light;
27
+ },
28
+ color: (theme) => {
29
+ if (isActive) {
30
+ return "white";
31
+ }
32
+ return TAG_COLOR.main;
33
+ },
34
+ borderColor: TAG_COLOR.main,
35
+ "&:hover": {
36
+ backgroundColor: (theme) => {
37
+ if (isActive) {
38
+ return TAG_COLOR.main;
39
+ }
40
+ return TAG_COLOR.light;
41
+ },
42
+ opacity: 0.8,
43
+ },
44
+ "& .MuiTouchRipple-root": {
45
+ transition: "none",
46
+ },
47
+ transition: "none",
48
+ }}
49
+ >
50
+ {label}
51
+ {count !== undefined && (
52
+ <Box
53
+ component="span"
54
+ sx={{
55
+ display: "inline-flex",
56
+ alignItems: "center",
57
+ gap: 0.75,
58
+ color: isActive ? "white" : "inherit",
59
+ ml: 0.75,
60
+ }}
61
+ >
62
+ <Box
63
+ component="span"
64
+ sx={(theme) => ({
65
+ width: "4px",
66
+ height: "4px",
67
+ borderRadius: "100%",
68
+ backgroundColor: isActive
69
+ ? "rgba(255, 255, 255, 0.5)"
70
+ : alpha(
71
+ TAG_COLOR.main,
72
+ theme.palette.mode === "dark" ? 0.4 : 0.3
73
+ ),
74
+ })}
75
+ />
76
+ {count}
77
+ </Box>
78
+ )}
79
+ </Button>
80
+ );
81
+ };
82
+
83
+ export default FilterTag;
client/src/context/LeaderboardContext.jsx CHANGED
@@ -6,33 +6,11 @@ import React, {
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
  // Constantes pour les tags de catégorisation
37
  const CATEGORIZATION_TAGS = [
38
  "modality:agent",
@@ -76,62 +54,70 @@ const isUncategorized = (board) => {
76
  };
77
 
78
  export const LeaderboardProvider = ({ children }) => {
79
- // Lire les paramètres initiaux depuis l'URL
80
- const initialParams = getURLParams();
81
 
82
  const [leaderboards, setLeaderboards] = useState([]);
83
- const [searchQuery, setSearchQuery] = useState(initialParams.search || "");
84
- const [arenaOnly, setArenaOnly] = useState(initialParams.arena);
85
- const [selectedCategory, setSelectedCategory] = useState(
86
- initialParams.category
87
  );
88
  const [selectedLanguage, setSelectedLanguage] = useState(
89
- initialParams.language
90
  );
91
  const [expandedSections, setExpandedSections] = useState(
92
- new Set(initialParams.category ? [initialParams.category] : [])
 
 
 
93
  );
94
 
95
  // Mettre à jour l'URL quand les filtres changent
96
  useEffect(() => {
97
- updateURL({
98
- category: selectedCategory,
99
  search: searchQuery,
100
  arena: arenaOnly ? "true" : null,
101
  language: selectedLanguage,
 
102
  });
103
- }, [selectedCategory, searchQuery, arenaOnly, selectedLanguage]);
104
-
105
- // Écouter les changements d'URL (navigation back/forward)
106
- useEffect(() => {
107
- const handleURLChange = () => {
108
- const params = getURLParams();
109
- setSearchQuery(params.search || "");
110
- setArenaOnly(params.arena);
111
- setSelectedCategory(params.category);
112
- setSelectedLanguage(params.language);
113
- setExpandedSections(new Set(params.category ? [params.category] : []));
114
- };
115
-
116
- window.addEventListener("popstate", handleURLChange);
117
- return () => window.removeEventListener("popstate", handleURLChange);
118
- }, []);
119
 
120
- // Wrapper pour setSelectedCategory qui gère aussi l'expansion des sections
121
  const handleCategorySelection = useCallback((categoryId) => {
122
- // On réinitialise toujours la langue sélectionnée
123
- setSelectedLanguage(null);
124
-
125
- setSelectedCategory((prev) => {
126
- if (prev === categoryId) {
127
- // Si on désélectionne, on replie toutes les sections
128
- setExpandedSections(new Set());
129
- return null;
 
 
 
 
 
 
 
 
 
 
130
  }
131
- // Si on sélectionne une nouvelle catégorie, on la déploie
132
- setExpandedSections(new Set([categoryId]));
133
- return categoryId;
134
  });
 
 
 
 
 
135
  }, []);
136
 
137
  // Wrapper pour la sélection de langue
@@ -182,7 +168,7 @@ export const LeaderboardProvider = ({ children }) => {
182
  [searchQuery, arenaOnly]
183
  );
184
 
185
- // Filter leaderboards based on all criteria including category and language selection
186
  const filterLeaderboards = useCallback(
187
  (boards) => {
188
  if (!boards) return [];
@@ -199,68 +185,71 @@ export const LeaderboardProvider = ({ children }) => {
199
  );
200
  }
201
 
202
- // Filter by selected category if any
203
- if (selectedCategory) {
204
  filtered = filtered.filter((board) => {
205
  const { tags = [] } = board;
206
- switch (selectedCategory) {
207
- case "agentic":
208
- return tags.includes("modality:agent");
209
- case "text":
210
- return tags.includes("modality:text");
211
- case "image":
212
- return tags.includes("modality:image");
213
- case "video":
214
- return tags.includes("modality:video");
215
- case "code":
216
- return tags.includes("eval:code");
217
- case "math":
218
- return tags.includes("eval:math");
219
- case "reasoning":
220
- return tags.includes("eval:reasoning");
221
- case "hallucination":
222
- return tags.includes("eval:hallucination");
223
- case "rag":
224
- return tags.includes("eval:rag");
225
- case "language":
226
- return tags.some((tag) => tag.startsWith("language:"));
227
- case "vision":
228
- return tags.some(
229
- (tag) => tag === "modality:video" || tag === "modality:image"
230
- );
231
- case "threeD":
232
- return tags.includes("modality:3d");
233
- case "audio":
234
- return tags.includes("modality:audio");
235
- case "financial":
236
- return tags.includes("domain:financial");
237
- case "medical":
238
- return tags.includes("domain:medical");
239
- case "legal":
240
- return tags.includes("domain:legal");
241
- case "biology":
242
- return tags.includes("domain:biology");
243
- case "commercial":
244
- return tags.includes("domain:commercial");
245
- case "translation":
246
- return tags.includes("domain:translation");
247
- case "chemistry":
248
- return tags.includes("domain:chemistry");
249
- case "safety":
250
- return tags.includes("eval:safety");
251
- case "performance":
252
- return tags.includes("eval:performance");
253
- case "uncategorized":
254
- return isUncategorized(board);
255
- default:
256
- return true;
257
- }
 
 
 
258
  });
259
  }
260
 
261
  return filtered;
262
  },
263
- [filterLeaderboardsForCount, selectedCategory, selectedLanguage]
264
  );
265
 
266
  // Fonction pour obtenir les leaderboards bruts d'une section
@@ -584,8 +573,8 @@ export const LeaderboardProvider = ({ children }) => {
584
 
585
  // Get filtered count
586
  const filteredCount = useMemo(() => {
587
- return filterLeaderboardsForCount(leaderboards).length;
588
- }, [filterLeaderboardsForCount, leaderboards]);
589
 
590
  // Function to get highlighted parts of text
591
  const getHighlightedText = useCallback((text, searchTerm) => {
@@ -619,9 +608,27 @@ export const LeaderboardProvider = ({ children }) => {
619
  };
620
  }, []);
621
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
622
  const value = {
623
  leaderboards,
624
- setLeaderboards,
625
  searchQuery,
626
  setSearchQuery,
627
  arenaOnly,
@@ -633,14 +640,17 @@ export const LeaderboardProvider = ({ children }) => {
633
  sections,
634
  allSections,
635
  getHighlightedText,
636
- selectedCategory,
637
- setSelectedCategory: handleCategorySelection,
638
  selectedLanguage,
639
  setSelectedLanguage: handleLanguageSelection,
640
  expandedSections,
641
  setExpandedSections,
642
  getLanguageStats,
643
  getSectionLeaderboards,
 
 
 
644
  };
645
 
646
  return (
 
6
  useMemo,
7
  useEffect,
8
  } from "react";
9
+ import { normalizeTags } from "../utils/tagFilters";
10
+ import { useUrlState } from "../hooks/useUrlState";
11
 
12
  const LeaderboardContext = createContext();
13
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
14
  // Constantes pour les tags de catégorisation
15
  const CATEGORIZATION_TAGS = [
16
  "modality:agent",
 
54
  };
55
 
56
  export const LeaderboardProvider = ({ children }) => {
57
+ const { params, updateParams } = useUrlState();
 
58
 
59
  const [leaderboards, setLeaderboards] = useState([]);
60
+ const [searchQuery, setSearchQuery] = useState(params.search || "");
61
+ const [arenaOnly, setArenaOnly] = useState(params.arena === "true");
62
+ const [selectedCategories, setSelectedCategories] = useState(
63
+ new Set(params.categories || [])
64
  );
65
  const [selectedLanguage, setSelectedLanguage] = useState(
66
+ params.language || null
67
  );
68
  const [expandedSections, setExpandedSections] = useState(
69
+ new Set(params.categories || [])
70
+ );
71
+ const [isLanguageExpanded, setIsLanguageExpanded] = useState(
72
+ params.languageExpanded === "true"
73
  );
74
 
75
  // Mettre à jour l'URL quand les filtres changent
76
  useEffect(() => {
77
+ updateParams({
78
+ categories: Array.from(selectedCategories),
79
  search: searchQuery,
80
  arena: arenaOnly ? "true" : null,
81
  language: selectedLanguage,
82
+ languageExpanded: isLanguageExpanded ? "true" : null,
83
  });
84
+ }, [
85
+ selectedCategories,
86
+ searchQuery,
87
+ arenaOnly,
88
+ selectedLanguage,
89
+ isLanguageExpanded,
90
+ updateParams,
91
+ ]);
 
 
 
 
 
 
 
 
92
 
93
+ // Wrapper pour setSelectedCategories qui gère aussi l'expansion des sections
94
  const handleCategorySelection = useCallback((categoryId) => {
95
+ setSelectedCategories((prev) => {
96
+ const newCategories = new Set(prev);
97
+ if (newCategories.has(categoryId)) {
98
+ newCategories.delete(categoryId);
99
+ // Si on désélectionne, on replie la section
100
+ setExpandedSections((prev) => {
101
+ const newExpanded = new Set(prev);
102
+ newExpanded.delete(categoryId);
103
+ return newExpanded;
104
+ });
105
+ } else {
106
+ newCategories.add(categoryId);
107
+ // Si on sélectionne, on déploie la section
108
+ setExpandedSections((prev) => {
109
+ const newExpanded = new Set(prev);
110
+ newExpanded.add(categoryId);
111
+ return newExpanded;
112
+ });
113
  }
114
+ return newCategories;
 
 
115
  });
116
+
117
+ // On réinitialise la langue sélectionnée seulement si on désélectionne "language"
118
+ if (categoryId === "language") {
119
+ setSelectedLanguage(null);
120
+ }
121
  }, []);
122
 
123
  // Wrapper pour la sélection de langue
 
168
  [searchQuery, arenaOnly]
169
  );
170
 
171
+ // Filter leaderboards based on all criteria including categories and language selection
172
  const filterLeaderboards = useCallback(
173
  (boards) => {
174
  if (!boards) return [];
 
185
  );
186
  }
187
 
188
+ // Filter by selected categories if any
189
+ if (selectedCategories.size > 0) {
190
  filtered = filtered.filter((board) => {
191
  const { tags = [] } = board;
192
+ // Un leaderboard est inclus s'il correspond à TOUTES les catégories sélectionnées
193
+ return Array.from(selectedCategories).every((category) => {
194
+ switch (category) {
195
+ case "agentic":
196
+ return tags.includes("modality:agent");
197
+ case "text":
198
+ return tags.includes("modality:text");
199
+ case "image":
200
+ return tags.includes("modality:image");
201
+ case "video":
202
+ return tags.includes("modality:video");
203
+ case "code":
204
+ return tags.includes("eval:code");
205
+ case "math":
206
+ return tags.includes("eval:math");
207
+ case "reasoning":
208
+ return tags.includes("eval:reasoning");
209
+ case "hallucination":
210
+ return tags.includes("eval:hallucination");
211
+ case "rag":
212
+ return tags.includes("eval:rag");
213
+ case "language":
214
+ return tags.some((tag) => tag.startsWith("language:"));
215
+ case "vision":
216
+ return tags.some(
217
+ (tag) => tag === "modality:video" || tag === "modality:image"
218
+ );
219
+ case "threeD":
220
+ return tags.includes("modality:3d");
221
+ case "audio":
222
+ return tags.includes("modality:audio");
223
+ case "financial":
224
+ return tags.includes("domain:financial");
225
+ case "medical":
226
+ return tags.includes("domain:medical");
227
+ case "legal":
228
+ return tags.includes("domain:legal");
229
+ case "biology":
230
+ return tags.includes("domain:biology");
231
+ case "commercial":
232
+ return tags.includes("domain:commercial");
233
+ case "translation":
234
+ return tags.includes("domain:translation");
235
+ case "chemistry":
236
+ return tags.includes("domain:chemistry");
237
+ case "safety":
238
+ return tags.includes("eval:safety");
239
+ case "performance":
240
+ return tags.includes("eval:performance");
241
+ case "uncategorized":
242
+ return isUncategorized(board);
243
+ default:
244
+ return false;
245
+ }
246
+ });
247
  });
248
  }
249
 
250
  return filtered;
251
  },
252
+ [searchQuery, arenaOnly, selectedCategories, selectedLanguage]
253
  );
254
 
255
  // Fonction pour obtenir les leaderboards bruts d'une section
 
573
 
574
  // Get filtered count
575
  const filteredCount = useMemo(() => {
576
+ return filterLeaderboards(leaderboards).length;
577
+ }, [filterLeaderboards, leaderboards]);
578
 
579
  // Function to get highlighted parts of text
580
  const getHighlightedText = useCallback((text, searchTerm) => {
 
608
  };
609
  }, []);
610
 
611
+ // Wrapper pour setLeaderboards qui normalise les tags
612
+ const setNormalizedLeaderboards = useCallback((boards) => {
613
+ const normalizedBoards = boards.map((board) => ({
614
+ ...board,
615
+ tags: normalizeTags(board.tags),
616
+ }));
617
+ setLeaderboards(normalizedBoards);
618
+ }, []);
619
+
620
+ const resetState = useCallback(() => {
621
+ setSearchQuery("");
622
+ setArenaOnly(false);
623
+ setSelectedCategories(new Set());
624
+ setSelectedLanguage(null);
625
+ setExpandedSections(new Set());
626
+ setIsLanguageExpanded(false);
627
+ }, []);
628
+
629
  const value = {
630
  leaderboards,
631
+ setLeaderboards: setNormalizedLeaderboards,
632
  searchQuery,
633
  setSearchQuery,
634
  arenaOnly,
 
640
  sections,
641
  allSections,
642
  getHighlightedText,
643
+ selectedCategories,
644
+ setSelectedCategories: handleCategorySelection,
645
  selectedLanguage,
646
  setSelectedLanguage: handleLanguageSelection,
647
  expandedSections,
648
  setExpandedSections,
649
  getLanguageStats,
650
  getSectionLeaderboards,
651
+ resetState,
652
+ isLanguageExpanded,
653
+ setIsLanguageExpanded,
654
  };
655
 
656
  return (
client/src/hooks/useUrlState.js ADDED
@@ -0,0 +1,75 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { useSearchParams, useLocation } from "react-router-dom";
2
+ import { useCallback, useEffect } from "react";
3
+
4
+ /**
5
+ * Hook personnalisé pour gérer l'état de l'URL
6
+ * @returns {Object} Méthodes et données pour gérer l'état de l'URL
7
+ */
8
+ export const useUrlState = () => {
9
+ const [searchParams, setSearchParams] = useSearchParams();
10
+ const location = useLocation();
11
+
12
+ // Convertit les searchParams en objet
13
+ const getParams = useCallback(() => {
14
+ const params = {};
15
+ for (const [key, value] of searchParams.entries()) {
16
+ if (key === "categories") {
17
+ params[key] = value.split(",").filter(Boolean);
18
+ } else if (key === "arena") {
19
+ params[key] = value === "true";
20
+ } else {
21
+ params[key] = value;
22
+ }
23
+ }
24
+ return params;
25
+ }, [searchParams]);
26
+
27
+ // Met à jour les paramètres d'URL
28
+ const updateParams = useCallback(
29
+ (newParams) => {
30
+ const updatedParams = new URLSearchParams(searchParams);
31
+
32
+ Object.entries(newParams).forEach(([key, value]) => {
33
+ if (value && (Array.isArray(value) ? value.length > 0 : true)) {
34
+ if (Array.isArray(value)) {
35
+ updatedParams.set(key, value.join(","));
36
+ } else {
37
+ updatedParams.set(key, value.toString());
38
+ }
39
+ } else {
40
+ updatedParams.delete(key);
41
+ }
42
+ });
43
+
44
+ setSearchParams(updatedParams);
45
+ },
46
+ [searchParams, setSearchParams]
47
+ );
48
+
49
+ // Réinitialise tous les paramètres d'URL
50
+ const resetParams = useCallback(() => {
51
+ setSearchParams(new URLSearchParams());
52
+ }, [setSearchParams]);
53
+
54
+ // Gestion de l'intégration avec Hugging Face
55
+ useEffect(() => {
56
+ const isHFSpace = window.location !== window.parent.location;
57
+ if (!isHFSpace) return;
58
+
59
+ window.parent.postMessage(
60
+ {
61
+ queryString: location.search,
62
+ hash: location.hash,
63
+ },
64
+ "https://huggingface.co"
65
+ );
66
+ }, [location]);
67
+
68
+ return {
69
+ params: getParams(),
70
+ updateParams,
71
+ resetParams,
72
+ searchParams,
73
+ location,
74
+ };
75
+ };
client/src/pages/LeaderboardPage/LeaderboardPage.jsx CHANGED
@@ -20,7 +20,7 @@ const LeaderboardPageContent = () => {
20
  allSections,
21
  searchQuery,
22
  arenaOnly,
23
- selectedCategory,
24
  } = useLeaderboard();
25
 
26
  useEffect(() => {
@@ -101,7 +101,7 @@ const LeaderboardPageContent = () => {
101
  <LeaderboardFilters allSections={allSections} />
102
 
103
  {/* Message global "No results" seulement si on n'a pas de catégorie sélectionnée */}
104
- {!hasLeaderboards && isFiltering && !selectedCategory && (
105
  <Box
106
  sx={{
107
  display: "flex",
@@ -146,23 +146,65 @@ const LeaderboardPageContent = () => {
146
  )}
147
 
148
  {/* On affiche toujours la section sélectionnée, sinon on affiche les sections avec des résultats */}
149
- {selectedCategory
150
- ? sections
151
- .filter(({ id }) => id === selectedCategory)
152
- .map(({ id, title, data }) => {
153
- const filteredLeaderboards = filterLeaderboards(data);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
154
  return (
155
- <Box key={id} id={id}>
156
  <LeaderboardSection
157
- id={id}
158
- title={title}
159
- leaderboards={data}
160
  filteredLeaderboards={filteredLeaderboards}
161
  />
162
  </Box>
163
  );
164
- })
165
- : (hasLeaderboards || !isFiltering) &&
 
166
  sections.map(({ id, title, data }) => {
167
  const filteredLeaderboards = filterLeaderboards(data);
168
  if (filteredLeaderboards.length === 0) return null;
 
20
  allSections,
21
  searchQuery,
22
  arenaOnly,
23
+ selectedCategories,
24
  } = useLeaderboard();
25
 
26
  useEffect(() => {
 
101
  <LeaderboardFilters allSections={allSections} />
102
 
103
  {/* Message global "No results" seulement si on n'a pas de catégorie sélectionnée */}
104
+ {!hasLeaderboards && isFiltering && selectedCategories.size === 0 && (
105
  <Box
106
  sx={{
107
  display: "flex",
 
146
  )}
147
 
148
  {/* On affiche toujours la section sélectionnée, sinon on affiche les sections avec des résultats */}
149
+ {selectedCategories.size > 0
150
+ ? // Si des catégories sont sélectionnées
151
+ selectedCategories.size === 1
152
+ ? // Si une seule catégorie est sélectionnée, on affiche sa section
153
+ sections
154
+ .filter(({ id }) => selectedCategories.has(id))
155
+ .map(({ id, title, data }) => {
156
+ const filteredLeaderboards = filterLeaderboards(data);
157
+ return (
158
+ <Box key={id} id={id}>
159
+ <LeaderboardSection
160
+ id={id}
161
+ title={title}
162
+ leaderboards={data}
163
+ filteredLeaderboards={filteredLeaderboards}
164
+ />
165
+ </Box>
166
+ );
167
+ })
168
+ : // Si plusieurs catégories sont sélectionnées, on les agrège
169
+ (() => {
170
+ // Agréger les données de toutes les sections sélectionnées
171
+ const selectedSections = sections.filter(({ id }) =>
172
+ selectedCategories.has(id)
173
+ );
174
+
175
+ // Créer un titre combiné
176
+ const combinedTitle = selectedSections
177
+ .map(({ title }) => title)
178
+ .join(" + ");
179
+
180
+ // Agréger les leaderboards
181
+ const combinedData = selectedSections.reduce(
182
+ (acc, { data }) => [...acc, ...data],
183
+ []
184
+ );
185
+
186
+ // Filtrer les doublons par ID
187
+ const uniqueData = Array.from(
188
+ new Map(
189
+ combinedData.map((item) => [item.id, item])
190
+ ).values()
191
+ );
192
+
193
+ const filteredLeaderboards = filterLeaderboards(uniqueData);
194
+
195
  return (
196
+ <Box key="combined">
197
  <LeaderboardSection
198
+ id="combined"
199
+ title={combinedTitle}
200
+ leaderboards={uniqueData}
201
  filteredLeaderboards={filteredLeaderboards}
202
  />
203
  </Box>
204
  );
205
+ })()
206
+ : // Si aucune catégorie n'est sélectionnée, on affiche toutes les sections avec des résultats
207
+ (hasLeaderboards || !isFiltering) &&
208
  sections.map(({ id, title, data }) => {
209
  const filteredLeaderboards = filterLeaderboards(data);
210
  if (filteredLeaderboards.length === 0) return null;
client/src/utils/tagFilters.js ADDED
@@ -0,0 +1,23 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * Map of tags that should be normalized
3
+ * Key: original tag
4
+ * Value: normalized tag
5
+ */
6
+ const TAG_NORMALIZATIONS = {
7
+ "language:日本語": "language:japanese",
8
+ };
9
+
10
+ /**
11
+ * Normalize tags according to predefined rules
12
+ * @param {string[]} tags - Array of tags to normalize
13
+ * @returns {string[]} - Array of normalized tags without duplicates
14
+ */
15
+ export const normalizeTags = (tags) => {
16
+ if (!tags || !Array.isArray(tags)) return [];
17
+
18
+ // First, normalize all tags
19
+ const normalizedTags = tags.map((tag) => TAG_NORMALIZATIONS[tag] || tag);
20
+
21
+ // Then remove duplicates using Set
22
+ return [...new Set(normalizedTags)];
23
+ };