import React, { createContext, useContext, useState, useCallback, useMemo, useEffect, } from "react"; const LeaderboardContext = createContext(); // Helper pour mettre à jour l'URL const updateURL = (params) => { const url = new URL(window.location); Object.entries(params).forEach(([key, value]) => { if (value) { url.searchParams.set(key, value); } else { url.searchParams.delete(key); } }); window.history.pushState({}, "", url); }; // Helper pour lire les paramètres de l'URL const getURLParams = () => { const params = new URLSearchParams(window.location.search); return { category: params.get("category"), search: params.get("search"), arena: params.get("arena") === "true", language: params.get("language"), }; }; export const LeaderboardProvider = ({ children }) => { // Lire les paramètres initiaux depuis l'URL const initialParams = getURLParams(); const [leaderboards, setLeaderboards] = useState([]); const [searchQuery, setSearchQuery] = useState(initialParams.search || ""); const [arenaOnly, setArenaOnly] = useState(initialParams.arena); const [selectedCategory, setSelectedCategory] = useState( initialParams.category ); const [selectedLanguage, setSelectedLanguage] = useState( initialParams.language ); const [expandedSections, setExpandedSections] = useState(new Set()); // Mettre à jour l'URL quand les filtres changent useEffect(() => { updateURL({ category: selectedCategory, search: searchQuery, arena: arenaOnly ? "true" : null, language: selectedLanguage, }); }, [selectedCategory, searchQuery, arenaOnly, selectedLanguage]); // Écouter les changements d'URL (navigation back/forward) useEffect(() => { const handleURLChange = () => { const params = getURLParams(); setSearchQuery(params.search || ""); setArenaOnly(params.arena); setSelectedCategory(params.category); setSelectedLanguage(params.language); }; window.addEventListener("popstate", handleURLChange); return () => window.removeEventListener("popstate", handleURLChange); }, []); // Wrapper pour setSelectedCategory qui gère aussi l'expansion des sections const handleCategorySelection = useCallback((categoryId) => { // On réinitialise toujours la langue sélectionnée setSelectedLanguage(null); setSelectedCategory((prev) => { if (prev === categoryId) { // Si on désélectionne, on replie toutes les sections setExpandedSections(new Set()); return null; } // Si on sélectionne une nouvelle catégorie, on la déploie setExpandedSections(new Set([categoryId])); return categoryId; }); }, []); // Wrapper pour la sélection de langue const handleLanguageSelection = useCallback((language) => { setSelectedLanguage((prev) => (prev === language ? null : language)); }, []); // Filter leaderboards based on search query and arena toggle, ignoring category selection const filterLeaderboardsForCount = useCallback( (boards) => { if (!boards) return []; let filtered = [...boards]; // Filter by search query (text only) if (searchQuery) { const query = searchQuery.toLowerCase(); filtered = filtered.filter((board) => board.card_data?.title?.toLowerCase().includes(query) ); } // Filter arena only if (arenaOnly) { filtered = filtered.filter((board) => board.tags?.includes("judge:humans") ); } return filtered; }, [searchQuery, arenaOnly] ); // Filter leaderboards based on all criteria including category and language selection const filterLeaderboards = useCallback( (boards) => { if (!boards) return []; let filtered = filterLeaderboardsForCount(boards); // Filter by selected language if any if (selectedLanguage) { filtered = filtered.filter((board) => board.tags?.some( (tag) => tag.toLowerCase() === `language:${selectedLanguage.toLowerCase()}` ) ); } // Filter by selected category if any if (selectedCategory) { filtered = filtered.filter((board) => { const { tags = [] } = board; switch (selectedCategory) { case "agentic": return tags.includes("modality:agent"); case "code": return tags.includes("eval:code"); case "math": return tags.includes("eval:math"); case "language": return tags.some((tag) => tag.startsWith("language:")); case "vision": return tags.some( (tag) => tag === "modality:video" || tag === "modality:image" ); case "threeD": return tags.includes("modality:3d"); case "audio": return tags.includes("modality:audio"); case "financial": return tags.includes("domain:financial"); case "medical": return tags.includes("domain:medical"); case "legal": return tags.includes("domain:legal"); case "safety": return tags.includes("eval:safety"); case "uncategorized": return !tags.some( (tag) => [ "modality:agent", "eval:code", "eval:math", "modality:video", "modality:image", "modality:3d", "modality:audio", "domain:financial", "domain:medical", "domain:legal", "eval:safety", ].includes(tag) || tag.startsWith("language:") ); default: return true; } }); } return filtered; }, [filterLeaderboardsForCount, selectedCategory, selectedLanguage] ); // Fonction pour obtenir les leaderboards bruts d'une section const getSectionLeaderboards = useCallback((boards) => { if (!boards) return []; return [...boards]; }, []); // Fonction pour compter les leaderboards par langue const getLanguageStats = useCallback((boards) => { const stats = new Map(); boards.forEach((board) => { board.tags?.forEach((tag) => { if (tag.startsWith("language:")) { const language = tag.split(":")[1]; const capitalizedLang = language.charAt(0).toUpperCase() + language.slice(1); const count = stats.get(capitalizedLang) || 0; stats.set(capitalizedLang, count + 1); } }); }); return stats; }, []); // Calculate total number of unique leaderboards (excluding duplicates) const totalLeaderboards = useMemo(() => { const uniqueIds = new Set(leaderboards.map((board) => board.id)); return uniqueIds.size; }, [leaderboards]); // Filter functions for categories const filterByTag = useCallback((tag, boards) => { const searchTag = tag.toLowerCase(); console.log("Filtering by tag:", { searchTag, boards: boards?.map((board) => ({ id: board.id, tags: board.tags, matches: { tags: board.tags?.some((t) => t.toLowerCase() === searchTag), }, })), }); return ( boards?.filter((board) => board.tags?.some((t) => t.toLowerCase() === searchTag) ) || [] ); }, []); const filterByLanguage = useCallback((boards) => { return ( boards?.filter((board) => board.tags?.some((tag) => tag.startsWith("language:")) ) || [] ); }, []); const filterByVision = useCallback((boards) => { return ( boards?.filter((board) => board.tags?.some( (tag) => tag === "modality:video" || tag === "modality:image" ) ) || [] ); }, []); // Helper function to get the category group of a section const getSectionGroup = (id) => { const groups = { agentic: ["agentic"], capabilities: ["code", "math"], languages: ["language"], modalities: ["vision", "audio"], threeD: ["threeD"], domains: ["financial", "medical", "legal"], evaluation: ["safety"], misc: ["uncategorized"], }; for (const [group, ids] of Object.entries(groups)) { if (ids.includes(id)) { return group; } } return "misc"; }; // Define sections with raw data const allSections = useMemo(() => { if (!leaderboards) return []; // Garder une trace des leaderboards déjà catégorisés const categorizedIds = new Set(); const sections = [ { id: "agentic", title: "Agentic", data: filterByTag("modality:agent", leaderboards).map((board) => { categorizedIds.add(board.id); return board; }), }, { id: "code", title: "Code", data: filterByTag("eval:code", leaderboards).map((board) => { categorizedIds.add(board.id); return board; }), }, { id: "math", title: "Math", data: filterByTag("eval:math", leaderboards).map((board) => { categorizedIds.add(board.id); return board; }), }, { id: "language", title: "Language Specific", data: filterByLanguage(leaderboards).map((board) => { categorizedIds.add(board.id); return board; }), }, { id: "vision", title: "Vision", data: filterByVision(leaderboards).map((board) => { categorizedIds.add(board.id); return board; }), }, { id: "audio", title: "Audio", data: filterByTag("modality:audio", leaderboards).map((board) => { categorizedIds.add(board.id); return board; }), }, { id: "threeD", title: "3D", data: filterByTag("modality:3d", leaderboards).map((board) => { categorizedIds.add(board.id); return board; }), }, { id: "financial", title: "Financial", data: filterByTag("domain:financial", leaderboards).map((board) => { categorizedIds.add(board.id); return board; }), }, { id: "medical", title: "Medical", data: filterByTag("domain:medical", leaderboards).map((board) => { categorizedIds.add(board.id); return board; }), }, { id: "legal", title: "Legal", data: filterByTag("domain:legal", leaderboards).map((board) => { categorizedIds.add(board.id); return board; }), }, { id: "safety", title: "Safety", data: filterByTag("eval:safety", leaderboards).map((board) => { categorizedIds.add(board.id); return board; }), }, { id: "uncategorized", title: "Uncategorized", // Mettre dans uncategorized uniquement les leaderboards qui n'apparaissent nulle part ailleurs data: leaderboards.filter((board) => !categorizedIds.has(board.id)), }, ]; return sections; }, [leaderboards, filterByTag, filterByLanguage, filterByVision]); // Get sections with data const sections = useMemo(() => { console.log("Starting sections filtering..."); console.log( "All sections before filtering:", allSections.map((s) => ({ title: s.title, count: s.data.length, ids: s.data.map((b) => b.id), })) ); // On garde une trace des titres déjà vus const seenTitles = new Set(); const filteredSections = allSections.filter((section) => { console.log(`\nAnalyzing section ${section.title}:`, { data: section.data, count: section.data.length, uniqueIds: new Set(section.data.map((b) => b.id)).size, boards: section.data.map((board) => ({ id: board.id, tags: board.tags, })), }); // On garde la section si elle a des données et qu'on ne l'a pas déjà vue if (section.data.length > 0 && !seenTitles.has(section.title)) { seenTitles.add(section.title); return true; } return false; }); console.log( "\nFinal sections after filtering:", filteredSections.map((s) => ({ title: s.title, count: s.data.length, uniqueIds: new Set(s.data.map((b) => b.id)).size, })) ); return filteredSections; }, [allSections]); // Get filtered count const filteredCount = useMemo(() => { return filterLeaderboardsForCount(leaderboards).length; }, [filterLeaderboardsForCount, leaderboards]); // Function to get highlighted parts of text const getHighlightedText = useCallback((text, searchTerm) => { if (!searchTerm || !text) return { text, shouldHighlight: false }; const query = searchTerm.toLowerCase(); const searchableTagPrefixes = [ "domain:", "language:", "judge:", "test:", "modality:", "submission:", ]; // Si c'est une recherche par tag, on ne highlight rien if (searchableTagPrefixes.some((prefix) => query.startsWith(prefix))) { return { text, shouldHighlight: false }; } // Sinon on highlight les parties qui matchent const index = text.toLowerCase().indexOf(query); if (index === -1) return { text, shouldHighlight: false }; return { text, shouldHighlight: true, highlightStart: index, highlightEnd: index + query.length, }; }, []); const value = { leaderboards, setLeaderboards, searchQuery, setSearchQuery, arenaOnly, setArenaOnly, totalLeaderboards, filteredCount, filterLeaderboards, filterLeaderboardsForCount, sections, allSections, getHighlightedText, selectedCategory, setSelectedCategory: handleCategorySelection, selectedLanguage, setSelectedLanguage: handleLanguageSelection, expandedSections, setExpandedSections, getLanguageStats, getSectionLeaderboards, }; return ( {children} ); }; export const useLeaderboard = () => { const context = useContext(LeaderboardContext); if (!context) { throw new Error("useLeaderboard must be used within a LeaderboardProvider"); } return context; };