tfrere commited on
Commit
f145689
·
1 Parent(s): edf1655

simplify filters

Browse files
client/src/components/LeaderboardFilters/LeaderboardFilters.jsx CHANGED
@@ -2,35 +2,13 @@ import React, { useState, useMemo } from "react";
2
  import { Box, Stack, useMediaQuery } from "@mui/material";
3
  import { useLeaderboard } from "../../context/LeaderboardContext";
4
  import { useDebounce } from "../../hooks/useDebounce";
5
- import { alpha, lighten, darken } from "@mui/material/styles";
6
  import SearchBar from "./SearchBar";
7
  import FilterTag from "../common/FilterTag";
8
-
9
- // Constantes pour les tags de catégorisation
10
- const CATEGORIZATION_TAGS = [
11
- "modality:agent",
12
- "modality:artefacts",
13
- "modality:text",
14
- "eval:code",
15
- "eval:math",
16
- "eval:reasoning",
17
- "eval:hallucination",
18
- "modality:video",
19
- "modality:image",
20
- "modality:3d",
21
- "modality:audio",
22
- "domain:financial",
23
- "domain:medical",
24
- "domain:legal",
25
- "domain:biology",
26
- "domain:translation",
27
- "domain:chemistry",
28
- "domain:physics",
29
- "domain:commercial",
30
- "eval:safety",
31
- "eval:performance",
32
- "eval:rag",
33
- ];
34
 
35
  // Helper function to get the category group of a section
36
  const getSectionGroup = (id) => {
@@ -116,175 +94,20 @@ const LeaderboardFilters = ({ allSections = [] }) => {
116
  );
117
 
118
  // Apply current filters except the category being counted
119
- let baseFiltered = allLeaderboards;
120
-
121
- // Apply arena filter if active
122
- if (arenaOnly) {
123
- baseFiltered = baseFiltered.filter((board) =>
124
- board.tags?.includes("judge:humans")
125
- );
126
- }
127
-
128
- // Apply search filter if active
129
- if (searchQuery) {
130
- const query = searchQuery.toLowerCase();
131
- const tagMatch = query.match(/^(\w+):(\w+)$/);
132
-
133
- if (tagMatch) {
134
- const [_, category, value] = tagMatch;
135
- const searchTag = `${category}:${value}`.toLowerCase();
136
- baseFiltered = baseFiltered.filter((board) => {
137
- const allTags = [...(board.tags || []), ...(board.editor_tags || [])];
138
- return allTags.some((tag) => tag.toLowerCase() === searchTag);
139
- });
140
- } else {
141
- baseFiltered = baseFiltered.filter((board) =>
142
- board.card_data?.title?.toLowerCase().includes(query)
143
- );
144
- }
145
- }
146
-
147
- // Apply language filters if active
148
- if (selectedLanguage.size > 0) {
149
- baseFiltered = baseFiltered.filter((board) =>
150
- Array.from(selectedLanguage).some((lang) =>
151
- board.tags?.some(
152
- (tag) => tag.toLowerCase() === `language:${lang.toLowerCase()}`
153
- )
154
- )
155
- );
156
- }
157
-
158
- // Apply category filters if active, but exclude the category being counted
159
- const applyCategoryFilters = (board, excludedCategory) => {
160
- if (selectedCategories.size === 0) return true;
161
-
162
- const tags = board.tags || [];
163
- return Array.from(selectedCategories)
164
- .filter((category) => category !== excludedCategory)
165
- .every((category) => {
166
- switch (category) {
167
- case "agentic":
168
- return tags.includes("modality:agent");
169
- case "text":
170
- return tags.includes("modality:text");
171
- case "image":
172
- return tags.includes("modality:image");
173
- case "video":
174
- return tags.includes("modality:video");
175
- case "code":
176
- return tags.includes("eval:code");
177
- case "math":
178
- return tags.includes("eval:math");
179
- case "reasoning":
180
- return tags.includes("eval:reasoning");
181
- case "hallucination":
182
- return tags.includes("eval:hallucination");
183
- case "rag":
184
- return tags.includes("eval:rag");
185
- case "language":
186
- return tags.some((tag) => tag.startsWith("language:"));
187
- case "vision":
188
- return tags.some(
189
- (tag) => tag === "modality:video" || tag === "modality:image"
190
- );
191
- case "threeD":
192
- return tags.includes("modality:3d");
193
- case "audio":
194
- return tags.includes("modality:audio");
195
- case "financial":
196
- return tags.includes("domain:financial");
197
- case "medical":
198
- return tags.includes("domain:medical");
199
- case "legal":
200
- return tags.includes("domain:legal");
201
- case "biology":
202
- return tags.includes("domain:biology");
203
- case "commercial":
204
- return tags.includes("domain:commercial");
205
- case "translation":
206
- return tags.includes("domain:translation");
207
- case "chemistry":
208
- return tags.includes("domain:chemistry");
209
- case "safety":
210
- return tags.includes("eval:safety");
211
- case "performance":
212
- return tags.includes("eval:performance");
213
- case "uncategorized":
214
- return !tags.some(
215
- (tag) =>
216
- CATEGORIZATION_TAGS.includes(tag) ||
217
- tag.startsWith("language:")
218
- );
219
- default:
220
- return false;
221
- }
222
- });
223
- };
224
-
225
- // Now calculate counts for each section based on the filtered boards
226
  allSections.forEach(({ id, title }) => {
227
- let sectionBoards = baseFiltered
228
- .filter((board) => applyCategoryFilters(board, id))
229
- .filter((board) => {
230
- const tags = board.tags || [];
231
- switch (id) {
232
- case "agentic":
233
- return tags.includes("modality:agent");
234
- case "text":
235
- return tags.includes("modality:text");
236
- case "image":
237
- return tags.includes("modality:image");
238
- case "video":
239
- return tags.includes("modality:video");
240
- case "code":
241
- return tags.includes("eval:code");
242
- case "math":
243
- return tags.includes("eval:math");
244
- case "reasoning":
245
- return tags.includes("eval:reasoning");
246
- case "hallucination":
247
- return tags.includes("eval:hallucination");
248
- case "rag":
249
- return tags.includes("eval:rag");
250
- case "language":
251
- return tags.some((tag) => tag.startsWith("language:"));
252
- case "vision":
253
- return tags.some(
254
- (tag) => tag === "modality:video" || tag === "modality:image"
255
- );
256
- case "threeD":
257
- return tags.includes("modality:3d");
258
- case "audio":
259
- return tags.includes("modality:audio");
260
- case "financial":
261
- return tags.includes("domain:financial");
262
- case "medical":
263
- return tags.includes("domain:medical");
264
- case "legal":
265
- return tags.includes("domain:legal");
266
- case "biology":
267
- return tags.includes("domain:biology");
268
- case "commercial":
269
- return tags.includes("domain:commercial");
270
- case "translation":
271
- return tags.includes("domain:translation");
272
- case "chemistry":
273
- return tags.includes("domain:chemistry");
274
- case "safety":
275
- return tags.includes("eval:safety");
276
- case "performance":
277
- return tags.includes("eval:performance");
278
- case "uncategorized":
279
- return !tags.some(
280
- (tag) =>
281
- CATEGORIZATION_TAGS.includes(tag) ||
282
- tag.startsWith("language:")
283
- );
284
- default:
285
- return false;
286
- }
287
- });
288
 
289
  // Only count approved leaderboards
290
  sectionBoards = sectionBoards.filter(
@@ -327,65 +150,7 @@ const LeaderboardFilters = ({ allSections = [] }) => {
327
  if (selectedCategories.size > 0) {
328
  boardsToCount = boardsToCount.filter((board) => {
329
  if (board.approval_status !== "approved") return false;
330
- const tags = board.tags || [];
331
- return Array.from(selectedCategories).every((category) => {
332
- switch (category) {
333
- case "agentic":
334
- return tags.includes("modality:agent");
335
- case "text":
336
- return tags.includes("modality:text");
337
- case "image":
338
- return tags.includes("modality:image");
339
- case "video":
340
- return tags.includes("modality:video");
341
- case "code":
342
- return tags.includes("eval:code");
343
- case "math":
344
- return tags.includes("eval:math");
345
- case "reasoning":
346
- return tags.includes("eval:reasoning");
347
- case "hallucination":
348
- return tags.includes("eval:hallucination");
349
- case "rag":
350
- return tags.includes("eval:rag");
351
- case "language":
352
- return tags.some((tag) => tag.startsWith("language:"));
353
- case "vision":
354
- return tags.some(
355
- (tag) => tag === "modality:video" || tag === "modality:image"
356
- );
357
- case "threeD":
358
- return tags.includes("modality:3d");
359
- case "audio":
360
- return tags.includes("modality:audio");
361
- case "financial":
362
- return tags.includes("domain:financial");
363
- case "medical":
364
- return tags.includes("domain:medical");
365
- case "legal":
366
- return tags.includes("domain:legal");
367
- case "biology":
368
- return tags.includes("domain:biology");
369
- case "commercial":
370
- return tags.includes("domain:commercial");
371
- case "translation":
372
- return tags.includes("domain:translation");
373
- case "chemistry":
374
- return tags.includes("domain:chemistry");
375
- case "safety":
376
- return tags.includes("eval:safety");
377
- case "performance":
378
- return tags.includes("eval:performance");
379
- case "uncategorized":
380
- return !tags.some(
381
- (tag) =>
382
- CATEGORIZATION_TAGS.includes(tag) ||
383
- tag.startsWith("language:")
384
- );
385
- default:
386
- return false;
387
- }
388
- });
389
  });
390
  }
391
 
@@ -396,15 +161,11 @@ const LeaderboardFilters = ({ allSections = [] }) => {
396
  setTotalArenaCount(arenaCount);
397
  }, [selectedCategories, allSections]);
398
 
399
- // Calculer le nombre total en fonction des catégories sélectionnées
400
  const totalCount = useMemo(() => {
401
  if (!allSections) return 0;
402
 
403
- if (arenaOnly && selectedCategories.size === 0) {
404
- return totalArenaCount;
405
- }
406
-
407
- // Récupérer tous les leaderboards uniques de toutes les sections
408
  const allLeaderboards = Array.from(
409
  new Set(
410
  allSections.reduce((acc, section) => {
@@ -413,94 +174,17 @@ const LeaderboardFilters = ({ allSections = [] }) => {
413
  )
414
  );
415
 
416
- // Si des catégories sont sélectionnées, filtrer les leaderboards qui correspondent à TOUTES les catégories
417
- let filteredBoards = allLeaderboards;
418
- if (selectedCategories.size > 0) {
419
- filteredBoards = filteredBoards.filter((board) => {
420
- if (board.approval_status !== "approved") return false;
421
- const tags = board.tags || [];
422
- return Array.from(selectedCategories).every((category) => {
423
- switch (category) {
424
- case "agentic":
425
- return tags.includes("modality:agent");
426
- case "text":
427
- return tags.includes("modality:text");
428
- case "image":
429
- return tags.includes("modality:image");
430
- case "video":
431
- return tags.includes("modality:video");
432
- case "code":
433
- return tags.includes("eval:code");
434
- case "math":
435
- return tags.includes("eval:math");
436
- case "reasoning":
437
- return tags.includes("eval:reasoning");
438
- case "hallucination":
439
- return tags.includes("eval:hallucination");
440
- case "rag":
441
- return tags.includes("eval:rag");
442
- case "language":
443
- return tags.some((tag) => tag.startsWith("language:"));
444
- case "vision":
445
- return tags.some(
446
- (tag) => tag === "modality:video" || tag === "modality:image"
447
- );
448
- case "threeD":
449
- return tags.includes("modality:3d");
450
- case "audio":
451
- return tags.includes("modality:audio");
452
- case "financial":
453
- return tags.includes("domain:financial");
454
- case "medical":
455
- return tags.includes("domain:medical");
456
- case "legal":
457
- return tags.includes("domain:legal");
458
- case "biology":
459
- return tags.includes("domain:biology");
460
- case "commercial":
461
- return tags.includes("domain:commercial");
462
- case "translation":
463
- return tags.includes("domain:translation");
464
- case "chemistry":
465
- return tags.includes("domain:chemistry");
466
- case "safety":
467
- return tags.includes("eval:safety");
468
- case "performance":
469
- return tags.includes("eval:performance");
470
- case "uncategorized":
471
- return !tags.some(
472
- (tag) =>
473
- CATEGORIZATION_TAGS.includes(tag) ||
474
- tag.startsWith("language:")
475
- );
476
- default:
477
- return false;
478
- }
479
- });
480
- });
481
- }
482
-
483
- // Appliquer le filtre Arena Only si nécessaire
484
- if (arenaOnly) {
485
- filteredBoards = filteredBoards.filter((board) =>
486
- board.tags?.includes("judge:humans")
487
- );
488
- }
489
-
490
- return filteredBoards.filter(
491
  (board) => board.approval_status === "approved"
492
  ).length;
493
- }, [selectedCategories, allSections, arenaOnly, totalArenaCount]);
494
 
495
- // Calculer le nombre filtré en prenant en compte tous les filtres
496
  const currentFilteredCount = useMemo(() => {
497
  if (!allSections) return 0;
498
 
499
- if (arenaOnly && !searchQuery && selectedCategories.size === 0) {
500
- return totalArenaCount;
501
- }
502
-
503
- // Récupérer tous les leaderboards uniques de toutes les sections
504
  const allLeaderboards = Array.from(
505
  new Set(
506
  allSections.reduce((acc, section) => {
@@ -509,19 +193,12 @@ const LeaderboardFilters = ({ allSections = [] }) => {
509
  )
510
  );
511
 
512
- // Appliquer les filtres sur l'ensemble des leaderboards
513
  const filteredBoards = filterLeaderboards(allLeaderboards);
514
  return filteredBoards.filter(
515
  (board) => board.approval_status === "approved"
516
  ).length;
517
- }, [
518
- selectedCategories,
519
- allSections,
520
- filterLeaderboards,
521
- arenaOnly,
522
- searchQuery,
523
- totalArenaCount,
524
- ]);
525
 
526
  const isMobile = useMediaQuery((theme) => theme.breakpoints.down("md"));
527
 
@@ -584,6 +261,8 @@ const LeaderboardFilters = ({ allSections = [] }) => {
584
  totalCount={totalCount}
585
  totalArenaCount={totalArenaCount}
586
  isMobile={isMobile}
 
 
587
  />
588
  </Stack>
589
  );
 
2
  import { Box, Stack, useMediaQuery } from "@mui/material";
3
  import { useLeaderboard } from "../../context/LeaderboardContext";
4
  import { useDebounce } from "../../hooks/useDebounce";
 
5
  import SearchBar from "./SearchBar";
6
  import FilterTag from "../common/FilterTag";
7
+ import {
8
+ CATEGORIZATION_TAGS,
9
+ filterLeaderboards as filterLeaderboardsUtil,
10
+ applyCategoryFilters,
11
+ } from "../../utils/filterUtils";
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
12
 
13
  // Helper function to get the category group of a section
14
  const getSectionGroup = (id) => {
 
94
  );
95
 
96
  // Apply current filters except the category being counted
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
97
  allSections.forEach(({ id, title }) => {
98
+ const baseFiltered = filterLeaderboardsUtil({
99
+ boards: allLeaderboards,
100
+ searchQuery,
101
+ arenaOnly,
102
+ selectedLanguage,
103
+ selectedCategories,
104
+ excludedCategory: id,
105
+ });
106
+
107
+ let sectionBoards = baseFiltered.filter((board) => {
108
+ const tags = board.tags || [];
109
+ return applyCategoryFilters(board, new Set([id]));
110
+ });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
111
 
112
  // Only count approved leaderboards
113
  sectionBoards = sectionBoards.filter(
 
150
  if (selectedCategories.size > 0) {
151
  boardsToCount = boardsToCount.filter((board) => {
152
  if (board.approval_status !== "approved") return false;
153
+ return applyCategoryFilters(board, selectedCategories);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
154
  });
155
  }
156
 
 
161
  setTotalArenaCount(arenaCount);
162
  }, [selectedCategories, allSections]);
163
 
164
+ // Calculer le nombre total de leaderboards approuvés (sans aucun filtre)
165
  const totalCount = useMemo(() => {
166
  if (!allSections) return 0;
167
 
168
+ // Récupérer tous les leaderboards uniques
 
 
 
 
169
  const allLeaderboards = Array.from(
170
  new Set(
171
  allSections.reduce((acc, section) => {
 
174
  )
175
  );
176
 
177
+ // Ne compter que les leaderboards approuvés
178
+ return allLeaderboards.filter(
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
179
  (board) => board.approval_status === "approved"
180
  ).length;
181
+ }, [allSections]);
182
 
183
+ // Calculer le nombre de leaderboards après application de tous les filtres
184
  const currentFilteredCount = useMemo(() => {
185
  if (!allSections) return 0;
186
 
187
+ // Récupérer tous les leaderboards uniques
 
 
 
 
188
  const allLeaderboards = Array.from(
189
  new Set(
190
  allSections.reduce((acc, section) => {
 
193
  )
194
  );
195
 
196
+ // Appliquer tous les filtres
197
  const filteredBoards = filterLeaderboards(allLeaderboards);
198
  return filteredBoards.filter(
199
  (board) => board.approval_status === "approved"
200
  ).length;
201
+ }, [allSections, filterLeaderboards]);
 
 
 
 
 
 
 
202
 
203
  const isMobile = useMediaQuery((theme) => theme.breakpoints.down("md"));
204
 
 
261
  totalCount={totalCount}
262
  totalArenaCount={totalArenaCount}
263
  isMobile={isMobile}
264
+ selectedCategories={selectedCategories}
265
+ selectedLanguage={selectedLanguage}
266
  />
267
  </Stack>
268
  );
client/src/components/LeaderboardFilters/SearchBar.jsx CHANGED
@@ -22,14 +22,29 @@ const SearchBar = ({
22
  totalCount,
23
  totalArenaCount,
24
  isMobile,
 
 
25
  }) => {
26
  const [inputValue, setInputValue] = useState(searchQuery || "");
27
  const debouncedSearch = useDebounce(inputValue, 200);
28
 
 
 
 
 
 
 
 
29
  // Update the search query after debounce
30
  React.useEffect(() => {
 
 
 
 
 
 
31
  setSearchQuery(debouncedSearch);
32
- }, [debouncedSearch, setSearchQuery]);
33
 
34
  // Update input value when searchQuery changes externally
35
  React.useEffect(() => {
@@ -73,39 +88,42 @@ const SearchBar = ({
73
  <Typography
74
  variant="body2"
75
  sx={{
76
- color: debouncedSearch
77
  ? "primary.main"
78
  : "text.secondary",
79
  fontWeight: 500,
80
  }}
81
  >
82
- {currentFilteredCount}
83
  </Typography>
84
- <Box
85
- sx={{
86
- display: "flex",
87
- alignItems: "center",
88
- color: arenaOnly ? "secondary.main" : "text.secondary",
89
- }}
90
- >
91
- <Typography
92
- variant="body2"
93
- sx={{
94
- fontWeight: 500,
95
- mx: 0.5,
96
- }}
97
- >
98
- /
99
- </Typography>
100
- <Typography
101
- variant="body2"
102
  sx={{
103
- fontWeight: 500,
 
 
104
  }}
105
  >
106
- {arenaOnly ? totalArenaCount : totalCount}
107
- </Typography>
108
- </Box>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
109
  <Typography
110
  variant="body2"
111
  sx={{
@@ -164,6 +182,8 @@ SearchBar.propTypes = {
164
  totalCount: PropTypes.number.isRequired,
165
  totalArenaCount: PropTypes.number.isRequired,
166
  isMobile: PropTypes.bool.isRequired,
 
 
167
  };
168
 
169
  export default SearchBar;
 
22
  totalCount,
23
  totalArenaCount,
24
  isMobile,
25
+ selectedCategories,
26
+ selectedLanguage,
27
  }) => {
28
  const [inputValue, setInputValue] = useState(searchQuery || "");
29
  const debouncedSearch = useDebounce(inputValue, 200);
30
 
31
+ // Détecter si des filtres sont actifs
32
+ const hasActiveFilters =
33
+ debouncedSearch ||
34
+ arenaOnly ||
35
+ selectedCategories?.size > 0 ||
36
+ selectedLanguage?.size > 0;
37
+
38
  // Update the search query after debounce
39
  React.useEffect(() => {
40
+ // Si l'input est vide, on met à jour immédiatement
41
+ if (!inputValue.trim()) {
42
+ setSearchQuery("");
43
+ return;
44
+ }
45
+ // Sinon on attend le debounce
46
  setSearchQuery(debouncedSearch);
47
+ }, [debouncedSearch, setSearchQuery, inputValue]);
48
 
49
  // Update input value when searchQuery changes externally
50
  React.useEffect(() => {
 
88
  <Typography
89
  variant="body2"
90
  sx={{
91
+ color: hasActiveFilters
92
  ? "primary.main"
93
  : "text.secondary",
94
  fontWeight: 500,
95
  }}
96
  >
97
+ {hasActiveFilters ? currentFilteredCount : totalCount}
98
  </Typography>
99
+ {hasActiveFilters && (
100
+ <Box
101
+ component="span"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
102
  sx={{
103
+ display: "flex",
104
+ alignItems: "center",
105
+ color: "text.secondary",
106
  }}
107
  >
108
+ <Typography
109
+ variant="body2"
110
+ sx={{
111
+ fontWeight: 500,
112
+ mx: 0.5,
113
+ }}
114
+ >
115
+ /
116
+ </Typography>
117
+ <Typography
118
+ variant="body2"
119
+ sx={{
120
+ fontWeight: 500,
121
+ }}
122
+ >
123
+ {totalCount}
124
+ </Typography>
125
+ </Box>
126
+ )}
127
  <Typography
128
  variant="body2"
129
  sx={{
 
182
  totalCount: PropTypes.number.isRequired,
183
  totalArenaCount: PropTypes.number.isRequired,
184
  isMobile: PropTypes.bool.isRequired,
185
+ selectedCategories: PropTypes.instanceOf(Set),
186
+ selectedLanguage: PropTypes.instanceOf(Set),
187
  };
188
 
189
  export default SearchBar;
client/src/components/LeaderboardSection/components/SectionHeader.jsx CHANGED
@@ -1,8 +1,40 @@
1
  import React from "react";
2
- import { Typography, Box, Button } from "@mui/material";
3
  import { alpha } from "@mui/material/styles";
4
  import ExpandMoreIcon from "@mui/icons-material/ExpandMore";
5
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
6
  const SectionHeader = ({
7
  title,
8
  count,
@@ -11,26 +43,70 @@ const SectionHeader = ({
11
  showExpandButton,
12
  isExpandButtonEnabled,
13
  }) => {
 
 
 
 
 
 
 
 
14
  return (
15
  <Box
16
  sx={{
17
  display: "flex",
18
- alignItems: "center",
19
  justifyContent: "space-between",
20
  mb: 4,
 
21
  }}
22
  >
23
  <Box sx={{ display: "flex", alignItems: "center", gap: 1 }}>
24
- <Typography
25
- variant="h4"
26
  sx={{
27
- color: "text.primary",
28
- fontWeight: 600,
29
- fontSize: { xs: "1.5rem", md: "2rem" },
30
  }}
31
  >
32
- {title}
33
- </Typography>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
34
  <Box
35
  sx={(theme) => ({
36
  width: "4px",
@@ -40,6 +116,7 @@ const SectionHeader = ({
40
  theme.palette.text.primary,
41
  theme.palette.mode === "dark" ? 0.2 : 0.15
42
  ),
 
43
  })}
44
  />
45
  <Typography
@@ -49,6 +126,7 @@ const SectionHeader = ({
49
  fontWeight: 400,
50
  fontSize: { xs: "1.25rem", md: "1.5rem" },
51
  opacity: 0.6,
 
52
  }}
53
  >
54
  {count}
@@ -64,6 +142,7 @@ const SectionHeader = ({
64
  fontSize: "0.875rem",
65
  textTransform: "none",
66
  opacity: isExpandButtonEnabled ? 1 : 0.5,
 
67
  "&:hover": {
68
  backgroundColor: (theme) =>
69
  isExpandButtonEnabled
 
1
  import React from "react";
2
+ import { Typography, Box, Button, Chip } from "@mui/material";
3
  import { alpha } from "@mui/material/styles";
4
  import ExpandMoreIcon from "@mui/icons-material/ExpandMore";
5
 
6
+ // Composant pour les chips
7
+ const StyledChip = ({ label, sx = {} }) => (
8
+ <Chip
9
+ label={label}
10
+ size="small"
11
+ sx={{
12
+ height: "24px",
13
+ backgroundColor: (theme) =>
14
+ alpha(
15
+ theme.palette.text.primary,
16
+ theme.palette.mode === "dark" ? 0.1 : 0.05
17
+ ),
18
+ color: "text.secondary",
19
+ fontSize: "0.75rem",
20
+ fontWeight: 500,
21
+ mx: 1,
22
+ "& .MuiChip-label": {
23
+ px: 1,
24
+ lineHeight: 1,
25
+ paddingTop: "1px", // Ajustement pour le centrage vertical
26
+ },
27
+ ...sx,
28
+ }}
29
+ />
30
+ );
31
+
32
+ // Composant pour le chip AND
33
+ const AndChip = () => <StyledChip label="AND" />;
34
+
35
+ // Composant pour le chip matching
36
+ const MatchingChip = () => <StyledChip label="MATCHING" />;
37
+
38
  const SectionHeader = ({
39
  title,
40
  count,
 
43
  showExpandButton,
44
  isExpandButtonEnabled,
45
  }) => {
46
+ // Séparer le titre en parties si c'est un titre combiné
47
+ const titleParts = title.split(" matching ");
48
+ const categories = titleParts[0].split(" + ");
49
+ const hasSearchQuery = titleParts.length > 1;
50
+ const searchQuery = hasSearchQuery
51
+ ? titleParts[1].replace(/['"]/g, "")
52
+ : null;
53
+
54
  return (
55
  <Box
56
  sx={{
57
  display: "flex",
58
+ alignItems: "flex-start",
59
  justifyContent: "space-between",
60
  mb: 4,
61
+ minHeight: { xs: "2.5rem", md: "3rem" },
62
  }}
63
  >
64
  <Box sx={{ display: "flex", alignItems: "center", gap: 1 }}>
65
+ <Box
 
66
  sx={{
67
+ display: "flex",
68
+ alignItems: "center",
69
+ flexWrap: "wrap",
70
  }}
71
  >
72
+ <Typography
73
+ variant="h4"
74
+ component="div"
75
+ sx={{
76
+ color: "text.primary",
77
+ fontWeight: 600,
78
+ fontSize: { xs: "1.5rem", md: "2rem" },
79
+ display: "flex",
80
+ alignItems: "center",
81
+ flexWrap: "wrap",
82
+ lineHeight: 1.2,
83
+ minHeight: { xs: "2rem", md: "2.5rem" },
84
+ }}
85
+ >
86
+ {categories.map((category, index) => (
87
+ <React.Fragment key={index}>
88
+ {index > 0 && <AndChip />}
89
+ {category}
90
+ </React.Fragment>
91
+ ))}
92
+ {hasSearchQuery && (
93
+ <>
94
+ <MatchingChip />
95
+ <Typography
96
+ component="span"
97
+ sx={{
98
+ color: "text.primary",
99
+ fontWeight: 600,
100
+ fontSize: "inherit",
101
+ lineHeight: "inherit",
102
+ }}
103
+ >
104
+ "{searchQuery}"
105
+ </Typography>
106
+ </>
107
+ )}
108
+ </Typography>
109
+ </Box>
110
  <Box
111
  sx={(theme) => ({
112
  width: "4px",
 
116
  theme.palette.text.primary,
117
  theme.palette.mode === "dark" ? 0.2 : 0.15
118
  ),
119
+ mx: 1,
120
  })}
121
  />
122
  <Typography
 
126
  fontWeight: 400,
127
  fontSize: { xs: "1.25rem", md: "1.5rem" },
128
  opacity: 0.6,
129
+ lineHeight: 1.2,
130
  }}
131
  >
132
  {count}
 
142
  fontSize: "0.875rem",
143
  textTransform: "none",
144
  opacity: isExpandButtonEnabled ? 1 : 0.5,
145
+ mt: { xs: "0.5rem", md: "0.75rem" },
146
  "&:hover": {
147
  backgroundColor: (theme) =>
148
  isExpandButtonEnabled
client/src/components/LeaderboardSection/index.jsx CHANGED
@@ -41,7 +41,8 @@ const LeaderboardSection = ({
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
@@ -57,8 +58,9 @@ const LeaderboardSection = ({
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;
 
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
+ id === "search-results"; // Toujours afficher tous les résultats pour la recherche textuelle
46
 
47
  // Si on doit tout montrer, on ne divise pas la liste
48
  const displayedLeaderboards = shouldShowAll
 
58
  ? 0
59
  : Math.max(0, 4 - approvedLeaderboards.length);
60
 
61
+ // On affiche le bouton expand seulement quand on n'a pas de sélection et que ce n'est pas une recherche textuelle
62
+ const showExpandButton =
63
+ selectedCategories.size === 0 && id !== "search-results";
64
 
65
  // Le bouton est actif seulement s'il y a plus de 4 leaderboards
66
  const isExpandButtonEnabled = approvedLeaderboards.length > ITEMS_PER_PAGE;
client/src/context/LeaderboardContext.jsx CHANGED
@@ -8,50 +8,17 @@ import React, {
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",
17
- "modality:artefacts",
18
- "modality:text",
19
- "eval:code",
20
- "eval:math",
21
- "eval:reasoning",
22
- "eval:hallucination",
23
- "modality:video",
24
- "modality:image",
25
- "modality:3d",
26
- "modality:audio",
27
- "domain:financial",
28
- "domain:medical",
29
- "domain:legal",
30
- "domain:biology",
31
- "domain:translation",
32
- "domain:chemistry",
33
- "domain:physics",
34
- "domain:commercial",
35
- "eval:safety",
36
- "eval:performance",
37
- "eval:rag",
38
- ];
39
-
40
  // Helper pour déterminer si un leaderboard est non catégorisé
41
- const isUncategorized = (board) => {
42
- const tags = board.tags || [];
43
- console.log("Checking uncategorized for board:", {
44
- id: board.id,
45
- tags,
46
- approval_status: board.approval_status,
47
- isUncategorized: !tags.some(
48
- (tag) => CATEGORIZATION_TAGS.includes(tag) || tag.startsWith("language:")
49
- ),
50
- });
51
- return !tags.some(
52
- (tag) => CATEGORIZATION_TAGS.includes(tag) || tag.startsWith("language:")
53
- );
54
- };
55
 
56
  export const LeaderboardProvider = ({ children }) => {
57
  const { params, updateParams } = useUrlState();
@@ -139,42 +106,11 @@ export const LeaderboardProvider = ({ children }) => {
139
  // Filter leaderboards based on search query and arena toggle, ignoring category selection
140
  const filterLeaderboardsForCount = useCallback(
141
  (boards) => {
142
- if (!boards) return [];
143
-
144
- let filtered = [...boards];
145
-
146
- // Filter by search query
147
- if (searchQuery) {
148
- const query = searchQuery.toLowerCase();
149
- const tagMatch = query.match(/^(\w+):(\w+)$/);
150
-
151
- if (tagMatch) {
152
- // Search by tag (ex: language:french)
153
- const [_, category, value] = tagMatch;
154
- const searchTag = `${category}:${value}`.toLowerCase();
155
- filtered = filtered.filter((board) => {
156
- const allTags = [
157
- ...(board.tags || []),
158
- ...(board.editor_tags || []),
159
- ];
160
- return allTags.some((tag) => tag.toLowerCase() === searchTag);
161
- });
162
- } else {
163
- // Regular search in title
164
- filtered = filtered.filter((board) =>
165
- board.card_data?.title?.toLowerCase().includes(query)
166
- );
167
- }
168
- }
169
-
170
- // Filter arena only
171
- if (arenaOnly) {
172
- filtered = filtered.filter((board) =>
173
- board.tags?.includes("judge:humans")
174
- );
175
- }
176
-
177
- return filtered;
178
  },
179
  [searchQuery, arenaOnly]
180
  );
@@ -182,84 +118,13 @@ export const LeaderboardProvider = ({ children }) => {
182
  // Filter leaderboards based on all criteria including categories and language selection
183
  const filterLeaderboards = useCallback(
184
  (boards) => {
185
- if (!boards) return [];
186
-
187
- let filtered = filterLeaderboardsForCount(boards);
188
-
189
- // Filter by selected languages if any
190
- if (selectedLanguage.size > 0) {
191
- filtered = filtered.filter((board) =>
192
- Array.from(selectedLanguage).some((lang) =>
193
- board.tags?.some(
194
- (tag) => tag.toLowerCase() === `language:${lang.toLowerCase()}`
195
- )
196
- )
197
- );
198
- }
199
-
200
- // Filter by selected categories if any
201
- if (selectedCategories.size > 0) {
202
- filtered = filtered.filter((board) => {
203
- const { tags = [] } = board;
204
- // Un leaderboard est inclus s'il correspond à TOUTES les catégories sélectionnées
205
- return Array.from(selectedCategories).every((category) => {
206
- switch (category) {
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 false;
257
- }
258
- });
259
- });
260
- }
261
-
262
- return filtered;
263
  },
264
  [searchQuery, arenaOnly, selectedCategories, selectedLanguage]
265
  );
@@ -270,49 +135,9 @@ export const LeaderboardProvider = ({ children }) => {
270
  return [...boards];
271
  }, []);
272
 
273
- // Fonction pour compter les leaderboards par langue
274
- const getLanguageStats = useCallback((boards) => {
275
- const stats = new Map();
276
-
277
- boards.forEach((board) => {
278
- board.tags?.forEach((tag) => {
279
- if (tag.startsWith("language:")) {
280
- const language = tag.split(":")[1];
281
- const capitalizedLang =
282
- language.charAt(0).toUpperCase() + language.slice(1);
283
- const count = stats.get(capitalizedLang) || 0;
284
- stats.set(capitalizedLang, count + 1);
285
- }
286
- });
287
- });
288
-
289
- return stats;
290
- }, []);
291
-
292
- // Calculate total number of unique leaderboards (excluding duplicates)
293
- const totalLeaderboards = useMemo(() => {
294
- // On ne compte que les leaderboards approuvés
295
- const uniqueIds = new Set(
296
- leaderboards
297
- .filter((board) => board.approval_status === "approved")
298
- .map((board) => board.id)
299
- );
300
- return uniqueIds.size;
301
- }, [leaderboards]);
302
-
303
  // Filter functions for categories
304
  const filterByTag = useCallback((tag, boards) => {
305
  const searchTag = tag.toLowerCase();
306
- console.log("Filtering by tag:", {
307
- searchTag,
308
- boards: boards?.map((board) => ({
309
- id: board.id,
310
- tags: board.tags,
311
- matches: {
312
- tags: board.tags?.some((t) => t.toLowerCase() === searchTag),
313
- },
314
- })),
315
- });
316
  return (
317
  boards?.filter((board) =>
318
  board.tags?.some((t) => t.toLowerCase() === searchTag)
@@ -331,257 +156,23 @@ export const LeaderboardProvider = ({ children }) => {
331
  // Define sections with raw data
332
  const allSections = useMemo(() => {
333
  if (!leaderboards) return [];
334
-
335
- // Garder une trace des leaderboards déjà catégorisés
336
- const categorizedIds = new Set();
337
-
338
- const sections = [
339
- // Science
340
- {
341
- id: "code",
342
- title: "Code",
343
- data: filterByTag("eval:code", leaderboards).map((board) => {
344
- categorizedIds.add(board.id);
345
- return board;
346
- }),
347
- },
348
- {
349
- id: "math",
350
- title: "Math",
351
- data: filterByTag("eval:math", leaderboards).map((board) => {
352
- categorizedIds.add(board.id);
353
- return board;
354
- }),
355
- },
356
- {
357
- id: "biology",
358
- title: "Biology",
359
- data: filterByTag("domain:biology", leaderboards).map((board) => {
360
- categorizedIds.add(board.id);
361
- return board;
362
- }),
363
- },
364
- {
365
- id: "chemistry",
366
- title: "Chemistry",
367
- data: filterByTag("domain:chemistry", leaderboards).map((board) => {
368
- categorizedIds.add(board.id);
369
- return board;
370
- }),
371
- },
372
- {
373
- id: "physics",
374
- title: "Physics",
375
- data: filterByTag("domain:physics", leaderboards).map((board) => {
376
- categorizedIds.add(board.id);
377
- return board;
378
- }),
379
- },
380
- // Modalities
381
- {
382
- id: "image",
383
- title: "Image",
384
- data: filterByTag("modality:image", leaderboards).map((board) => {
385
- categorizedIds.add(board.id);
386
- return board;
387
- }),
388
- },
389
- {
390
- id: "video",
391
- title: "Video",
392
- data: filterByTag("modality:video", leaderboards).map((board) => {
393
- categorizedIds.add(board.id);
394
- return board;
395
- }),
396
- },
397
- {
398
- id: "audio",
399
- title: "Audio",
400
- data: filterByTag("modality:audio", leaderboards).map((board) => {
401
- categorizedIds.add(board.id);
402
- return board;
403
- }),
404
- },
405
- {
406
- id: "text",
407
- title: "Text",
408
- data: filterByTag("modality:text", leaderboards).map((board) => {
409
- categorizedIds.add(board.id);
410
- return board;
411
- }),
412
- },
413
- {
414
- id: "threeD",
415
- title: "3D",
416
- data: filterByTag("modality:3d", leaderboards).map((board) => {
417
- categorizedIds.add(board.id);
418
- return board;
419
- }),
420
- },
421
- {
422
- id: "embeddings",
423
- title: "Embeddings",
424
- data: filterByTag("modality:artefacts", leaderboards).map((board) => {
425
- categorizedIds.add(board.id);
426
- return board;
427
- }),
428
- },
429
- // LLM Capabilities
430
- {
431
- id: "rag",
432
- title: "RAG",
433
- data: filterByTag("eval:rag", leaderboards).map((board) => {
434
- categorizedIds.add(board.id);
435
- return board;
436
- }),
437
- },
438
- {
439
- id: "reasoning",
440
- title: "Reasoning",
441
- data: filterByTag("eval:reasoning", leaderboards).map((board) => {
442
- categorizedIds.add(board.id);
443
- return board;
444
- }),
445
- },
446
- {
447
- id: "agentic",
448
- title: "Agentic",
449
- data: filterByTag("modality:agent", leaderboards).map((board) => {
450
- categorizedIds.add(board.id);
451
- return board;
452
- }),
453
- },
454
- {
455
- id: "safety",
456
- title: "Safety",
457
- data: filterByTag("eval:safety", leaderboards).map((board) => {
458
- categorizedIds.add(board.id);
459
- return board;
460
- }),
461
- },
462
- {
463
- id: "performance",
464
- title: "Performance",
465
- data: filterByTag("eval:performance", leaderboards).map((board) => {
466
- categorizedIds.add(board.id);
467
- return board;
468
- }),
469
- },
470
- {
471
- id: "hallucination",
472
- title: "Hallucination",
473
- data: filterByTag("eval:hallucination", leaderboards).map((board) => {
474
- categorizedIds.add(board.id);
475
- return board;
476
- }),
477
- },
478
- // Domain Specific
479
- {
480
- id: "medical",
481
- title: "Medical",
482
- data: filterByTag("domain:medical", leaderboards).map((board) => {
483
- categorizedIds.add(board.id);
484
- return board;
485
- }),
486
- },
487
- {
488
- id: "financial",
489
- title: "Financial",
490
- data: filterByTag("domain:financial", leaderboards).map((board) => {
491
- categorizedIds.add(board.id);
492
- return board;
493
- }),
494
- },
495
- {
496
- id: "legal",
497
- title: "Legal",
498
- data: filterByTag("domain:legal", leaderboards).map((board) => {
499
- categorizedIds.add(board.id);
500
- return board;
501
- }),
502
- },
503
- {
504
- id: "commercial",
505
- title: "Commercial",
506
- data: filterByTag("domain:commercial", leaderboards).map((board) => {
507
- categorizedIds.add(board.id);
508
- return board;
509
- }),
510
- },
511
- // Language Related
512
- {
513
- id: "language",
514
- title: "Language Specific",
515
- data: filterByLanguage(leaderboards).map((board) => {
516
- categorizedIds.add(board.id);
517
- return board;
518
- }),
519
- },
520
- {
521
- id: "translation",
522
- title: "Translation",
523
- data: filterByTag("domain:translation", leaderboards).map((board) => {
524
- categorizedIds.add(board.id);
525
- return board;
526
- }),
527
- },
528
- // Misc
529
- {
530
- id: "uncategorized",
531
- title: "Uncategorized",
532
- data: leaderboards
533
- .filter(isUncategorized)
534
- .filter((board) => board.approval_status === "approved"),
535
- },
536
- ];
537
-
538
- return sections;
539
  }, [leaderboards, filterByTag, filterByLanguage]);
540
 
541
  // Get sections with data
542
  const sections = useMemo(() => {
543
- console.log("Starting sections filtering...");
544
- console.log(
545
- "All sections before filtering:",
546
- allSections.map((s) => ({
547
- title: s.title,
548
- count: s.data.length,
549
- ids: s.data.map((b) => b.id),
550
- }))
551
- );
552
-
553
- // On garde une trace des titres déjà vus
554
- const seenTitles = new Set();
555
- const filteredSections = allSections.filter((section) => {
556
- console.log(`\nAnalyzing section ${section.title}:`, {
557
- data: section.data,
558
- count: section.data.length,
559
- uniqueIds: new Set(section.data.map((b) => b.id)).size,
560
- boards: section.data.map((board) => ({
561
- id: board.id,
562
- tags: board.tags,
563
- })),
564
- });
565
-
566
- // On garde la section si elle a des données et qu'on ne l'a pas déjà vue
567
- if (section.data.length > 0 && !seenTitles.has(section.title)) {
568
- seenTitles.add(section.title);
569
- return true;
570
- }
571
- return false;
572
- });
573
 
574
- console.log(
575
- "\nFinal sections after filtering:",
576
- filteredSections.map((s) => ({
577
- title: s.title,
578
- count: s.data.length,
579
- uniqueIds: new Set(s.data.map((b) => b.id)).size,
580
- }))
581
  );
582
-
583
- return filteredSections;
584
- }, [allSections]);
585
 
586
  // Get filtered count
587
  const filteredCount = useMemo(() => {
@@ -658,8 +249,6 @@ export const LeaderboardProvider = ({ children }) => {
658
  setSelectedLanguage: handleLanguageSelection,
659
  expandedSections,
660
  setExpandedSections,
661
- getLanguageStats,
662
- getSectionLeaderboards,
663
  resetState,
664
  isLanguageExpanded,
665
  setIsLanguageExpanded,
 
8
  } from "react";
9
  import { normalizeTags } from "../utils/tagFilters";
10
  import { useUrlState } from "../hooks/useUrlState";
11
+ import {
12
+ CATEGORIZATION_TAGS,
13
+ isUncategorized,
14
+ filterLeaderboards as filterLeaderboardsUtil,
15
+ generateSections,
16
+ } from "../utils/filterUtils";
17
 
18
  const LeaderboardContext = createContext();
19
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
20
  // Helper pour déterminer si un leaderboard est non catégorisé
21
+ const isUncategorizedBoard = isUncategorized;
 
 
 
 
 
 
 
 
 
 
 
 
 
22
 
23
  export const LeaderboardProvider = ({ children }) => {
24
  const { params, updateParams } = useUrlState();
 
106
  // Filter leaderboards based on search query and arena toggle, ignoring category selection
107
  const filterLeaderboardsForCount = useCallback(
108
  (boards) => {
109
+ return filterLeaderboardsUtil({
110
+ boards,
111
+ searchQuery,
112
+ arenaOnly,
113
+ });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
114
  },
115
  [searchQuery, arenaOnly]
116
  );
 
118
  // Filter leaderboards based on all criteria including categories and language selection
119
  const filterLeaderboards = useCallback(
120
  (boards) => {
121
+ return filterLeaderboardsUtil({
122
+ boards,
123
+ searchQuery,
124
+ arenaOnly,
125
+ selectedCategories,
126
+ selectedLanguage,
127
+ });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
128
  },
129
  [searchQuery, arenaOnly, selectedCategories, selectedLanguage]
130
  );
 
135
  return [...boards];
136
  }, []);
137
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
138
  // Filter functions for categories
139
  const filterByTag = useCallback((tag, boards) => {
140
  const searchTag = tag.toLowerCase();
 
 
 
 
 
 
 
 
 
 
141
  return (
142
  boards?.filter((board) =>
143
  board.tags?.some((t) => t.toLowerCase() === searchTag)
 
156
  // Define sections with raw data
157
  const allSections = useMemo(() => {
158
  if (!leaderboards) return [];
159
+ return generateSections(leaderboards, filterByTag, filterByLanguage);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
160
  }, [leaderboards, filterByTag, filterByLanguage]);
161
 
162
  // Get sections with data
163
  const sections = useMemo(() => {
164
+ return allSections.filter((section) => section.data.length > 0);
165
+ }, [allSections]);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
166
 
167
+ // Calculate total number of unique leaderboards (excluding duplicates)
168
+ const totalLeaderboards = useMemo(() => {
169
+ const uniqueIds = new Set(
170
+ leaderboards
171
+ .filter((board) => board.approval_status === "approved")
172
+ .map((board) => board.id)
 
173
  );
174
+ return uniqueIds.size;
175
+ }, [leaderboards]);
 
176
 
177
  // Get filtered count
178
  const filteredCount = useMemo(() => {
 
249
  setSelectedLanguage: handleLanguageSelection,
250
  expandedSections,
251
  setExpandedSections,
 
 
252
  resetState,
253
  isLanguageExpanded,
254
  setIsLanguageExpanded,
client/src/hooks/useDebounce.js CHANGED
@@ -4,6 +4,13 @@ export const useDebounce = (value, delay = 200) => {
4
  const [debouncedValue, setDebouncedValue] = useState(value);
5
 
6
  useEffect(() => {
 
 
 
 
 
 
 
7
  const timer = setTimeout(() => {
8
  setDebouncedValue(value);
9
  }, delay);
 
4
  const [debouncedValue, setDebouncedValue] = useState(value);
5
 
6
  useEffect(() => {
7
+ // Si la valeur est vide, on met à jour immédiatement
8
+ if (!value || !value.trim()) {
9
+ setDebouncedValue("");
10
+ return;
11
+ }
12
+
13
+ // Sinon on attend le délai
14
  const timer = setTimeout(() => {
15
  setDebouncedValue(value);
16
  }, delay);
client/src/pages/LeaderboardPage/LeaderboardPage.jsx CHANGED
@@ -1,4 +1,4 @@
1
- import React, { useState, useEffect } from "react";
2
  import { Box, CircularProgress, Typography } from "@mui/material";
3
  import SearchOffIcon from "@mui/icons-material/SearchOff";
4
  import Logo from "../../components/Logo/Logo";
@@ -23,6 +23,28 @@ const LeaderboardPageContent = () => {
23
  selectedCategories,
24
  } = useLeaderboard();
25
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
26
  useEffect(() => {
27
  fetch(API_URLS.leaderboards)
28
  .then((res) => {
@@ -145,80 +167,98 @@ const LeaderboardPageContent = () => {
145
  </Box>
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;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
211
  return (
212
- <Box key={id} id={id}>
213
  <LeaderboardSection
214
- id={id}
215
- title={title}
216
- leaderboards={data}
217
  filteredLeaderboards={filteredLeaderboards}
218
  />
219
  </Box>
220
  );
221
- })}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
222
  </Box>
223
  )}
224
  </Box>
 
1
+ import React, { useState, useEffect, useMemo } from "react";
2
  import { Box, CircularProgress, Typography } from "@mui/material";
3
  import SearchOffIcon from "@mui/icons-material/SearchOff";
4
  import Logo from "../../components/Logo/Logo";
 
23
  selectedCategories,
24
  } = useLeaderboard();
25
 
26
+ // Vérifier si on a uniquement une recherche textuelle active
27
+ const isOnlyTextSearch =
28
+ searchQuery && !arenaOnly && selectedCategories.size === 0;
29
+
30
+ // Obtenir tous les leaderboards uniques de toutes les sections
31
+ const allUniqueLeaderboards = useMemo(() => {
32
+ if (!allSections) return [];
33
+ return Array.from(
34
+ new Set(
35
+ allSections.reduce((acc, section) => {
36
+ return [...acc, ...(section.data || [])];
37
+ }, [])
38
+ )
39
+ );
40
+ }, [allSections]);
41
+
42
+ // Filtrer tous les leaderboards pour la recherche textuelle
43
+ const searchResults = useMemo(() => {
44
+ if (!isOnlyTextSearch) return [];
45
+ return filterLeaderboards(allUniqueLeaderboards);
46
+ }, [isOnlyTextSearch, filterLeaderboards, allUniqueLeaderboards]);
47
+
48
  useEffect(() => {
49
  fetch(API_URLS.leaderboards)
50
  .then((res) => {
 
167
  </Box>
168
  )}
169
 
170
+ {isOnlyTextSearch ? (
171
+ // Vue spéciale pour la recherche textuelle
172
+ <Box key="search-results">
173
+ <LeaderboardSection
174
+ id="search-results"
175
+ title={`All leaderboards matching "${searchQuery}"`}
176
+ leaderboards={allUniqueLeaderboards}
177
+ filteredLeaderboards={searchResults}
178
+ />
179
+ </Box>
180
+ ) : selectedCategories.size > 0 ? (
181
+ // Si des catégories sont sélectionnées
182
+ selectedCategories.size === 1 ? (
183
+ // Si une seule catégorie est sélectionnée, on affiche sa section
184
+ sections
185
+ .filter(({ id }) => selectedCategories.has(id))
186
+ .map(({ id, title, data }) => {
187
+ const filteredLeaderboards = filterLeaderboards(data);
188
+ // Ajouter le terme de recherche au titre si présent
189
+ const sectionTitle = searchQuery
190
+ ? `${title} matching "${searchQuery}"`
191
+ : title;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
192
  return (
193
+ <Box key={id} id={id}>
194
  <LeaderboardSection
195
+ id={id}
196
+ title={sectionTitle}
197
+ leaderboards={data}
198
  filteredLeaderboards={filteredLeaderboards}
199
  />
200
  </Box>
201
  );
202
+ })
203
+ ) : (
204
+ // Si plusieurs catégories sont sélectionnées, on les agrège
205
+ (() => {
206
+ // Agréger les données de toutes les sections sélectionnées
207
+ const selectedSections = sections.filter(({ id }) =>
208
+ selectedCategories.has(id)
209
+ );
210
+
211
+ // Créer un titre combiné avec le terme de recherche si présent
212
+ const combinedTitle = selectedSections
213
+ .map(({ title }) => title)
214
+ .join(" + ");
215
+ const finalTitle = searchQuery
216
+ ? `${combinedTitle} matching "${searchQuery}"`
217
+ : combinedTitle;
218
+
219
+ // Agréger les leaderboards
220
+ const combinedData = selectedSections.reduce(
221
+ (acc, { data }) => [...acc, ...data],
222
+ []
223
+ );
224
+
225
+ // Filtrer les doublons par ID
226
+ const uniqueData = Array.from(
227
+ new Map(combinedData.map((item) => [item.id, item])).values()
228
+ );
229
+
230
+ const filteredLeaderboards = filterLeaderboards(uniqueData);
231
+
232
  return (
233
+ <Box key="combined">
234
  <LeaderboardSection
235
+ id="combined"
236
+ title={finalTitle}
237
+ leaderboards={uniqueData}
238
  filteredLeaderboards={filteredLeaderboards}
239
  />
240
  </Box>
241
  );
242
+ })()
243
+ )
244
+ ) : (
245
+ // Si aucune catégorie n'est sélectionnée, on affiche toutes les sections avec des résultats
246
+ (hasLeaderboards || !isFiltering) &&
247
+ sections.map(({ id, title, data }) => {
248
+ const filteredLeaderboards = filterLeaderboards(data);
249
+ if (filteredLeaderboards.length === 0) return null;
250
+ return (
251
+ <Box key={id} id={id}>
252
+ <LeaderboardSection
253
+ id={id}
254
+ title={title}
255
+ leaderboards={data}
256
+ filteredLeaderboards={filteredLeaderboards}
257
+ />
258
+ </Box>
259
+ );
260
+ })
261
+ )}
262
  </Box>
263
  )}
264
  </Box>
client/src/utils/filterUtils.js ADDED
@@ -0,0 +1,219 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // Map of category IDs to their corresponding tag checks
2
+ export const CATEGORY_TAG_MAPPING = {
3
+ agentic: (tags) => tags.includes("modality:agent"),
4
+ text: (tags) => tags.includes("modality:text"),
5
+ image: (tags) => tags.includes("modality:image"),
6
+ video: (tags) => tags.includes("modality:video"),
7
+ code: (tags) => tags.includes("eval:code"),
8
+ math: (tags) => tags.includes("eval:math"),
9
+ reasoning: (tags) => tags.includes("eval:reasoning"),
10
+ hallucination: (tags) => tags.includes("eval:hallucination"),
11
+ rag: (tags) => tags.includes("eval:rag"),
12
+ embeddings: (tags) => tags.includes("modality:artefacts"),
13
+ language: (tags) => tags.some((tag) => tag.startsWith("language:")),
14
+ vision: (tags) =>
15
+ tags.some((tag) => tag === "modality:video" || tag === "modality:image"),
16
+ threeD: (tags) => tags.includes("modality:3d"),
17
+ audio: (tags) => tags.includes("modality:audio"),
18
+ financial: (tags) => tags.includes("domain:financial"),
19
+ medical: (tags) => tags.includes("domain:medical"),
20
+ legal: (tags) => tags.includes("domain:legal"),
21
+ biology: (tags) => tags.includes("domain:biology"),
22
+ commercial: (tags) => tags.includes("domain:commercial"),
23
+ translation: (tags) => tags.includes("domain:translation"),
24
+ chemistry: (tags) => tags.includes("domain:chemistry"),
25
+ physics: (tags) => tags.includes("domain:physics"),
26
+ safety: (tags) => tags.includes("eval:safety"),
27
+ performance: (tags) => tags.includes("eval:performance"),
28
+ };
29
+
30
+ // Constantes pour les tags de catégorisation
31
+ export const CATEGORIZATION_TAGS = [
32
+ "modality:agent",
33
+ "modality:artefacts",
34
+ "modality:text",
35
+ "eval:code",
36
+ "eval:math",
37
+ "eval:reasoning",
38
+ "eval:hallucination",
39
+ "modality:video",
40
+ "modality:image",
41
+ "modality:3d",
42
+ "modality:audio",
43
+ "domain:financial",
44
+ "domain:medical",
45
+ "domain:legal",
46
+ "domain:biology",
47
+ "domain:translation",
48
+ "domain:chemistry",
49
+ "domain:physics",
50
+ "domain:commercial",
51
+ "eval:safety",
52
+ "eval:performance",
53
+ "eval:rag",
54
+ ];
55
+
56
+ /**
57
+ * Vérifie si un leaderboard est non catégorisé
58
+ */
59
+ export const isUncategorized = (board) => {
60
+ const tags = board.tags || [];
61
+ return !tags.some(
62
+ (tag) => CATEGORIZATION_TAGS.includes(tag) || tag.startsWith("language:")
63
+ );
64
+ };
65
+
66
+ /**
67
+ * Applique les filtres de catégorie à un leaderboard
68
+ */
69
+ export const applyCategoryFilters = (
70
+ board,
71
+ selectedCategories,
72
+ excludedCategory = null
73
+ ) => {
74
+ if (selectedCategories.size === 0) return true;
75
+
76
+ const tags = board.tags || [];
77
+ return Array.from(selectedCategories)
78
+ .filter((category) => category !== excludedCategory)
79
+ .every((category) => {
80
+ if (category === "uncategorized") {
81
+ return isUncategorized(board);
82
+ }
83
+ const tagCheck = CATEGORY_TAG_MAPPING[category];
84
+ return tagCheck ? tagCheck(tags) : false;
85
+ });
86
+ };
87
+
88
+ /**
89
+ * Filtre une liste de leaderboards selon les critères donnés
90
+ */
91
+ export const filterLeaderboards = ({
92
+ boards,
93
+ searchQuery = "",
94
+ arenaOnly = false,
95
+ selectedCategories = new Set(),
96
+ selectedLanguage = new Set(),
97
+ excludedCategory = null,
98
+ }) => {
99
+ if (!boards) return [];
100
+
101
+ let filtered = [...boards];
102
+
103
+ // Filter by search query
104
+ if (searchQuery) {
105
+ const query = searchQuery.toLowerCase();
106
+ const tagMatch = query.match(/^(\w+):(\w+)$/);
107
+
108
+ if (tagMatch) {
109
+ const [_, category, value] = tagMatch;
110
+ const searchTag = `${category}:${value}`.toLowerCase();
111
+ filtered = filtered.filter((board) => {
112
+ const allTags = [...(board.tags || []), ...(board.editor_tags || [])];
113
+ return allTags.some((tag) => tag.toLowerCase() === searchTag);
114
+ });
115
+ } else {
116
+ filtered = filtered.filter((board) =>
117
+ board.card_data?.title?.toLowerCase().includes(query)
118
+ );
119
+ }
120
+ }
121
+
122
+ // Filter arena only
123
+ if (arenaOnly) {
124
+ filtered = filtered.filter((board) => board.tags?.includes("judge:humans"));
125
+ }
126
+
127
+ // Filter by selected languages
128
+ if (selectedLanguage.size > 0) {
129
+ filtered = filtered.filter((board) =>
130
+ Array.from(selectedLanguage).every((lang) =>
131
+ board.tags?.some(
132
+ (tag) => tag.toLowerCase() === `language:${lang.toLowerCase()}`
133
+ )
134
+ )
135
+ );
136
+ }
137
+
138
+ // Filter by categories
139
+ if (selectedCategories.size > 0) {
140
+ filtered = filtered.filter((board) =>
141
+ applyCategoryFilters(board, selectedCategories, excludedCategory)
142
+ );
143
+ }
144
+
145
+ return filtered;
146
+ };
147
+
148
+ // Structure des sections avec leurs groupes
149
+ export const SECTIONS_STRUCTURE = {
150
+ science: [
151
+ { id: "code", title: "Code", tag: "eval:code" },
152
+ { id: "math", title: "Math", tag: "eval:math" },
153
+ { id: "biology", title: "Biology", tag: "domain:biology" },
154
+ { id: "chemistry", title: "Chemistry", tag: "domain:chemistry" },
155
+ { id: "physics", title: "Physics", tag: "domain:physics" },
156
+ ],
157
+ modalities: [
158
+ { id: "image", title: "Image", tag: "modality:image" },
159
+ { id: "video", title: "Video", tag: "modality:video" },
160
+ { id: "audio", title: "Audio", tag: "modality:audio" },
161
+ { id: "text", title: "Text", tag: "modality:text" },
162
+ { id: "threeD", title: "3D", tag: "modality:3d" },
163
+ { id: "embeddings", title: "Embeddings", tag: "modality:artefacts" },
164
+ ],
165
+ llm_capabilities: [
166
+ { id: "rag", title: "RAG", tag: "eval:rag" },
167
+ { id: "reasoning", title: "Reasoning", tag: "eval:reasoning" },
168
+ { id: "agentic", title: "Agentic", tag: "modality:agent" },
169
+ { id: "safety", title: "Safety", tag: "eval:safety" },
170
+ { id: "performance", title: "Performance", tag: "eval:performance" },
171
+ { id: "hallucination", title: "Hallucination", tag: "eval:hallucination" },
172
+ ],
173
+ domain_specific: [
174
+ { id: "medical", title: "Medical", tag: "domain:medical" },
175
+ { id: "financial", title: "Financial", tag: "domain:financial" },
176
+ { id: "legal", title: "Legal", tag: "domain:legal" },
177
+ { id: "commercial", title: "Commercial", tag: "domain:commercial" },
178
+ ],
179
+ language_related: [
180
+ { id: "language", title: "Language Specific", isLanguageFilter: true },
181
+ { id: "translation", title: "Translation", tag: "domain:translation" },
182
+ ],
183
+ };
184
+
185
+ /**
186
+ * Génère les sections avec leurs données
187
+ */
188
+ export const generateSections = (
189
+ leaderboards,
190
+ filterByTag,
191
+ filterByLanguage
192
+ ) => {
193
+ if (!leaderboards) return [];
194
+
195
+ const sections = [];
196
+
197
+ // Parcourir la structure des sections
198
+ Object.entries(SECTIONS_STRUCTURE).forEach(([group, groupSections]) => {
199
+ groupSections.forEach(({ id, title, tag, isLanguageFilter }) => {
200
+ let sectionData;
201
+ if (isLanguageFilter) {
202
+ sectionData = filterByLanguage(leaderboards);
203
+ } else {
204
+ sectionData = filterByTag(tag, leaderboards);
205
+ }
206
+
207
+ if (sectionData.length > 0) {
208
+ sections.push({
209
+ id,
210
+ title,
211
+ data: sectionData,
212
+ group,
213
+ });
214
+ }
215
+ });
216
+ });
217
+
218
+ return sections;
219
+ };